diff --git a/src/game.cpp b/src/game.cpp index e506164..3164685 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -2088,6 +2088,9 @@ auto game::advance_time_action_tick() -> int // Returns true if game is over (death, saved, quit, etc) bool game::do_turn() { +#if defined(__EMSCRIPTEN__) + EM_ASM( window.game_unsaved = true; window.bnSaveRevision = ( window.bnSaveRevision || 0 ) + 1; ); +#endif ZoneScopedN( "game::do_turn" ); const auto reset_time_action_tick = on_out_of_scope( [this]() { action_time_scale::set_calendar_turns_this_tick_to_next_tick( @@ -4488,6 +4491,9 @@ bool game::save( bool quitting ) auto duration = world->commit_save_tx(); add_msg( m_info, _( "World Saved (took %dms)." ), duration ); +#if defined(__EMSCRIPTEN__) + EM_ASM( window.bnPersist?.(); ); +#endif return true; } } catch( std::ios::failure &err ) { diff --git a/src/ncurses_def.cpp b/src/ncurses_def.cpp index 3bfb91b..a4d967b 100644 --- a/src/ncurses_def.cpp +++ b/src/ncurses_def.cpp @@ -29,6 +29,57 @@ #include "ncurses_def.h" +#if defined(__EMSCRIPTEN__) +#include + +// Suspend only the WebAssembly call stack while waiting for browser input. +// ncurses still handles all window composition and terminal output. +EM_ASYNC_JS( int, browser_getch, ( int delay ), { + window.bnFlushOutput?.(); + if( window.bnInput.length ) { return window.bnInput.shift(); } + if( delay === 0 ) { return -1; } + await new Promise( resolve => { + let timer; + const finish = () => { + clearTimeout( timer ); + window.bnInputWaiter = null; + resolve(); + }; + window.bnInputWaiter = finish; + if( delay > 0 ) { timer = setTimeout( finish, delay ); } + } ); + return window.bnInput.shift() ?? -1; +} ); + +EM_JS( void, browser_ungetch, ( int key ), { + window.bnInput.unshift( key ); +} ); + +EM_JS( int, browser_rows, (), { return window.bnRows; } ); +EM_JS( int, browser_columns, (), { return window.bnCols; } ); + +EM_ASYNC_JS( void, browser_present, (), { + window.bnFlushOutput?.(); + if( window.scheduler?.yield ) { + await window.scheduler.yield(); + } else { + await new Promise( resolve => setTimeout( resolve, 0 ) ); + } +} ); +#endif + +namespace +{ +auto next_utf8_byte() -> int +{ +#if defined(__EMSCRIPTEN__) + return browser_getch( 0 ); +#else + return getch(); +#endif +} +} // namespace + struct pairs { catacurses::base_color FG; catacurses::base_color BG; @@ -161,7 +212,10 @@ void refresh_display() void catacurses::doupdate() { - return curses_check_result( ::doupdate(), OK, "doupdate" ); + curses_check_result( ::doupdate(), OK, "doupdate" ); +#if defined(__EMSCRIPTEN__) + browser_present(); +#endif } void catacurses::clear() @@ -280,6 +334,12 @@ catacurses::window catacurses::stdscr; void catacurses::resizeterm() { +#if defined(__EMSCRIPTEN__) + ::resizeterm( browser_rows(), browser_columns() ); + game_ui::init_ui(); + ui_manager::screen_resized(); + catacurses::doupdate(); +#else const int new_x = ::getmaxx( stdscr.get<::WINDOW>() ); const int new_y = ::getmaxy( stdscr.get<::WINDOW>() ); if( ::is_term_resized( new_x, new_y ) ) { @@ -287,6 +347,7 @@ void catacurses::resizeterm() ui_manager::screen_resized(); catacurses::doupdate(); } +#endif } // init_interface is defined in another cpp file, depending on build type: @@ -330,7 +391,11 @@ void input_manager::pump_events() const int prev_timeout = input_timeout; set_timeout( 0 ); do { +#if defined(__EMSCRIPTEN__) + key = browser_getch( 0 ); +#else key = getch(); +#endif if( key == KEY_RESIZE ) { resize = true; } @@ -351,19 +416,31 @@ input_event input_manager::get_input_event() previously_pressed_key = 0; // flush any output catacurses::doupdate(); +#if defined(__EMSCRIPTEN__) + key = browser_getch( input_timeout ); +#else key = getch(); +#endif if( key != ERR ) { int newch; // Clear the buffer of characters that match the one we're going to act on. const int prev_timeout = input_timeout; set_timeout( 0 ); do { +#if defined(__EMSCRIPTEN__) + newch = browser_getch( 0 ); +#else newch = getch(); +#endif } while( newch != ERR && newch == key ); set_timeout( prev_timeout ); // If we read a different character than the one we're going to act on, re-queue it. if( newch != ERR && newch != key ) { +#if defined(__EMSCRIPTEN__) + browser_ungetch( newch ); +#else ungetch( newch ); +#endif } } rval = input_event(); @@ -408,14 +485,14 @@ input_event input_manager::get_input_event() if( key < 127 ) { // Single byte sequence } else if( 194 <= key && key <= 223 ) { - rval.text.append( 1, static_cast( getch() ) ); + rval.text.append( 1, static_cast( next_utf8_byte() ) ); } else if( 224 <= key && key <= 239 ) { - rval.text.append( 1, static_cast( getch() ) ); - rval.text.append( 1, static_cast( getch() ) ); + rval.text.append( 1, static_cast( next_utf8_byte() ) ); + rval.text.append( 1, static_cast( next_utf8_byte() ) ); } else if( 240 <= key && key <= 244 ) { - rval.text.append( 1, static_cast( getch() ) ); - rval.text.append( 1, static_cast( getch() ) ); - rval.text.append( 1, static_cast( getch() ) ); + rval.text.append( 1, static_cast( next_utf8_byte() ) ); + rval.text.append( 1, static_cast( next_utf8_byte() ) ); + rval.text.append( 1, static_cast( next_utf8_byte() ) ); } else { // Other control character, etc. - no text at all, return an event // without the text property diff --git a/src/options.cpp b/src/options.cpp index f3ec2b8..5aa3651 100644 --- a/src/options.cpp +++ b/src/options.cpp @@ -4413,6 +4413,9 @@ void options_manager::cache_to_globals() safe_mode_proximity = ::get_option( "SAFEMODEPROXIMITY" ); parallel_enabled = ::get_option( "MULTITHREADING_ENABLED" ); +#if defined(__EMSCRIPTEN__) + parallel_enabled = false; +#endif parallel_monster_planning = ::get_option( "PARALLEL_MONSTER_PLANNING" ); monster_plan_chunk_size = ::get_option( "MONSTER_PLAN_CHUNK_SIZE" ); parallel_map_cache = ::get_option( "PARALLEL_MAP_CACHE" ); diff --git a/src/thread_pool.cpp b/src/thread_pool.cpp index 2d522da..6b2b26c 100644 --- a/src/thread_pool.cpp +++ b/src/thread_pool.cpp @@ -83,6 +83,11 @@ void cata_thread_pool::submit( std::function task ) cata_thread_pool &get_thread_pool() { +#if defined(__EMSCRIPTEN__) + // Browser builds run the existing synchronous fallback on one thread. + static auto pool = cata_thread_pool( 0 ); + return pool; +#else // Worker count is read once at first call (the static pool is constructed // only once). Changes to THREAD_POOL_WORKERS or MULTITHREADING_ENABLED // require a restart. @@ -104,4 +109,5 @@ cata_thread_pool &get_thread_pool() return hc > 1u ? hc - 1u : 0u; }() ); return pool; +#endif } diff --git a/src/utils/url.cpp b/src/utils/url.cpp index 09082f6..13b3d44 100644 --- a/src/utils/url.cpp +++ b/src/utils/url.cpp @@ -9,6 +9,14 @@ #include auto open_url(const std::string& url) -> void { +#if defined(__EMSCRIPTEN__) + EM_ASM({ + const url = new URL( UTF8ToString( $0 ) ); + if( url.protocol === 'https:' || url.protocol === 'http:' ) { + window.open( url.href, '_blank', 'noopener,noreferrer' ); + } + }, url.c_str() ); +#else static const auto executable = #if defined(_WIN32) "start \"\""; @@ -25,6 +33,7 @@ auto open_url(const std::string& url) -> void { if (exitcode != 0) { debugmsg("Failed to open URL: %s\nAttemped command was: %s", url, command); } +#endif } auto encode_url(const std::string& text) -> std::string {