diff options
31 files changed, 1084 insertions, 751 deletions
diff --git a/crawl-ref/source/AppHdr.h b/crawl-ref/source/AppHdr.h index eaa1fa23a6..976e3fc121 100644 --- a/crawl-ref/source/AppHdr.h +++ b/crawl-ref/source/AppHdr.h @@ -96,7 +96,7 @@ #endif #define FILE_SEPARATOR '/' -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL #define USE_CURSES #endif diff --git a/crawl-ref/source/Makefile b/crawl-ref/source/Makefile index 2ee432fc65..fff221bf8d 100644 --- a/crawl-ref/source/Makefile +++ b/crawl-ref/source/Makefile @@ -319,7 +319,7 @@ ifdef WIN32 EXTRA_OBJECTS += icon.o endif -ifndef TILES_ANY +ifndef TILES ifdef NEED_LIBW32C OBJECTS += libw32c.o else diff --git a/crawl-ref/source/Makefile.obj b/crawl-ref/source/Makefile.obj index 76ff5b07aa..f1ec05c8b9 100644 --- a/crawl-ref/source/Makefile.obj +++ b/crawl-ref/source/Makefile.obj @@ -268,7 +268,6 @@ endif ifdef WEBTILES OBJECTS += \ -libweb.o \ tiledoll.o \ tilemcache.o \ tilepick.o \ diff --git a/crawl-ref/source/cio.cc b/crawl-ref/source/cio.cc index 8606113fe7..c5aff06e01 100644 --- a/crawl-ref/source/cio.cc +++ b/crawl-ref/source/cio.cc @@ -21,7 +21,7 @@ extern int unixcurses_get_vi_key(int keyin); static keycode_type _numpad2vi(keycode_type key) { -#if defined(UNIX) && !defined(USE_TILE) +#if defined(UNIX) && !defined(USE_TILE_LOCAL) key = unixcurses_get_vi_key(key); #endif switch (key) @@ -30,7 +30,7 @@ static keycode_type _numpad2vi(keycode_type key) case CK_DOWN: key = 'j'; break; case CK_LEFT: key = 'h'; break; case CK_RIGHT: key = 'l'; break; -#if defined(UNIX) && !defined(USE_TILE) +#if defined(UNIX) && !defined(USE_TILE_LOCAL) case -1001: key = 'b'; break; case -1002: key = 'j'; break; case -1003: key = 'n'; break; @@ -84,7 +84,7 @@ int unmangle_direction_keys(int keyin, KeymapContext keymap, case '8': return 'k'; case '9': return 'u'; - #ifndef USE_TILE + #ifndef USE_TILE_LOCAL default: return unixcurses_get_vi_key(keyin); #endif @@ -107,7 +107,7 @@ int unmangle_direction_keys(int keyin, KeymapContext keymap, // cursoring over darkgrey or black causes problems. void cursorxy(int x, int y) { -#if defined(USE_TILE) +#if defined(USE_TILE_LOCAL) coord_def ep(x, y); coord_def gc = crawl_view.screen2grid(ep); tiles.place_cursor(CURSOR_MOUSE, gc); diff --git a/crawl-ref/source/colour.cc b/crawl-ref/source/colour.cc index c95758b061..8bb36fcfa3 100644 --- a/crawl-ref/source/colour.cc +++ b/crawl-ref/source/colour.cc @@ -802,7 +802,7 @@ unsigned real_colour(unsigned raw_colour, const coord_def& loc) if (_is_element_colour(raw_colour)) raw_colour = colflags | element_colour(raw_colour, false, loc); -#if defined(TARGET_OS_WINDOWS) || defined(USE_TILE) +#if defined(TARGET_OS_WINDOWS) || defined(USE_TILE_LOCAL) if (colflags) { unsigned brand = _colflag2brand(colflags); diff --git a/crawl-ref/source/directn.cc b/crawl-ref/source/directn.cc index d979e93648..b839e2971a 100644 --- a/crawl-ref/source/directn.cc +++ b/crawl-ref/source/directn.cc @@ -114,7 +114,7 @@ static bool _find_feature(const coord_def& where, int mode, bool need_path, static bool _find_fprop_unoccupied(const coord_def& where, int mode, bool need_path, int range, targetter *hitfunc); -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL static bool _find_mlist(const coord_def& where, int mode, bool need_path, int range, targetter *hitfunc); #endif @@ -424,7 +424,7 @@ void direction_chooser::describe_cell() const flush_prev_message(); } -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL static void _draw_ray_glyph(const coord_def &pos, int colour, int glych, int mcol) { @@ -657,7 +657,7 @@ void full_describe_view() // (A) An angel (neutral), wielding a glowing long sword std::string prefix = ""; -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL glyph g = get_mons_glyph(mi->mon()); const std::string col_string = colour_to_str(g.col); prefix = "(<" + col_string + ">" @@ -673,7 +673,7 @@ void full_describe_view() if (mi->dam != MDAM_OKAY) str += ", " + mi->damage_desc(); -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL // Wraparound if the description is longer than allowed. linebreak_string(str, get_number_of_cols() - 9); #endif @@ -684,7 +684,7 @@ void full_describe_view() { if (j == 0) me = new MonsterMenuEntry(prefix+str, mi->mon(), hotkey++); -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL else { str = " " + fss[j].tostring(); @@ -710,7 +710,7 @@ void full_describe_view() for (unsigned int i = 0; i < all_items.size(); ++i, hotkey++) { InvEntry *me = all_items[i]; -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL // Show glyphs only for ASCII. me->set_show_glyph(true); #endif @@ -729,7 +729,7 @@ void full_describe_view() { const coord_def c = list_features[i]; std::string desc = ""; -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL glyph g = get_cell_glyph(c); const std::string colour_str = colour_to_str(g.col); desc = "(<" + colour_str + ">"; @@ -825,7 +825,7 @@ void full_describe_view() } } -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL if (!list_items.empty()) { // Unset show_glyph for other menus. @@ -833,7 +833,8 @@ void full_describe_view() me->set_show_glyph(false); delete me; } -#else +#endif +#ifdef USE_TILE // Clear cursor placement. tiles.place_cursor(CURSOR_TUTORIAL, NO_CURSOR); tiles.clear_text_tags(TAG_TUTORIAL); @@ -862,7 +863,7 @@ void full_describe_view() // //-------------------------------------------------------------- -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL // XXX: Hack - can't pass mlist entries into _find_mlist(). bool mlist_full_info; std::vector<monster_info> mlist; @@ -1041,7 +1042,7 @@ static std::string _targ_mode_name(targ_mode_type mode) } } -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL static void _update_mlist(bool enable) { crawl_state.mlist_targeting = enable; @@ -1163,7 +1164,7 @@ void direction_chooser::draw_beam_if_needed() if (!show_beam) { viewwindow( -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL false #endif ); @@ -1179,7 +1180,7 @@ void direction_chooser::draw_beam_if_needed() if (!hitfunc->valid_aim(target())) { #ifdef USE_TILE - viewwindow(); + viewwindow(false, true); #endif return; } @@ -1190,7 +1191,8 @@ void direction_chooser::draw_beam_if_needed() { #ifdef USE_TILE tile_place_ray(*ri, aff); -#else +#endif +#ifndef USE_TILE_LOCAL int bcol = BLACK; if (aff < 0) bcol = DARKGREY; @@ -1202,7 +1204,7 @@ void direction_chooser::draw_beam_if_needed() #endif } #ifdef USE_TILE - viewwindow(); + viewwindow(false, true); #endif return; } @@ -1212,7 +1214,7 @@ void direction_chooser::draw_beam_if_needed() { #ifdef USE_TILE // Clear the old beam if we're not drawing anything else. - viewwindow(); + viewwindow(false, true); #endif return; } @@ -1236,7 +1238,8 @@ void direction_chooser::draw_beam_if_needed() const bool inrange = in_range(p); #ifdef USE_TILE tile_place_ray(p, inrange ? AFF_YES : AFF_NO); -#else +#endif +#ifndef USE_TILE_LOCAL const int bcol = inrange ? MAGENTA : DARKGREY; _draw_ray_glyph(p, bcol, '*', bcol | COLFLAG_REVERSE); #endif @@ -1246,7 +1249,7 @@ void direction_chooser::draw_beam_if_needed() tile_place_ray(target(), in_range(ray.pos()) ? AFF_YES : AFF_NO); // In tiles, we need to refresh the window to get the beam drawn. - viewwindow(); + viewwindow(false, true); #endif } @@ -1837,7 +1840,7 @@ command_type direction_chooser::massage_command(command_type key_command) const void direction_chooser::handle_mlist_cycle_command(command_type key_command) { -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL if (key_command >= CMD_TARGET_CYCLE_MLIST && key_command <= CMD_TARGET_CYCLE_MLIST_END) { @@ -1904,7 +1907,7 @@ bool direction_chooser::do_main_loop() { case CMD_TARGET_SHOW_PROMPT: describe_cell(); break; -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL case CMD_TARGET_TOGGLE_MLIST: Options.mlist_targeting = !Options.mlist_targeting; _update_mlist(Options.mlist_targeting); @@ -2053,7 +2056,7 @@ void direction_chooser::finalize_moves() bool direction_chooser::choose_direction() { -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL if (may_target_monster && restricts != DIR_DIR && Options.mlist_targeting) _update_mlist(true); #endif @@ -2100,7 +2103,7 @@ bool direction_chooser::choose_direction() ; msgwin_set_temporary(false); -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL _update_mlist(false); #endif finalize_moves(); @@ -2314,7 +2317,7 @@ static bool _mons_is_valid_target(const monster* mon, int mode, int range) return (true); } -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL static bool _find_mlist(const coord_def& where, int idx, bool need_path, int range, targetter *hitfunc) { @@ -4033,7 +4036,7 @@ command_type targeting_behaviour::get_command(int key) if (cmd >= CMD_MIN_TARGET && cmd < CMD_TARGET_CYCLE_TARGET_MODE) return (cmd); -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL // Overrides the movement keys while mlist_targeting is active. if (crawl_state.mlist_targeting && isalower(key)) return static_cast<command_type>(CMD_TARGET_CYCLE_MLIST + (key - 'a')); diff --git a/crawl-ref/source/directn.h b/crawl-ref/source/directn.h index 33f6c06eb5..4dc350b77a 100644 --- a/crawl-ref/source/directn.h +++ b/crawl-ref/source/directn.h @@ -272,7 +272,7 @@ enum mons_equip_desc_level_type DESC_IDENTIFIED, }; -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL char mlist_index_to_letter(int index); #endif diff --git a/crawl-ref/source/godabil.cc b/crawl-ref/source/godabil.cc index fbb978deff..dffa98e8a5 100644 --- a/crawl-ref/source/godabil.cc +++ b/crawl-ref/source/godabil.cc @@ -2482,11 +2482,12 @@ bool fedhas_plant_ring_from_fruit() for (int i = 0; i < max_use; ++i) { -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL coord_def temp = grid2view(adjacent[i]); cgotoxy(temp.x, temp.y, GOTO_DNGN); put_colour_ch(GREEN, '1' + i); -#else +#endif +#ifdef USE_TILE tiles.add_overlay(adjacent[i], TILE_INDICATOR + i); #endif } @@ -2504,7 +2505,7 @@ bool fedhas_plant_ring_from_fruit() // The user entered a number, remove all number overlays which // are higher than that number. -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL unsigned not_used = adjacent.size() - unsigned(target_count); for (unsigned i = adjacent.size() - not_used; i < adjacent.size(); @@ -2512,7 +2513,8 @@ bool fedhas_plant_ring_from_fruit() { view_update_at(adjacent[i]); } -#else +#endif +#ifdef USE_TILE // For tiles we have to clear all overlays and redraw the ones // we want. tiles.clear_overlays(); @@ -2680,7 +2682,7 @@ int fedhas_corpse_spores(beh_type behavior, bool interactive) viewwindow(false); for (unsigned i = 0; i < positions.size(); ++i) { -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL coord_def temp = grid2view(positions[i]->pos); cgotoxy(temp.x, temp.y, GOTO_DNGN); @@ -2690,7 +2692,8 @@ int fedhas_corpse_spores(beh_type behavior, bool interactive) unsigned character = mons_char(MONS_GIANT_SPORE); put_colour_ch(color, character); -#else +#endif +#ifdef USE_TILE tiles.add_overlay(positions[i]->pos, TILE_SPORE_OVERLAY); #endif } diff --git a/crawl-ref/source/initfile.cc b/crawl-ref/source/initfile.cc index 6cdeea87e8..efa356a9cc 100644 --- a/crawl-ref/source/initfile.cc +++ b/crawl-ref/source/initfile.cc @@ -26,6 +26,9 @@ #ifdef USE_TILE #include "tilereg-map.h" #endif +#ifdef USE_TILE_WEB + #include "tileweb.h" +#endif #include "invent.h" #include "item_use.h" #include "libutil.h" @@ -3417,6 +3420,10 @@ enum commandline_option_type CLO_ZOTDEF, CLO_TUTORIAL, CLO_WIZARD, +#ifdef USE_TILE_WEB + CLO_WEBTILES_SOCKET, + CLO_AWAIT_CONNECTION, +#endif CLO_NOPS }; @@ -3427,7 +3434,10 @@ static const char *cmd_ops[] = { "mapstat", "arena", "dump-maps", "test", "script", "builddb", "help", "version", "seed", "save-version", "sprint", "extra-opt-first", "extra-opt-last", "sprint-map", "edit-save", - "print-charset", "zotdef", "tutorial", "wizard" + "print-charset", "zotdef", "tutorial", "wizard", +#ifdef USE_TILE_WEB + "webtiles-socket", "await-connection", +#endif }; static const int num_cmd_ops = CLO_NOPS; @@ -4068,6 +4078,17 @@ bool parse_args(int argc, char **argv, bool rc_only) #endif break; +#ifdef USE_TILE_WEB + case CLO_WEBTILES_SOCKET: + nextUsed = true; + tiles.m_sock_name = next_arg; + break; + + case CLO_AWAIT_CONNECTION: + tiles.m_await_connection = true; + break; +#endif + case CLO_PRINT_CHARSET: if (rc_only) break; diff --git a/crawl-ref/source/libunix.cc b/crawl-ref/source/libunix.cc index 80143870bf..deaee2afac 100644 --- a/crawl-ref/source/libunix.cc +++ b/crawl-ref/source/libunix.cc @@ -227,6 +227,17 @@ int getchk() } wint_t c; + +#ifdef USE_TILE_WEB + refresh(); + + tiles.redraw(); + tiles.await_input(c, true); + + if (c > 0) + return c; +#endif + switch (get_wch(&c)) { case ERR: @@ -414,10 +425,18 @@ void console_startup(void) crawl_view.init_geometry(); set_mouse_enabled(false); + +#ifdef USE_TILE_WEB + tiles.resize(); +#endif } void console_shutdown() { +#ifdef USE_TILE_WEB + tiles.shutdown(); +#endif + // resetty(); endwin(); @@ -457,6 +476,13 @@ void putwch(ucs_t chr) c = ' '; // TODO: recognize unsupported characters and try to transliterate addnwstr(&c, 1); + +#ifdef USE_TILE_WEB + ucs_t buf[2]; + buf[0] = chr; + buf[1] = 0; + tiles.put_ucs_string(buf); +#endif } void puttext(int x1, int y1, const crawl_view_buffer &vbuf) @@ -483,6 +509,10 @@ void puttext(int x1, int y1, const crawl_view_buffer &vbuf) void update_screen(void) { refresh(); + +#ifdef USE_TILE_WEB + tiles.set_need_redraw(); +#endif } void clear_to_end_of_line(void) @@ -490,6 +520,10 @@ void clear_to_end_of_line(void) textcolor(LIGHTGREY); textbackground(BLACK); clrtoeol(); + +#ifdef USE_TILE_WEB + tiles.clear_to_end_of_line(); +#endif } int get_number_of_lines(void) @@ -511,6 +545,10 @@ void clrscr() printf("%s", DGL_CLEAR_SCREEN); fflush(stdout); #endif + +#ifdef USE_TILE_WEB + tiles.clrscr(); +#endif } void set_cursor_enabled(bool enabled) @@ -604,6 +642,10 @@ static int curs_fg_attr(int col) void textcolor(int col) { (void)attrset(Current_Colour = curs_fg_attr(col)); + +#ifdef USE_TILE_WEB + tiles.textcolor(col); +#endif } static int curs_bg_attr(int col) @@ -662,6 +704,10 @@ static int curs_bg_attr(int col) void textbackground(int col) { (void)attrset(Current_Colour = curs_bg_attr(col)); + +#ifdef USE_TILE_WEB + tiles.textbackground(col); +#endif } @@ -745,6 +791,11 @@ int wherey() void delay(unsigned int time) { +#ifdef USE_TILE_WEB + tiles.redraw(); + tiles.send_message("delay(%d);", time); +#endif + refresh(); if (time) usleep(time * 1000); @@ -757,6 +808,7 @@ bool kbhit() return true; wint_t c; +#ifndef USE_TILE_WEB int i; nodelay(stdscr, TRUE); @@ -775,4 +827,12 @@ bool kbhit() default: return false; } +#else + bool result = tiles.await_input(c, false); + + if (result && (c != 0)) + pending = c; + + return result; +#endif } diff --git a/crawl-ref/source/libunix.h b/crawl-ref/source/libunix.h index 5f971d41dc..83f20697d5 100644 --- a/crawl-ref/source/libunix.h +++ b/crawl-ref/source/libunix.h @@ -5,7 +5,7 @@ #define O_BINARY 0 #endif -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL #include <stdio.h> diff --git a/crawl-ref/source/libutil.cc b/crawl-ref/source/libutil.cc index 5c5211a98e..ddeddd8cad 100644 --- a/crawl-ref/source/libutil.cc +++ b/crawl-ref/source/libutil.cc @@ -905,7 +905,7 @@ bool version_is_stable(const char *v) } } -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL coord_def cgettopleft(GotoRegion region) { switch (region) @@ -942,13 +942,17 @@ void cgotoxy(int x, int y, GotoRegion region) ASSERT_SAVE(y >= 1 && y <= sz.y); gotoxy_sys(tl.x + x - 1, tl.y + y - 1); + +#ifdef USE_TILE_WEB + tiles.cgotoxy(x, y, region); +#endif } GotoRegion get_cursor_region() { return (_current_region); } -#endif // USE_TILE +#endif // USE_TILE_LOCAL coord_def cgetsize(GotoRegion region) { @@ -1097,7 +1101,7 @@ static void handle_hangup(int) if (crawl_state.seen_hups++) return; -#ifdef USE_TILE +#ifdef USE_TILE_LOCAL // XXX: Will a tiles build ever need to handle the HUP signal? sighup_save_and_exit(); #elif defined(USE_CURSES) diff --git a/crawl-ref/source/libweb.cc b/crawl-ref/source/libweb.cc deleted file mode 100644 index 44478e87c6..0000000000 --- a/crawl-ref/source/libweb.cc +++ /dev/null @@ -1,214 +0,0 @@ -/** - * @file - * @brief Functions that any display port needs to implement -- webtiles version. -**/ - -#include "AppHdr.h" - -#ifdef USE_TILE_WEB - -#include <unistd.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - -#include "cio.h" -#include "defines.h" -#include "env.h" -#include "externs.h" -#include "message.h" -#include "stash.h" -#include "state.h" -#include "stuff.h" -#include "terrain.h" -#include "tiledef-main.h" -#include "travel.h" -#include "viewgeom.h" - -int m_getch() -{ - return getchk(); -} - -void set_mouse_enabled(bool enabled) -{ - crawl_state.mouse_enabled = enabled; -} - -void gui_init_view_params(crawl_view_geometry &geom) -{ - // The tile version handles its own layout on a pixel-by-pixel basis. - // Pretend that all of the regions start at character location (1,1). - - geom.termp.x = 1; - geom.termp.y = 1; - - geom.termsz.x = 80; - geom.termsz.y = 24; - - geom.viewp.x = 1; - geom.viewp.y = 1; - - geom.hudp.x = 1; - geom.hudp.y = 1; - - geom.msgp.x = 1; - geom.msgp.y = 1; - - geom.mlistp.x = 1; - geom.mlistp.y = 1; - geom.mlistsz.x = 0; - geom.mlistsz.y = 0; - - geom.viewsz.x = 17; - geom.viewsz.y = 17; -} - -void putwch(ucs_t chr) -{ - if (!chr) - chr = ' '; - ucs_t buf[2]; - buf[0] = chr; - buf[1] = 0; - tiles.put_ucs_string(buf); -} - -void clear_to_end_of_line() -{ - tiles.clear_to_end_of_line(); -} - -void cprintf(const char *format,...) -{ - char buffer[2048]; // One full screen if no control seq... - va_list argp; - va_start(argp, format); - vsnprintf(buffer, sizeof(buffer), format, argp); - va_end(argp); - tiles.put_string(buffer); -} - -void textcolor(int color) -{ - tiles.textcolor(color); -} - -void textbackground(int bg) -{ - tiles.textbackground(bg); -} - -void set_cursor_enabled(bool enabled) -{ - // if (enabled) - // TextRegion::_setcursortype(1); - // else - // TextRegion::_setcursortype(0); -} - -bool is_cursor_enabled() -{ - // if (TextRegion::cursor_flag) - // return (true); - - return (false); -} - -bool is_smart_cursor_enabled() -{ - return false; -} - -void enable_smart_cursor(bool dummy) -{ -} - -int wherex() -{ - return tiles.wherex(); -} - -int wherey() -{ - return tiles.wherey(); -} - -int get_number_of_lines() -{ - return tiles.get_number_of_lines(); -} - -int get_number_of_cols() -{ - return tiles.get_number_of_cols(); -} - -int getch_ck() -{ - return (tiles.getch_ck()); -} - -int getchk() -{ - return getch_ck(); -} - -void clrscr() -{ - tiles.clrscr(); -} - -void cgotoxy(int x, int y, GotoRegion region) -{ - tiles.cgotoxy(x, y, region); -} - -coord_def cgetpos(GotoRegion region) -{ - ASSERT(region == get_cursor_region()); - return (coord_def(wherex(), wherey())); -} - -GotoRegion get_cursor_region() -{ - return (tiles.get_cursor_region()); -} - -void delay(unsigned int ms) -{ - tiles.redraw(); - fprintf(stdout, "delay(%d);\n", ms); - - if (ms) - usleep(ms * 1000); -} - -void update_screen() -{ - tiles.set_need_redraw(); -} - -bool kbhit() -{ - // Check stdin in a non-blocking way - struct timeval tv; - fd_set fds; - tv.tv_sec = 0; - tv.tv_usec = 0; - FD_ZERO(&fds); - FD_SET(STDIN_FILENO, &fds); //STDIN_FILENO is 0 - select(STDIN_FILENO+1, &fds, NULL, NULL, &tv); - return FD_ISSET(STDIN_FILENO, &fds) != 0; -} - -void console_startup() -{ - tiles.resize(); -} - -void console_shutdown() -{ - tiles.shutdown(); -} -#endif // #ifdef USE_TILE_WEB diff --git a/crawl-ref/source/main.cc b/crawl-ref/source/main.cc index c1f0188ca8..2388575de9 100644 --- a/crawl-ref/source/main.cc +++ b/crawl-ref/source/main.cc @@ -312,10 +312,12 @@ static void _reset_game() note_list.clear(); msg::deinitialise_mpr_streams(); -#ifdef USE_TILE +#ifdef USE_TILE_LOCAL // [ds] Don't show the title screen again, just go back to // the menu. crawl_state.title_screen = false; +#endif +#ifdef USE_TILE tiles.clear_text_tags(TAG_NAMED_MONSTER); #endif } @@ -1008,9 +1010,9 @@ bool apply_berserk_penalty = false; static void _center_cursor() { -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL const coord_def cwhere = crawl_view.grid2screen(you.pos()); - cgotoxy(cwhere.x, cwhere.y); + cgotoxy(cwhere.x, cwhere.y, GOTO_DNGN); #endif } @@ -1227,7 +1229,7 @@ static void _input() crawl_state.waiting_for_command = true; c_input_reset(true); -#ifdef USE_TILE +#ifdef USE_TILE_LOCAL cursor_control con(false); #endif const command_type cmd = _get_next_cmd(); @@ -1717,7 +1719,7 @@ static void _do_display_map() if (Hints.hints_events[HINT_MAP_VIEW]) Hints.hints_events[HINT_MAP_VIEW] = false; -#ifdef USE_TILE +#ifdef USE_TILE_LOCAL // Since there's no actual overview map, but the functionality // exists, give a message to explain what's going on. mpr("Move the cursor to view the level map, or type <w>?</w> for " @@ -1728,7 +1730,7 @@ static void _do_display_map() level_pos pos; const bool travel = show_map(pos, true, true, true); -#ifdef USE_TILE +#ifdef USE_TILE_LOCAL mpr("Returning to the game..."); #endif if (travel) @@ -2032,7 +2034,7 @@ void process_command(command_type cmd) // because we want to have CTRL-Y available... // and unfortunately they tend to be stuck together. clrscr(); -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL console_shutdown(); kill(0, SIGTSTP); console_startup(); diff --git a/crawl-ref/source/output.cc b/crawl-ref/source/output.cc index 14fcfe860d..f7da086626 100644 --- a/crawl-ref/source/output.cc +++ b/crawl-ref/source/output.cc @@ -107,7 +107,7 @@ class colour_bar textcolor(BLACK); for (int cx = 0; cx < width; cx++) { -#ifdef USE_TILE +#ifdef USE_TILE_LOCAL // Maybe this should use textbackground too? textcolor(BLACK + m_empty * 16); @@ -162,7 +162,7 @@ class colour_bar colour_bar HP_Bar(LIGHTGREEN, GREEN, RED, DARKGREY); -#ifdef USE_TILE +#ifdef USE_TILE_LOCAL colour_bar MP_Bar(BLUE, BLUE, LIGHTBLUE, DARKGREY); #else colour_bar MP_Bar(LIGHTBLUE, BLUE, MAGENTA, DARKGREY); @@ -684,7 +684,7 @@ static void _print_status_lights(int y) } } -#ifdef USE_TILE +#ifdef USE_TILE_LOCAL static bool _need_stats_printed() { return you.redraw_hit_points @@ -714,7 +714,7 @@ void print_stats(void) if (MP_Bar.wants_redraw()) you.redraw_magic_points = true; -#ifdef USE_TILE +#ifdef USE_TILE_LOCAL bool has_changed = _need_stats_printed(); #endif @@ -806,7 +806,7 @@ void print_stats(void) } textcolor(LIGHTGREY); -#ifdef USE_TILE +#ifdef USE_TILE_LOCAL if (has_changed) update_screen(); #else @@ -1085,7 +1085,7 @@ std::string mpr_monster_list(bool past) return (msg); } -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL static void _print_next_monster_desc(const std::vector<monster_info>& mons, int& start, bool zombified = false, int idx = -1) @@ -1190,7 +1190,7 @@ static void _print_next_monster_desc(const std::vector<monster_info>& mons, } #endif -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL // #define BOTTOM_JUSTIFY_MONSTER_LIST // Returns -1 if the monster list is empty, 0 if there are so many monsters // they have to be consolidated, and 1 otherwise. @@ -1879,7 +1879,7 @@ static char _get_overview_screen_results() formatted_scroller overview; overview.set_flags(MF_SINGLESELECT | MF_ALWAYS_SHOW_MORE | MF_NOWRAP); overview.set_more(formatted_string::parse_string( -#ifdef USE_TILE +#ifdef USE_TILE_LOCAL "<cyan>[ +/L-click : Page down. - : Page up." " Esc/R-click exits.]")); #else diff --git a/crawl-ref/source/state.cc b/crawl-ref/source/state.cc index facc854f29..b2b482677b 100644 --- a/crawl-ref/source/state.cc +++ b/crawl-ref/source/state.cc @@ -38,7 +38,7 @@ game_state::game_state() repeat_cmd(CMD_NO_CMD),cmd_repeat_started_unsafe(false), lua_calls_no_turn(0), stat_gain_prompt(false), level_annotation_shown(false), -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL mlist_targeting(false), #else title_screen(true), diff --git a/crawl-ref/source/state.h b/crawl-ref/source/state.h index d58bf068b7..5714ab1577 100644 --- a/crawl-ref/source/state.h +++ b/crawl-ref/source/state.h @@ -89,7 +89,7 @@ struct game_state bool level_annotation_shown; -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL // Are we currently targeting using the mlist? // This is global because the monster pane uses this when // drawing. diff --git a/crawl-ref/source/tileweb.cc b/crawl-ref/source/tileweb.cc index 0d3cf87aed..58021f9706 100644 --- a/crawl-ref/source/tileweb.cc +++ b/crawl-ref/source/tileweb.cc @@ -33,7 +33,11 @@ #include "viewgeom.h" #include <sys/time.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/un.h> #include <stdarg.h> +#include <errno.h> @@ -49,7 +53,8 @@ static unsigned int get_milliseconds() TilesFramework tiles; TilesFramework::TilesFramework() - : m_view_loaded(false), + : m_crt_enabled(true), + m_view_loaded(false), m_next_view_tl(0, 0), m_next_view_br(-1, -1), m_current_flash_colour(BLACK), @@ -72,6 +77,8 @@ TilesFramework::~TilesFramework() void TilesFramework::shutdown() { + close(m_sock); + remove(m_sock_name.c_str()); } void TilesFramework::draw_doll_edit() @@ -80,6 +87,30 @@ void TilesFramework::draw_doll_edit() bool TilesFramework::initialise() { + // Init socket + m_sock = socket(PF_UNIX, SOCK_DGRAM, 0); + if (m_sock < 0) + { + die("Can't open the webtiles socket!"); + } + sockaddr_un addr; + addr.sun_family = AF_UNIX; + strcpy(addr.sun_path, m_sock_name.c_str()); + if (bind(m_sock, (sockaddr*) &addr, sizeof (sockaddr_un))) + { + die("Can't bind the webtiles socket!"); + } + + int bufsize = 64 * 1024; + if (setsockopt(m_sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof (bufsize))) + { + die("Can't set buffer size!"); + } + m_max_msg_size = bufsize; + + if (m_await_connection) + _await_connection(); + std::string title = CRAWL " " + Version::Long(); send_message("document.title = \"%s\";", title.c_str()); @@ -95,36 +126,196 @@ bool TilesFramework::initialise() void TilesFramework::write_message(const char *format, ...) { for (unsigned int i = 0; i < m_prefixes.size(); ++i) - fputs(m_prefixes[i].c_str(), stdout); + m_msg_buf.append(m_prefixes[i].data()); m_prefixes.clear(); + char buf[2048]; + int len; + va_list argp; va_start(argp, format); - vfprintf(stdout, format, argp); + if ((len = vsnprintf(buf, sizeof (buf), format, argp)) >= sizeof (buf)) + die("Webtiles message too long! (%d)", len); va_end(argp); + + m_msg_buf.append(buf); } void TilesFramework::finish_message() { - fprintf(stdout, "\n"); - fflush(stdout); + m_msg_buf.append("\n"); + const char* fragment_start = m_msg_buf.data(); + const char* data_end = m_msg_buf.data() + m_msg_buf.size(); + while (fragment_start < data_end) + { + int fragment_size = data_end - fragment_start; + if (fragment_size > m_max_msg_size) + fragment_size = m_max_msg_size; + + for (unsigned int i = 0; i < m_dest_addrs.size(); ++i) + { + if (sendto(m_sock, fragment_start, fragment_size, 0, + (sockaddr*) &m_dest_addrs[i], sizeof (sockaddr_un)) == -1) + { + if (errno == ECONNREFUSED || errno == ENOENT) + { + // the other side is dead + m_dest_addrs.erase(m_dest_addrs.begin() + i); + i--; + } + else + die("Socket write error: %s", strerror(errno)); + } + } + + fragment_start += fragment_size; + } m_prefixes.clear(); + m_msg_buf.clear(); } void TilesFramework::send_message(const char *format, ...) { for (unsigned int i = 0; i < m_prefixes.size(); ++i) - fputs(m_prefixes[i].c_str(), stdout); + m_msg_buf.append(m_prefixes[i].data()); m_prefixes.clear(); + char buf[2048]; + int len; + va_list argp; va_start(argp, format); - vfprintf(stdout, format, argp); + if ((len = vsnprintf(buf, sizeof (buf), format, argp)) >= sizeof (buf)) + die("Webtiles message too long! (%d)", len); va_end(argp); + m_msg_buf.append(buf); + finish_message(); } +void TilesFramework::_await_connection() +{ + while (m_dest_addrs.size() == 0) + { + _receive_control_message(); + } +} + +wint_t TilesFramework::_receive_control_message() +{ + char buf[4096]; // Should be enough for client->server messages + sockaddr_un srcaddr; + socklen_t srcaddr_len; + + srcaddr_len = sizeof (srcaddr); + + int len = recvfrom(m_sock, buf, sizeof (buf), + 0, + (sockaddr *) &srcaddr, &srcaddr_len); + + if (len == -1) + { + die("Socket read error: %s", strerror(errno)); + } + + std::string data(buf, len); + return _handle_control_message(srcaddr, data); +} + +wint_t TilesFramework::_handle_control_message(sockaddr_un addr, std::string data) +{ + // Hack - this needs a real JSON parser + + static const std::string keymsgstart("{\"msg\":\"key\",\"keycode\":"); + + int c = 0; + + if (data == "{\"msg\":\"attach\"}") + { + m_dest_addrs.push_back(addr); + } + else if (data.compare(0, keymsgstart.size(), keymsgstart) == 0) + { + std::stringstream ss(data); + ss.ignore(keymsgstart.size()); + + ss >> c; + + if (ss.fail()) + c = 0; + } + else if (data == "{\"msg\":\"spectator_joined\"}") + { + _send_everything(); + } + + return c; +} + +bool TilesFramework::await_input(wint_t& c, bool block) +{ + int result; + fd_set fds; + int maxfd = m_sock; + + while (true) + { + do + { + FD_ZERO(&fds); + FD_SET(STDIN_FILENO, &fds); + FD_SET(m_sock, &fds); + + if (block) + { + result = select(maxfd + 1, &fds, NULL, NULL, NULL); + } + else + { + timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 0; + + result = select(maxfd + 1, &fds, NULL, NULL, &timeout); + } + } + while (result == -1 && errno == EINTR); + + if (result == 0) + { + return false; + } + else if (result > 0) + { + if (FD_ISSET(m_sock, &fds)) + { + c = _receive_control_message(); + + if (c > 0) + return true; + } + + if (FD_ISSET(STDIN_FILENO, &fds)) + { + c = 0; + return true; + } + } + else if (errno == EBADF) + { + // This probably means that stdin got closed because of a + // SIGHUP. We'll just return. + c = 0; + return false; + } + else + { + die("select error: %s", strerror(errno)); + } + } +} + void TilesFramework::push_prefix(std::string prefix) { m_prefixes.push_back(prefix); @@ -612,131 +803,65 @@ void TilesFramework::load_dungeon(const coord_def &cen) unwind_var<coord_def> vlos1(crawl_view.vlos1); unwind_var<coord_def> vlos2(crawl_view.vlos2); + m_next_gc = cen; + crawl_view.calc_vlos(); - viewwindow(false); + viewwindow(false, true); place_cursor(CURSOR_MAP, cen); } static const int min_stat_height = 12; static const int stat_width = 42; -static void _send_layout_data(bool need_response) +static void _send_layout_data() { - // need_response indicates if the client needs to set a layout tiles.send_message("layout({view_max_width:%u,view_max_height:%u,\ force_overlay:%u,show_diameter:%u,msg_min_height:%u,stat_width:%u, \ -min_stat_height:%u,gxm:%u,gym:%u},%u);", +min_stat_height:%u,gxm:%u,gym:%u});", Options.view_max_width, Options.view_max_height, Options.tile_force_overlay, ENV_SHOW_DIAMETER, Options.msg_min_height, stat_width, min_stat_height + (Options.show_gold_turns ? 1 : 0), - GXM, GYM, - need_response); + GXM, GYM); } void TilesFramework::resize() { - // Width of status area in characters. - crawl_view.hudsz.x = stat_width; - crawl_view.msgsz.y = Options.msg_min_height; - m_text_message.resize(crawl_view.msgsz.x, crawl_view.msgsz.y); - - crawl_view.viewsz = coord_def(ENV_SHOW_DIAMETER, ENV_SHOW_DIAMETER); - crawl_view.init_view(); - // Send the client the necessary data to do the layout - _send_layout_data(true); + _send_layout_data(); - // Now wait for the response - getch_ck(); + m_text_message.resize(crawl_view.msgsz.x, crawl_view.msgsz.y); + m_text_stat.resize(crawl_view.hudsz.x, crawl_view.hudsz.y); + m_text_crt.resize(crawl_view.termsz.x, crawl_view.termsz.y); } -int TilesFramework::getch_ck() +/* + Send everything a newly joined spectator needs + */ +void TilesFramework::_send_everything() { - m_text_crt.send(); - m_text_stat.send(); - m_text_message.send(); - - if (need_redraw()) - redraw(); - - int key = getchar(); - if (key == '\\') - { - // Char encoded as a number - char data[10]; - fgets(data, 10, stdin); - return atoi(data); - } - else if (key == '^') + std::string title = CRAWL " " + Version::Long(); + send_message("document.title = \"%s\";", title.c_str()); + m_text_crt.send(true); + m_text_stat.send(true); + m_text_message.send(true); + _send_layout_data(); + send_message("vgrdc(%d,%d);", + m_current_gc.x - m_origin.x, m_current_gc.y - m_origin.y); + send_message("set_flash(%d);", m_current_flash_colour); + _send_map(true); + switch (m_active_layer) { - // Control messages - // TODO: This would be much nicer if we just sent messages in JSON - int msg = getchar(); - int num = 0; - if (msg == 'w' || msg == 'h' || msg == 's' - || msg == 'W' || msg == 'H' || msg == 'm') - { - // Read the number - char data[10]; - fgets(data, 10, stdin); - num = atoi(data); - } - switch (msg) - { - case 's': // Set height of the stats area - if (num <= 0) num = 1; - if (num > 400) num = 400; - crawl_view.hudsz.y = num; - m_text_stat.resize(crawl_view.hudsz.x, crawl_view.hudsz.y); - break; - case 'W': // Set width of CRT - if (num <= 0) num = 1; - if (num > 400) num = 400; - crawl_view.termsz.x = num; - m_text_crt.resize(crawl_view.termsz.x, crawl_view.termsz.y); - break; - case 'H': // Set height of CRT - if (num <= 0) num = 1; - if (num > 400) num = 400; - crawl_view.termsz.y = num; - m_text_crt.resize(crawl_view.termsz.x, crawl_view.termsz.y); - break; - case 'm': // Set width of the message view - if (num <= 0) num = 1; - if (num > 400) num = 400; - crawl_view.msgsz.x = num; - m_text_message.resize(crawl_view.msgsz.x, crawl_view.msgsz.y); - break; - case 'r': // A spectator joined, resend the necessary data - std::string title = CRAWL " " + Version::Long(); - send_message("document.title = \"%s\";", title.c_str()); - m_text_crt.send(true); - m_text_stat.send(true); - m_text_message.send(true); - _send_layout_data(false); - send_message("vgrdc(%d,%d);", - m_current_gc.x - m_origin.x, m_current_gc.y - m_origin.y); - send_message("set_flash(%d);", m_current_flash_colour); - _send_map(true); - switch (m_active_layer) - { - case LAYER_CRT: - send_message("set_layer('crt');"); - break; - case LAYER_NORMAL: - send_message("set_layer('normal');"); - break; - default: - // Cannot happen - break; - } - break; - } - - return getch_ck(); + case LAYER_CRT: + send_message("set_layer('crt');"); + break; + case LAYER_NORMAL: + send_message("set_layer('normal');"); + break; + default: + // Cannot happen + break; } - return key; } void TilesFramework::clrscr() @@ -744,30 +869,20 @@ void TilesFramework::clrscr() // TODO: Clear cursor m_text_crt.clear(); - m_text_message.clear(); - m_text_stat.clear(); - cgotoxy(1, 1); + this->cgotoxy(1, 1); set_need_redraw(); } -int TilesFramework::get_number_of_lines() +void TilesFramework::set_crt_enabled(bool value) { - return m_text_crt.my; + m_crt_enabled = value; } -int TilesFramework::get_number_of_cols() +bool TilesFramework::is_crt_enabled() { - switch (m_active_layer) - { - default: - return 0; - case LAYER_NORMAL: - return m_text_message.mx; - case LAYER_CRT: - return m_text_crt.mx; - } + return m_crt_enabled; } void TilesFramework::cgotoxy(int x, int y, GotoRegion region) @@ -777,10 +892,17 @@ void TilesFramework::cgotoxy(int x, int y, GotoRegion region) switch (region) { case GOTO_CRT: - if (m_active_layer != LAYER_CRT) - send_message("set_layer(\"crt\");"); - m_active_layer = LAYER_CRT; - m_print_area = &m_text_crt; + if (m_crt_enabled > 0) + { + if (m_active_layer != LAYER_CRT) + send_message("set_layer(\"crt\");"); + m_active_layer = LAYER_CRT; + m_print_area = &m_text_crt; + } + else + { + m_print_area = NULL; + } break; case GOTO_MSG: if (m_active_layer != LAYER_NORMAL) @@ -795,42 +917,40 @@ void TilesFramework::cgotoxy(int x, int y, GotoRegion region) m_print_area = &m_text_stat; break; default: - die("invalid cgotoxy region in webtiles: %d", region); + m_print_area = NULL; break; } m_cursor_region = region; } -GotoRegion TilesFramework::get_cursor_region() const -{ - return m_cursor_region; -} - void TilesFramework::redraw() { m_text_crt.send(); m_text_stat.send(); m_text_message.send(); - if (m_current_gc != m_next_gc) + if (m_need_redraw) { - if (m_origin.equals(-1, -1)) - m_origin = m_next_gc; - write_message("vgrdc(%d,%d);", - m_next_gc.x - m_origin.x, - m_next_gc.y - m_origin.y); - m_current_gc = m_next_gc; - } + if (m_current_gc != m_next_gc) + { + if (m_origin.equals(-1, -1)) + m_origin = m_next_gc; + write_message("vgrdc(%d,%d);", + m_next_gc.x - m_origin.x, + m_next_gc.y - m_origin.y); + m_current_gc = m_next_gc; + } - if (m_current_flash_colour != m_next_flash_colour) - { - write_message("set_flash(%d);", - m_next_flash_colour); - m_current_flash_colour = m_next_flash_colour; - } + if (m_current_flash_colour != m_next_flash_colour) + { + write_message("set_flash(%d);", + m_next_flash_colour); + m_current_flash_colour = m_next_flash_colour; + } - if (m_view_loaded) - _send_map(false); + if (m_view_loaded) + _send_map(false); + } m_need_redraw = false; m_last_tick_redraw = get_milliseconds(); @@ -1013,6 +1133,9 @@ void TilesFramework::put_string(char *buffer) void TilesFramework::put_ucs_string(ucs_t *str) { + if (m_print_area == NULL) + return; + while (*str) { if (*str == '\r') @@ -1048,6 +1171,9 @@ void TilesFramework::put_ucs_string(ucs_t *str) void TilesFramework::clear_to_end_of_line() { + if (m_print_area == NULL) + return; + for (int x = m_print_x; x < m_print_area->mx; ++x) m_print_area->put_character(' ', m_print_fg, m_print_bg, x, m_print_y); } diff --git a/crawl-ref/source/tileweb.h b/crawl-ref/source/tileweb.h index fd34a1e691..9c08072928 100644 --- a/crawl-ref/source/tileweb.h +++ b/crawl-ref/source/tileweb.h @@ -12,6 +12,7 @@ #include "viewgeom.h" #include "map_knowledge.h" #include <map> +#include <sys/un.h> class TilesFramework { @@ -28,9 +29,6 @@ public: void clrscr(); void cgotoxy(int x, int y, GotoRegion region = GOTO_CRT); - GotoRegion get_cursor_region() const; - int get_number_of_lines(); - int get_number_of_cols(); void update_minimap(const coord_def &gc); void clear_minimap(); @@ -60,13 +58,30 @@ public: void put_string(char *str); void put_ucs_string(ucs_t *str); void clear_to_end_of_line(); - int wherex() { return m_print_x + 1; } - int wherey() { return m_print_y + 1; } void write_message(const char *format, ...); void finish_message(); void send_message(const char *format, ...); + /* Webtiles can receive input both via stdin, and on the + socket. Also, while waiting for input, it should be + able to handle other control messages (for example, + requests to re-send data when a new spectator joins). + + This function waits until input is available either via + stdin or from a control message. If the input came from + a control message, it will be written into c; otherwise, + it still has to be read from stdin. + + If block is false, await_input will immediately return, + even if no input is available. The return value indicates + whether input can be read from stdin; c will be non-zero + if input came via a control message. + */ + bool await_input(wint_t& c, bool block); + + void check_for_control_messages(); + /* Adds a prefix that will be written before any other data that is sent after this call, unless no other data is sent until pop_prefix is called. The suffix @@ -75,7 +90,23 @@ public: void push_prefix(std::string prefix); void pop_prefix(std::string suffix); bool prefix_popped(); + + std::string m_sock_name; + bool m_await_connection; + + void set_crt_enabled(bool value); + bool is_crt_enabled(); + protected: + int m_sock; + int m_max_msg_size; + std::string m_msg_buf; + std::vector<sockaddr_un> m_dest_addrs; + + void _await_connection(); + wint_t _handle_control_message(sockaddr_un addr, std::string data); + wint_t _receive_control_message(); + std::vector<std::string> m_prefixes; enum LayerID @@ -86,6 +117,7 @@ protected: LAYER_MAX, }; LayerID m_active_layer; + bool m_crt_enabled; unsigned int m_last_tick_redraw; bool m_need_redraw; @@ -126,6 +158,8 @@ protected: dolls_data last_player_doll; + void _send_everything(); + void _send_map(bool force_full = false); void _send_cell(const coord_def &gc, const screen_cell_t ¤t_sc, const screen_cell_t &next_sc, @@ -140,4 +174,22 @@ protected: // Main interface for tiles functions extern TilesFramework tiles; +class tiles_crt_control +{ +public: + tiles_crt_control(bool crt_enabled) + : m_was_enabled(tiles.is_crt_enabled()) + { + tiles.set_crt_enabled(crt_enabled); + } + + ~tiles_crt_control() + { + tiles.set_crt_enabled(m_was_enabled); + } + +private: + bool m_was_enabled; +}; + #endif diff --git a/crawl-ref/source/view.cc b/crawl-ref/source/view.cc index 7979a98060..dfd02e3f5a 100644 --- a/crawl-ref/source/view.cc +++ b/crawl-ref/source/view.cc @@ -724,7 +724,7 @@ void view_update_at(const coord_def &pos) show_update_at(pos); -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL if (!env.map_knowledge(pos).visible()) return; glyph g = get_cell_glyph(pos); @@ -750,7 +750,7 @@ void view_update_at(const coord_def &pos) #endif } -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL void flash_monster_colour(const monster* mon, uint8_t fmc_colour, int fmc_delay) { @@ -1000,8 +1000,11 @@ static bool _show_terrain = false; // // If show_updates is set, env.show and dependent structures // are updated. Should be set if anything in view has changed. +// +// If tiles_only is set, only the tile view will be updated. This +// is only relevant for Webtiles. //--------------------------------------------------------------- -void viewwindow(bool show_updates) +void viewwindow(bool show_updates, bool tiles_only) { if (you.duration[DUR_TIME_STEP]) return; @@ -1070,10 +1073,18 @@ void viewwindow(bool show_updates) // and this simply works without requiring a stack. you.flash_colour = BLACK; you.last_view_update = you.num_turns; -#ifndef USE_TILE - puttext(crawl_view.viewp.x, crawl_view.viewp.y, crawl_view.vbuf); - update_monster_pane(); -#else +#ifndef USE_TILE_LOCAL +#ifdef USE_TILE_WEB + tiles_crt_control crt(false); +#endif + + if (!tiles_only) + { + puttext(crawl_view.viewp.x, crawl_view.viewp.y, crawl_view.vbuf); + update_monster_pane(); + } +#endif +#ifdef USE_TILE tiles.set_need_redraw(you.running ? Options.tile_runrest_rate : 0); tiles.load_dungeon(crawl_view.vbuf, crawl_view.vgrdc); tiles.update_tabs(); @@ -1116,7 +1127,7 @@ void draw_cell(screen_cell_t *cell, const coord_def &gc, { if (you.see_cell(gc)) { -#ifdef USE_TILE +#ifdef USE_TILE_LOCAL cell->colour = real_colour(flash_colour); #else monster_type mons = env.map_knowledge(gc).monster(); diff --git a/crawl-ref/source/view.h b/crawl-ref/source/view.h index b6713f7eac..35751f2f94 100644 --- a/crawl-ref/source/view.h +++ b/crawl-ref/source/view.h @@ -35,12 +35,12 @@ bool view_update(); void view_update_at(const coord_def &pos); void flash_view(uint8_t colour = BLACK); // inside #ifndef USE_TILE_LOCAL? void flash_view_delay(uint8_t colour = BLACK, int delay = 150); -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL void flash_monster_colour(const monster* mon, uint8_t fmc_colour, int fmc_delay); #endif -void viewwindow(bool show_updates = true); +void viewwindow(bool show_updates = true, bool tiles_only = false); void draw_cell(screen_cell_t *cell, const coord_def &gc, bool anim_updates, int flash_colour); diff --git a/crawl-ref/source/viewgeom.cc b/crawl-ref/source/viewgeom.cc index d816dbbf3b..759578c023 100644 --- a/crawl-ref/source/viewgeom.cc +++ b/crawl-ref/source/viewgeom.cc @@ -51,7 +51,7 @@ class _layout #pragma GCC diagnostic ignored "-Wstrict-overflow" void _assert_validity() const { -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL // Check that all the panes fit in the view. ASSERT((viewp+viewsz - termp).x <= termsz.x); ASSERT((viewp+viewsz - termp).y <= termsz.y); @@ -378,7 +378,7 @@ void crawl_view_geometry::init_geometry() const _inline_layout lay_inline(termsz, hudsz); const _mlist_col_layout lay_mlist(termsz, hudsz); -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL if ((termsz.x < MIN_COLS || termsz.y < MIN_LINES || !lay_inline.valid) && !crawl_state.need_save) { @@ -405,7 +405,7 @@ void crawl_view_geometry::init_geometry() mlistp = winner->mlistp; mlistsz = winner->mlistsz; -#ifdef USE_TILE +#ifdef USE_TILE_LOCAL // libgui may redefine these based on its own settings. gui_init_view_params(*this); #endif diff --git a/crawl-ref/source/viewmap.cc b/crawl-ref/source/viewmap.cc index dd600ce8a0..d818673abe 100644 --- a/crawl-ref/source/viewmap.cc +++ b/crawl-ref/source/viewmap.cc @@ -41,7 +41,7 @@ #include "tileview.h" #endif -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL static unsigned _get_travel_colour(const coord_def& p) { #ifdef WIZARD @@ -62,7 +62,7 @@ static unsigned _get_travel_colour(const coord_def& p) } #endif -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL static bool _travel_colour_override(const coord_def& p) { if (is_waypoint(p) || is_stair_exclusion(p) @@ -431,7 +431,7 @@ static int _get_number_of_lines_levelmap() return get_number_of_lines() - (Options.level_map_title ? 1 : 0); } -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL static void _draw_level_map(int start_x, int start_y, bool travel_mode, bool on_level) { @@ -503,7 +503,7 @@ static void _draw_level_map(int start_x, int start_y, bool travel_mode, puttext(1, top, vbuf); } -#endif // USE_TILE +#endif // USE_TILE_LOCAL static void _reset_travel_colours(std::vector<coord_def> &features, bool on_level) @@ -530,7 +530,7 @@ static bool _comp_glyphs(const glyph& g1, const glyph& g2) return (g1.ch < g2.ch || g1.ch == g2.ch && g1.col < g2.col); } -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL static glyph _get_feat_glyph(const coord_def& gc); #endif @@ -573,7 +573,7 @@ class feature_list void maybe_add(const coord_def& gc) { -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL if (!env.map_knowledge(gc).known()) return; @@ -604,7 +604,7 @@ public: } }; -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL static void _draw_title(const coord_def& cpos, const feature_list& feats) { if (!Options.level_map_title) @@ -718,6 +718,10 @@ bool show_map(level_pos &lpos, cursor_control ccon(!Options.use_fake_cursor); int i, j; +#ifdef USE_TILE_WEB + tiles_crt_control crt(false); +#endif + int move_x = 0, move_y = 0, scroll_y = 0; bool new_level = true; @@ -750,7 +754,7 @@ bool show_map(level_pos &lpos, bool map_alive = true; bool redraw_map = true; -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL clrscr(); #endif textcolor(DARKGREY); @@ -871,12 +875,13 @@ bool show_map(level_pos &lpos, // location. It silently ignores everything else going // on in this function. --Enne tiles.load_dungeon(lpos.pos); -#else +#endif +#ifndef USE_TILE_LOCAL _draw_title(lpos.pos, feats); _draw_level_map(start_x, start_y, travel_mode, on_level); -#endif // USE_TILE +#endif } -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL cursorxy(curs_x, curs_y + top - 1); #endif redraw_map = true; @@ -1248,7 +1253,7 @@ bool show_map(level_pos &lpos, lpos.pos.y = std::min(std::max(lpos.pos.y, min_y), max_y); move_x = lpos.pos.x - oldp.x; move_y = lpos.pos.y - oldp.y; -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL if (num_lines < map_lines) { // Scrolling only happens when we don't have a large enough @@ -1293,7 +1298,7 @@ bool emphasise(const coord_def& where) && you.where_are_you != BRANCH_VESTIBULE_OF_HELL); } -#ifndef USE_TILE +#ifndef USE_TILE_LOCAL // Get glyph for feature list; here because it's so similar // to get_map_col. static glyph _get_feat_glyph(const coord_def& gc) diff --git a/crawl-ref/source/webserver/config.py b/crawl-ref/source/webserver/config.py index 62af85001a..23ad8cfd18 100644 --- a/crawl-ref/source/webserver/config.py +++ b/crawl-ref/source/webserver/config.py @@ -18,6 +18,8 @@ password_db = "./webserver/passwd.db3" static_path = "./webserver/static" template_path = "./webserver/templates/" +server_socket_path = None # Uses global temp dir + games = OrderedDict([ ("dcss-web-trunk", dict( name = "DCSS trunk", @@ -26,6 +28,7 @@ games = OrderedDict([ macro_path = "./rcs/", morgue_path = "./rcs", running_game_path = "./rcs/running", + socket_path = "./rcs", client_prefix = "game")), ("sprint-web-trunk", dict( name = "Sprint trunk", @@ -34,6 +37,7 @@ games = OrderedDict([ macro_path = "./rcs/", morgue_path = "./rcs", running_game_path = "./rcs/running", + socket_path = "./rcs", options = ["-sprint"], client_prefix = "game")), ("zd-web-trunk", dict( @@ -43,6 +47,7 @@ games = OrderedDict([ macro_path = "./rcs/", morgue_path = "./rcs", running_game_path = "./rcs/running", + socket_path = "./rcs", options = ["-zotdef"], client_prefix = "game")), ("tut-web-trunk", dict( @@ -52,6 +57,7 @@ games = OrderedDict([ macro_path = "./rcs/", morgue_path = "./rcs", running_game_path = "./rcs/running", + socket_path = "./rcs", options = ["-tutorial"], client_prefix = "game")), ]) diff --git a/crawl-ref/source/webserver/connection.py b/crawl-ref/source/webserver/connection.py new file mode 100644 index 0000000000..e5675aa561 --- /dev/null +++ b/crawl-ref/source/webserver/connection.py @@ -0,0 +1,81 @@ +import socket +import fcntl +import os, os.path +import time +import warnings + +from config import server_socket_path + +class WebtilesSocketConnection(object): + def __init__(self, io_loop, socketpath): + self.io_loop = io_loop + self.crawl_socketpath = socketpath + self.message_callback = None + self.socket = None + self.socketpath = None + + self.msg_buffer = None + + def connect(self): + if not os.path.exists(self.crawl_socketpath): + # Wait until the socket exists + self.io_loop.add_timeout(time.time() + 1, self.connect) + return + + self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + + # Set close-on-exec + flags = fcntl.fcntl(self.socket.fileno(), fcntl.F_GETFD) + fcntl.fcntl(self.socket.fileno(), flags | fcntl.FD_CLOEXEC) + + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + # Bind to a temp path + # Ignore the security warning about tempnam; in this case, + # there is no security risk (the most that can happen is that + # the bind call fails) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.socketpath = os.tempnam(server_socket_path, "crawl") + self.socket.bind(self.socketpath) + + # Install handler + self.io_loop.add_handler(self.socket.fileno(), + self._handle_read, + self.io_loop.ERROR | self.io_loop.READ) + + self.socket.sendto('{"msg":"attach"}', self.crawl_socketpath) + + def _handle_read(self, fd, events): + if events & self.io_loop.READ: + data = self.socket.recv(128 * 1024, socket.MSG_DONTWAIT) + + self._handle_data(data) + + if events & self.io_loop.ERROR: + pass + + def _handle_data(self, data): + if self.msg_buffer is not None: + data = self.msg_buffer + data + + if data[-1] != "\n": + # All messages from crawl end with \n. + # If this one doesn't, it's fragmented. + self.msg_buffer = data + + else: + self.msg_buffer = None + + if self.message_callback: + self.message_callback(data) + + def send_message(self, data): + self.socket.sendto(data, self.crawl_socketpath) + + def close(self): + if self.socket: + self.io_loop.remove_handler(self.socket.fileno()) + self.socket.close() + os.remove(self.socketpath) + self.socket = None diff --git a/crawl-ref/source/webserver/process_handler.py b/crawl-ref/source/webserver/process_handler.py new file mode 100644 index 0000000000..08c2521e2b --- /dev/null +++ b/crawl-ref/source/webserver/process_handler.py @@ -0,0 +1,219 @@ +import os, os.path, errno +import subprocess +import datetime, time + +import config + +from tornado.escape import json_decode, json_encode + +from terminal import TerminalRecorder +from connection import WebtilesSocketConnection + +class CrawlProcessHandlerBase(object): + def __init__(self, game_params, username, logger, io_loop): + self.game_params = game_params + self.username = username + self.logger = logger + self.io_loop = io_loop + + self.process = None + self.client_version = None + self.where = None + self.wheretime = time.time() + self.kill_timeout = None + + self.end_callback = None + self._watchers = set() + self.last_activity_time = time.time() + + def idle_time(self): + return time.time() - self.last_activity_time + + def send_to_all(self, msg): + for watcher in self._watchers: + watcher.handle_message(msg) + + def update_watcher_description(self): + watcher_names = [watcher.username for watcher in self._watchers + if watcher.username] + anon_count = len(self._watchers) - len(watcher_names) + s = ", ".join(watcher_names) + if len(watcher_names) > 0 and anon_count > 0: + s = s + ", and %i Anon" % anon_count + elif anon_count > 0: + s = "%i Anon" % anon_count + self.send_to_all("watchers(%i,'%s')" % + (len(self._watchers), s)) + + def add_watcher(self, watcher): + self._watchers.add(watcher) + if self.client_version: + watcher.set_client(self.game_params, self.client_version) + + def remove_watcher(self, watcher): + self._watchers.remove(watcher) + self.update_watcher_description() + + def watcher_count(self): + return len(self._watchers) + + def stop(self): + self.process.send_signal(subprocess.signal.SIGHUP) + t = time.time() + config.kill_timeout + self.kill_timeout = self.io_loop.add_timeout(t, self.kill) + + def kill(self): + if self.process: + self.logger.info("Killing crawl process after SIGHUP did nothing.") + self.process.send_signal(subprocess.signal.SIGTERM) + self.kill_timeout = None + + def check_where(self): + morgue_path = self.game_params["morgue_path"] + wherefile = os.path.join(morgue_path, self.username, + self.username + ".dglwhere") + try: + if os.path.getmtime(wherefile) > self.wheretime: + self.wheretime = time.time() + f = open(wherefile, "r") + _, _, newwhere = f.readline().partition("|") + f.close() + + newwhere = newwhere.strip() + + if self.where != newwhere: + self.where = newwhere + except (OSError, IOError): + pass + + def _base_call(self): + game = self.game_params + + call = [game["crawl_binary"], + "-name", self.username, + "-rc", os.path.join(game["rcfile_path"], + self.username + ".rc"), + "-macro", os.path.join(game["macro_path"], + self.username + ".macro"), + "-morgue", os.path.join(game["morgue_path"], + self.username)] + + if "options" in game: + call += game["options"] + + return call + + def handle_input(self, msg): + raise NotImplementedError() + +class CrawlProcessHandler(CrawlProcessHandlerBase): + def __init__(self, game_params, username, logger, io_loop): + super(CrawlProcessHandler, self).__init__(game_params, username, + logger, io_loop) + self.socketpath = None + self.conn = None + self.ttyrec_filename = None + + if "client_prefix" in game_params: + self.client_version = game_params["client_prefix"] + + + def start(self): + self.socketpath = os.path.join(self.game_params["socket_path"], + self.username + ".sock") + + try: # Unlink if necessary + os.unlink(self.socketpath) + except OSError, e: + if e.errno != errno.ENOENT: + raise + + game = self.game_params + + call = self._base_call() + ["-webtiles-socket", self.socketpath, + "-await-connection"] + + now = datetime.datetime.utcnow() + running_game_path = game["running_game_path"] + self.ttyrec_filename = os.path.join(running_game_path, + self.username + ":" + + now.strftime("%Y-%m-%d.%H:%M:%S") + + ".ttyrec") + + self.logger.info("Starting crawl.") + + self.process = TerminalRecorder(call, self.ttyrec_filename, + self.io_loop) + self.process.end_callback = self._on_process_end + self.process.output_callback = self._on_process_output + + self.conn = WebtilesSocketConnection(self.io_loop, self.socketpath) + self.conn.message_callback = self._on_socket_message + self.conn.connect() + + self.last_activity_time = time.time() + + def _on_process_end(self): + self.logger.info("Crawl terminated.") + + self.process = None + + if self.conn: + self.conn.close() + self.conn = None + + if self.kill_timeout: + self.io_loop.remove_timeout(self.kill_timeout) + self.kill_timeout = None + + for watcher in list(self._watchers): + watcher.stop_watching() + + if self.end_callback: + self.end_callback() + + def add_watcher(self, watcher): + super(CrawlProcessHandler, self).add_watcher(watcher) + + if self.conn: + self.conn.send_message('{"msg":"spectator_joined"}') + + def handle_input(self, msg): + if msg.startswith("{"): + obj = json_decode(msg) + + if obj["msg"] == "input" and self.process: + self.last_action_time = time.time() + + data = "" + for x in obj["data"]: + data += chr(x) + + self.process.poll() + if self.process: + self.process.write_input(data) + + elif self.conn: + self.conn.send_message(msg.encode("utf8")) + + else: + self.process.poll() + if self.process: + self.process.write_input(msg.encode("utf8")) + + def _on_process_output(self, line): + self.check_where() + self.last_activity_time = time.time() + + self.send_to_all(line) + + def _on_socket_message(self, msg): + # stdout data is only used for compatibility to wrapper + # scripts -- so as soon as we receive something on the socket, + # we stop using stdout + self.process.output_callback = None + + self.check_where() + self.last_activity_time = time.time() + + self.send_to_all(msg) diff --git a/crawl-ref/source/webserver/server.py b/crawl-ref/source/webserver/server.py index 5737b70083..51233a7bf5 100755 --- a/crawl-ref/source/webserver/server.py +++ b/crawl-ref/source/webserver/server.py @@ -13,21 +13,22 @@ import crypt import sqlite3 import logging -import sys import signal import time, datetime -import collections import re import random import codecs from config import * +from process_handler import CrawlProcessHandler + logging.basicConfig(**logging_config) class TornadoFilter(logging.Filter): def filter(self, record): - if record.module == "web" and record.levelno <= logging.INFO: return False + if record.module == "web" and record.levelno <= logging.INFO: + return False return True logging.getLogger().addFilter(TornadoFilter()) logging.addLevelName(logging.DEBUG, "DEBG") @@ -121,15 +122,10 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler): def __init__(self, app, req, **kwargs): tornado.websocket.WebSocketHandler.__init__(self, app, req, **kwargs) self.username = None - self.p = None self.timeout = None - self.kill_timeout = None - self.last_action_time = time.time() self.watched_game = None - self.watchers = set() - self.ttyrec_filename = None - self.where = None - self.wheretime = time.time() + self.process = None + self.game_id = None self.ioloop = tornado.ioloop.IOLoop.instance() @@ -137,6 +133,12 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler): self.id = current_id current_id += 1 + self.logger = logging.LoggerAdapter(logging.getLogger(), {}) + self.logger.process = self._process_log_msg + + def _process_log_msg(self, msg, kwargs): + return "#%-5s %s" % (self.id, msg), kwargs + def __hash__(self): return self.id @@ -144,16 +146,16 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler): return self.id == other.id def open(self): - logging.info("#%s: Socket opened from ip %s (fd%s).", self.id, - self.request.remote_ip, self.request.connection.stream.socket.fileno()) - global sockets + self.logger.info("Socket opened from ip %s (fd%s).", + self.request.remote_ip, + self.request.connection.stream.socket.fileno()) sockets.add(self) self.reset_timeout() if max_connections < len(sockets): self.write_message("connection_closed('The maximum number of connections " - + "has been reached, sorry :(');"); + + "has been reached, sorry :(');") self.close() elif shutting_down: self.close() @@ -164,22 +166,20 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler): self.start_crawl(None) def idle_time(self): - return time.time() - self.last_action_time + return self.process.idle_time() def is_running(self): - return self.p is not None + return self.process is not None def is_in_lobby(self): return not self.is_running() and self.watched_game is None def update_lobby(self): if self.client_terminated: - logging.warn("#%s: update_lobby called for closed " + - "socket! (Crawl is %srunning)" % - (self.id, - ("not " if self.is_running() else ""))) + self.logger.warning("update_lobby called for closed " + + "socket! (Crawl is %srunning)", + "not " if self.is_running() else "") if not self.is_running(): - global sockets sockets.remove(self) return @@ -192,7 +192,7 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler): # Rerender Banner banner_html = self.render_string("banner.html", username = self.username) self.write_message("$('#banner').html(" + - tornado.escape.json_encode(banner_html) + ");"); + tornado.escape.json_encode(banner_html) + ");") play_html = self.render_string("game_links.html", games = games) self.write_message("$('#play_now').html(" + tornado.escape.json_encode(play_html) + ");") @@ -210,14 +210,12 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler): self.timeout = None if not self.received_pong: - logging.info("#%s: Connection to remote ip %s timed out.", - self.id, self.request.remote_ip) + self.logger.info("Connection timed out.") self.close() else: - if self.is_running() and self.idle_time() > max_idle_time: - logging.info("#%s: Stopping crawl after idle time limit for %s.", - self.id, self.username) - self.stop_crawl() + if self.is_running() and self.process.idle_time() > max_idle_time: + self.logger.info("Stopping crawl after idle time limit.") + self.process.stop() if not self.client_terminated: self.reset_timeout() @@ -228,63 +226,42 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler): self.write_message("go_lobby();") return - if game_id not in games: return - - self.last_action_time = time.time() + if game_id not in games: return self.game_id = game_id - if dgl_mode: - game = games[game_id] - - call = [game["crawl_binary"], - "-name", self.username, - "-rc", os.path.join(game["rcfile_path"], self.username + ".rc"), - "-macro", os.path.join(game["macro_path"], self.username + ".macro"), - "-morgue", os.path.join(game["morgue_path"], self.username)] - - if "options" in game: - call += game["options"] - - if "client_prefix" in game: - self.client_prefix = game["client_prefix"] - self.send_client(self.client_prefix) - else: - call = ["./crawl"] - self.send_client("game") - - self.p = subprocess.Popen(call, - stdin = subprocess.PIPE, - stdout = subprocess.PIPE, - stderr = subprocess.PIPE) - - self.ioloop.add_handler(self.p.stdout.fileno(), self.on_stdout, - self.ioloop.READ | self.ioloop.ERROR) - self.ioloop.add_handler(self.p.stderr.fileno(), self.on_stderr, - self.ioloop.READ | self.ioloop.ERROR) - - logging.info("#%s: Starting crawl for user %s (ip %s, fd%s, fd%s, fd%s).", - self.id, self.username, self.request.remote_ip, - self.p.stdin.fileno(), self.p.stdout.fileno(), - self.p.stderr.fileno()) + self.process = CrawlProcessHandler(games[game_id], self.username, + self.logger, self.ioloop) + self.process.end_callback = self._on_crawl_end + self.process.add_watcher(self) + self.process.start() self.write_message("crawl_started();") if dgl_mode: - self.create_mock_ttyrec() - update_global_status() - def stop_crawl(self): - self.p.send_signal(subprocess.signal.SIGHUP) - self.kill_timeout = self.ioloop.add_timeout(time.time() + kill_timeout, self.kill_crawl) + def _on_crawl_end(self): + self.process = None - def kill_crawl(self): - if self.p: - logging.info("#%s: Killing crawl process after SIGHUP did nothing (user %s, ip %s).", - self.id, self.username, self.request.remote_ip) - self.p.send_signal(subprocess.signal.SIGTERM) - self.kill_timeout = None + if self.client_terminated: + sockets.remove(self) + else: + if shutting_down: + self.close() + else: + # Go back to lobby + self.write_message("crawl_ended();") + if dgl_mode: + self.update_lobby() + else: + self.start_crawl(None) + + if dgl_mode: update_global_status() + + if shutting_down and len(sockets) == 0: + # The last crawl process has ended, now we can go + self.ioloop.stop() def init_user(self): with open("/dev/null", "w") as f: @@ -292,72 +269,15 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler): stdout = f, stderr = subprocess.STDOUT) return result == 0 - def create_mock_ttyrec(self): - now = datetime.datetime.utcnow() - running_game_path = games[self.game_id]["running_game_path"] - self.ttyrec_filename = os.path.join(running_game_path, - self.username + ":" + now.strftime("%Y-%m-%d.%H:%M:%S") - + ".ttyrec") - f = open(self.ttyrec_filename, "w") - f.close() - - def delete_mock_ttyrec(self): - if self.ttyrec_filename: - os.remove(self.ttyrec_filename) - self.ttyrec_filename = None - - def check_where(self): - morgue_path = games[self.game_id]["morgue_path"] - wherefile = os.path.join(morgue_path, self.username, self.username + ".dglwhere") - try: - stat = os.stat(wherefile) - if stat.st_mtime > self.wheretime: - self.wheretime = time.time() - f = open(wherefile, "r") - _, _, newwhere = f.readline().partition("|") - f.close() - - newwhere = newwhere.strip() - - if self.where != newwhere: - self.where = newwhere - update_global_status() - except (OSError, IOError): - pass - - def add_watcher(self, watcher): - self.watchers.add(watcher) - watcher.send_client(self.client_prefix) - self.p.stdin.write("^r") # Redraw - self.update_watcher_description() - - def remove_watcher(self, watcher): - self.watchers.remove(watcher) - self.update_watcher_description() - - def update_watcher_description(self): - watcher_names = [watcher.username for watcher in self.watchers - if watcher.username] - anon_count = len(self.watchers) - len(watcher_names) - s = ", ".join(watcher_names) - if len(watcher_names) > 0 and anon_count > 0: - s = s + ", and %i Anon" % anon_count - elif anon_count > 0: - s = "%i Anon" % anon_count - self.write_message_all("watchers(%i,'%s')" % - (len(self.watchers), s)) - def stop_watching(self): if self.watched_game: - logging.info("#%s: User %s stopped watching %s #%s (ip: %s)", - self.id, self.username, self.watched_game.username, - self.watched_game.id, self.request.remote_ip) + self.logger.info("Stopped watching %s.", self.watched_game.username) self.watched_game.remove_watcher(self) self.watched_game = None self.write_message("set_watching(false);") self.write_message("go_lobby();") - def send_client(self, client_prefix): + def set_client(self, game_params, client_prefix): game_html = self.render_string(client_prefix + "/game.html", prefix = client_prefix) self.write_message("delay_timeout = 1;$('#game').html(" + tornado.escape.json_encode(game_html) + ");delay_ended();") @@ -369,56 +289,17 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler): tornado.escape.json_encode(msg) + ");") self.close() if self.is_running(): - self.stop_crawl() - - def poll_crawl(self): - if self.p is not None and self.p.poll() is not None: - self.ioloop.remove_handler(self.p.stdout.fileno()) - self.ioloop.remove_handler(self.p.stderr.fileno()) - self.p.stdout.close() - self.p.stderr.close() - self.p = None - - logging.info("#%s: Crawl terminated.", self.id) - - if self.kill_timeout: - self.ioloop.remove_timeout(self.kill_timeout) - self.kill_timeout = None - - if dgl_mode: self.delete_mock_ttyrec() - self.where = None - - if self.client_terminated: - global sockets - sockets.remove(self) - else: - if shutting_down: - self.close() - else: - # Go back to lobby - self.write_message("crawl_ended();") - if dgl_mode: - self.update_lobby() - else: - self.start_crawl(None) - - if dgl_mode: update_global_status() - - for watcher in list(self.watchers): - watcher.stop_watching() - - if shutting_down and len(sockets) == 0: - # The last crawl process has ended, now we can go - self.ioloop.stop() + self.process.stop() def login(self, username): self.username = username + self.logger.extra["username"] = username if not self.init_user(): self.write_message("connection_closed('Could not initialize your rc and morgue!<br>" + "This probably means there is something wrong with the server " + "configuration.');") - logging.warn("#%s: User initialization returned an error for user %s!", - self.id, self.username) + self.logger.warning("User initialization returned an error for user %s!", + self.username) self.username = None self.close() return @@ -433,12 +314,10 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler): username, _, password = message.partition(' ') real_username = user_passwd_match(username, password) if real_username: - logging.info("#%s: User %s logged in from ip %s.", - self.id, real_username, self.request.remote_ip) + self.logger.info("User %s logged in.", real_username) self.login(real_username) else: - logging.warn("#%s: Failed login for user %s from ip %s.", - self.id, username, self.request.remote_ip) + self.logger.warning("Failed login for user %s.", username) self.write_message("login_failed();") elif message.startswith("LoginToken: "): @@ -447,12 +326,10 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler): token = long(token) if (token, username) in login_tokens: del login_tokens[(token, username)] - logging.info("#%s: User %s logged in from ip %s (via token).", - self.id, username, self.request.remote_ip) + self.logger.info("User %s logged in (via token).", username) self.login(username) else: - logging.warn("#%s: Wrong login token for user %s from ip %s.", - self.id, username, self.request.remote_ip) + self.logger.warning("Wrong login token for user %s.", username) self.write_message("login_failed();") elif message == "Remember": @@ -476,7 +353,7 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler): return elif message.startswith("Play: "): - if self.p or self.watched_game: return + if self.process or self.watched_game: return game_id = message[len("Play: "):] self.start_crawl(game_id) @@ -487,17 +364,15 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler): self.update_lobby() elif message.startswith("Watch: "): - if self.p or self.watched_game: return + if self.process or self.watched_game: return watch_username = message[len("Watch: "):] - sockets = [socket for socket in find_user_sockets(watch_username) - if socket.is_running()] - if len(sockets) >= 1: - socket = sockets[0] - logging.info("#%s: User %s (ip: %s) started watching #%s %s.", - self.id, self.username, self.request.remote_ip, - socket.id, socket.username) - self.watched_game = socket - socket.add_watcher(self) + procs = [socket.process for socket in find_user_sockets(watch_username) + if socket.is_running()] + if len(procs) >= 1: + process = procs[0] + self.logger.info("Started watching %s.", process.username) + self.watched_game = process + process.add_watcher(self) self.write_message("set_watching(true);") else: self.write_message("go_lobby();") @@ -510,12 +385,12 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler): chat_msg = ("<span class='chat_sender'>%s</span>: <span class='chat_msg'>%s</span>" % (self.username, tornado.escape.xhtml_escape(message[len("Chat: "):]))) receiver = None - if self.p: - receiver = self + if self.process: + receiver = self.process elif self.watched_game: receiver = self.watched_game if receiver: - receiver.write_message_all("chat(%s);" % tornado.escape.json_encode(chat_msg)) + receiver.send_to_all("chat(%s);" % tornado.escape.json_encode(chat_msg)) elif message.startswith("Register: "): message = message[len("Register: "):] @@ -523,18 +398,17 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler): email, _, password = message.partition(" ") error = register_user(username, password, email) if error is None: - logging.info("#%s: Registered user: %s (ip: %s)", self.id, username, - self.request.remote_ip) + self.logger.info("Registered user %s.", username) self.login(username) else: - logging.info("#%s: Registration attempt failed for username %s: %s (ip: %s)", - self.id, username, error, self.request.remote_ip) + self.logger.info("Registration attempt failed for username %s: %s", + username, error) self.write_message("register_failed(" + tornado.escape.json_encode(error) + ");") elif message == "GoLobby": if self.is_running(): - self.stop_crawl() + self.process.stop() elif self.watched_game: self.stop_watching() @@ -555,40 +429,32 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler): f.write(codecs.BOM_UTF8) f.write(contents.encode("utf8")) - elif self.p is not None: - if not message.startswith("^"): - self.last_action_time = time.time() + elif message.startswith("{"): + self.process.handle_input(message) - logging.debug("#%s: Message: %s (user: %s)", self.id, message, self.username) - self.poll_crawl() - if self.p is not None: - self.p.stdin.write(message.encode("utf8")) + elif self.process: + self.logger.debug("Message: %s", message) + self.process.handle_input(message) def write_message(self, msg): try: super(CrawlWebSocket, self).write_message(msg) except: - logging.warn("#%s: Exception trying to send message to %s.", - self.id, self.request.remote_ip, exc_info = True) + self.logger.warning("Exception trying to send message.", exc_info = True) self.ws_connection._abort() - def write_message_all(self, msg): + def handle_message(self, msg): if not self.client_terminated: self.write_message(msg) - for watcher in self.watchers: - if not watcher.client_terminated: - watcher.write_message(msg) - def on_close(self): - global sockets - if self.p is None and self in sockets: + if self.process is None and self in sockets: sockets.remove(self) if shutting_down and len(sockets) == 0: # The last socket has been closed, now we can go self.ioloop.stop() elif self.is_running(): - self.stop_crawl() + self.process.stop() if self.watched_game: self.watched_game.remove_watcher(self) @@ -596,49 +462,8 @@ class CrawlWebSocket(tornado.websocket.WebSocketHandler): if self.timeout: self.ioloop.remove_timeout(self.timeout) - logging.info("#%s: Socket for ip %s closed.", self.id, self.request.remote_ip) - - def on_stderr(self, fd, events): - if events & self.ioloop.ERROR: - self.poll_crawl() - elif events & self.ioloop.READ: - s = self.p.stderr.readline() - - if self.client_terminated: - return - - if not (s.isspace() or s == ""): - logging.info("#%s: ERR: %s: %s", - self.id, self.username, s.strip()) - - self.poll_crawl() - - def handle_crawl_message(self, msg): - if msg.startswith("^"): - msg = msg[1:] - if msg.startswith("ClientPrefix:"): - msg = msg[len("ClientPrefix:"):] - msg = msg.strip() - self.client_prefix = msg - logging.info("#%s: User %s is using client version %s.", - self.id, self.username, self.client_prefix) - self.send_client(self.client_prefix) - else: - self.write_message_all(msg) - - def on_stdout(self, fd, events): - if events & self.ioloop.ERROR: - self.poll_crawl() - elif events & self.ioloop.READ: - msg = self.p.stdout.readline() - - if self.client_terminated: - return - - self.handle_crawl_message(msg) + self.logger.info("Socket closed.") - self.poll_crawl() - if dgl_mode: self.check_where() def update_global_status(): update_all_lobbys() @@ -654,14 +479,13 @@ def write_dgl_status_file(): f = open(dgl_status_file, "w") for socket in list(sockets): if socket.username and socket.is_running(): - f.write(socket.username + "#" + - socket.game_id + "#" + - (socket.where or "") + "#" + - "0x0" + "#" + - str(int(socket.idle_time())) + "#" + - str(len(socket.watchers)) + "#\n") + f.write("%s#%s#%s#0x0#%s#%s#\n" % + (socket.username, socket.game_id, + (socket.process.where or ""), + str(int(socket.process.idle_time())), + str(socket.process.watcher_count()))) except (OSError, IOError) as e: - logging.warn("Could not write dgl status file: %s", e) + logging.warning("Could not write dgl status file: %s", e) finally: if f: f.close() diff --git a/crawl-ref/source/webserver/static/client.js b/crawl-ref/source/webserver/static/client.js index f5c72ba515..86e55ab6c0 100644 --- a/crawl-ref/source/webserver/static/client.js +++ b/crawl-ref/source/webserver/static/client.js @@ -82,18 +82,32 @@ function handle_keypress(e) { focus_chat(); } - else if (s == "\\") + else if (s == "{") { - socket.send("\\92\n"); - } - else if (s == "^") - { - socket.send("\\94\n"); + send_bytes(["{".charCodeAt(0)]); } else socket.send(s); } +function send_keycode(code) +{ + socket.send('{"msg":"key","keycode":' + code + '}'); +} + +function send_bytes(bytes) +{ + s = '{"msg":"input","data":['; + $.each(bytes, function (i, code) { + if (i == 0) + s += code; + else + s += "," + code; + }); + s += "]}"; + socket.send(s); +} + function handle_keydown(e) { if (current_layer == "lobby") @@ -113,13 +127,13 @@ function handle_keydown(e) if (e.which in ctrl_key_conversion) { e.preventDefault(); - socket.send("\\" + ctrl_key_conversion[e.which] + "\n"); + send_keycode(ctrl_key_conversion[e.which]); } else if ($.inArray(String.fromCharCode(e.which), captured_control_keys) != -1) { e.preventDefault(); var code = e.which - "A".charCodeAt(0) + 1; // Compare the CONTROL macro in defines.h - socket.send("\\" + code + "\n"); + send_keycode(code); } } else if (!e.ctrlKey && e.shiftKey && !e.altKey) @@ -127,14 +141,15 @@ function handle_keydown(e) if (e.which in shift_key_conversion) { e.preventDefault(); - socket.send("\\" + shift_key_conversion[e.which] + "\n"); + send_keycode(shift_key_conversion[e.which]); } } else if (!e.ctrlKey && !e.shiftKey && e.altKey) { - //e.preventDefault(); - //var s = String.fromCharCode(e.which); - //socket.send("\\27\n" + s); + if (e.which == 18) return; + + e.preventDefault(); + send_bytes([27, e.which]); } else if (!e.ctrlKey && !e.shiftKey && !e.altKey) { @@ -146,7 +161,7 @@ function handle_keydown(e) else if (e.which in key_conversion) { e.preventDefault(); - socket.send("\\" + key_conversion[e.which] + "\n"); + send_keycode(key_conversion[e.which]); } else log("Key: " + e.which); diff --git a/crawl-ref/source/webserver/static/game/game.js b/crawl-ref/source/webserver/static/game/game.js index e59364a082..eba2799bf7 100644 --- a/crawl-ref/source/webserver/static/game/game.js +++ b/crawl-ref/source/webserver/static/game/game.js @@ -168,20 +168,9 @@ function do_layout() current_layout = layout; - send_layout(layout); return true; } -function send_layout(layout) -{ - var msg = ""; - msg += "^s" + layout.stats_height + "\n"; - msg += "^W" + layout.crt_width + "\n"; - msg += "^H" + layout.crt_height + "\n"; - msg += "^m" + layout.msg_width + "\n"; - socket.send(msg); -} - // View area ------------------------------------------------------------------- function vgrdc(x, y) { diff --git a/crawl-ref/source/webserver/templates/lobby.html b/crawl-ref/source/webserver/templates/lobby.html index 3a268af3d2..29f33c72fd 100644 --- a/crawl-ref/source/webserver/templates/lobby.html +++ b/crawl-ref/source/webserver/templates/lobby.html @@ -6,8 +6,8 @@ </a> </td> <td>{{ running_game.game_id }}</td> - <td>{{ (running_game.where or "") }}</td> - <td>{{ int(running_game.idle_time()) }}s</td> - <td>{{ len(running_game.watchers) }}</td> + <td>{{ (running_game.process.where or "") }}</td> + <td>{{ int(running_game.process.idle_time()) }}s</td> + <td>{{ running_game.process.watcher_count() }}</td> </tr> {% end %} diff --git a/crawl-ref/source/webserver/terminal.py b/crawl-ref/source/webserver/terminal.py new file mode 100644 index 0000000000..d26cf06a98 --- /dev/null +++ b/crawl-ref/source/webserver/terminal.py @@ -0,0 +1,127 @@ +import pty +import termios +import os +import fcntl +import struct +import resource +import sys +import time + +BUFSIZ = 2048 + +class TerminalRecorder(object): + def __init__(self, command, filename, io_loop): + self.io_loop = io_loop + self.command = command + self.ttyrec = open(filename, "w", 0) + self.id = id + self.returncode = None + self.output_buffer = "" + + self.pid = None + self.child_fd = None + + self.end_callback = None + self.output_callback = None + + self._spawn() + + def _spawn(self): + self.pid, self.child_fd = pty.fork() + + if self.pid == 0: + # We're the child + # Set window size + cols = 80 + lines = 24 + s = struct.pack("HHHH", lines, cols, 0, 0) + fcntl.ioctl(sys.stdout.fileno(), termios.TIOCSWINSZ, s) + + # Make sure not to retain any files from the parent + max_fd = resource.getrlimit(resource.RLIMIT_NOFILE)[0] + for i in range (3, max_fd): + try: + os.close (i) + except OSError: + pass + + # And exec + env = dict(os.environ) + env["COLUMNS"] = str(cols) + env["LINES"] = str(lines) + env["TERM"] = "linux" + os.execvpe(self.command[0], self.command, env) + + # We're the parent + self.io_loop.add_handler(self.child_fd, + self._handle_read, + self.io_loop.ERROR | self.io_loop.READ) + + def _handle_read(self, fd, events): + if events & self.io_loop.READ: + buf = os.read(fd, BUFSIZ) + + if len(buf) > 0: + self.write_ttyrec_chunk(buf) + + self.output_buffer += buf + self._do_output_callback() + + self.poll() + + if events & self.io_loop.ERROR: + self.poll() + + def write_ttyrec_header(self, sec, usec, l): + s = struct.pack("<iii", sec, usec, l) + self.ttyrec.write(s) + + def write_ttyrec_chunk(self, data): + t = time.time() + self.write_ttyrec_header(int(t), int((t % 1) * 1000000), len(data)) + self.ttyrec.write(data) + + def _do_output_callback(self): + pos = self.output_buffer.find("\n") + while pos >= 0: + line = self.output_buffer[:pos] + self.output_buffer = self.output_buffer[pos + 1:] + + if len(line) > 0: + if line[-1] == "\r": line = line[:-1] + + if self.output_callback: + self.output_callback(line) + + pos = self.output_buffer.find("\n") + + def send_signal(self, signal): + os.kill(self.pid, signal) + + def poll(self): + if self.returncode is None: + pid, status = os.waitpid(self.pid, os.WNOHANG) + if pid == self.pid: + if os.WIFSIGNALED(status): + self.returncode = -os.WTERMSIG(status) + elif os.WIFEXITED(status): + self.returncode = os.WEXITSTATUS(status) + else: + # Should never happen + raise RuntimeError("Unknown child exit status!") + + if self.returncode is not None: + self.io_loop.remove_handler(self.child_fd) + os.close(self.child_fd) + + self.ttyrec.close() + + if self.end_callback: + self.end_callback() + + return self.returncode + + def write_input(self, data): + while len(data) > 0: + written = os.write(self.child_fd, data) + data = data[written:] |