diff --git a/builtin/common/settings/dlg_settings.lua b/builtin/common/settings/dlg_settings.lua index f9e8d5c27..a4c05fdf6 100644 --- a/builtin/common/settings/dlg_settings.lua +++ b/builtin/common/settings/dlg_settings.lua @@ -379,7 +379,8 @@ local function check_requirements(name, requires, context) local shadows_support = core.driver_supports_shadows() local special = { android = PLATFORM == "Android", - desktop = PLATFORM ~= "Android", + -- on the web the page sizes the game's window and keeps its files + desktop = PLATFORM ~= "Android" and PLATFORM ~= "Emscripten", touch_support = touch_support, -- When touch_controls is "auto", we don't know which input method will -- be used, so we show settings for both. diff --git a/builtin/mainmenu/dlg_config_world.lua b/builtin/mainmenu/dlg_config_world.lua index a0c1bc0a1..61501cf5c 100644 --- a/builtin/mainmenu/dlg_config_world.lua +++ b/builtin/mainmenu/dlg_config_world.lua @@ -198,9 +198,14 @@ local function get_formspec(data) "button[3.25,7;2.5,0.5;btn_config_world_cancel;" .. fgettext("Cancel") .. "]" .. "button[5.75,7;2.5,0.5;btn_config_world_save;" .. - fgettext("Save") .. "]" .. - "button[9,7;2.5,0.5;btn_config_world_cdb;" .. - fgettext("Find More Mods") .. "]" + fgettext("Save") .. "]" + + -- No ContentDB on the web + if PLATFORM ~= "Emscripten" then + retval = retval .. + "button[9,7;2.5,0.5;btn_config_world_cdb;" .. + fgettext("Find More Mods") .. "]" + end if mod.name ~= "" and not mod.always_on then if mod.is_modpack then diff --git a/builtin/mainmenu/init.lua b/builtin/mainmenu/init.lua index eca3c4058..c9e93c222 100644 --- a/builtin/mainmenu/init.lua +++ b/builtin/mainmenu/init.lua @@ -49,6 +49,10 @@ local tabs = { local function main_event_handler(tabview, event) if event == "MenuQuit" then + -- The web page is the game's window: there is nothing to quit to + if PLATFORM == "Emscripten" then + return true + end local show_dialog = core.settings:get_bool("enable_esc_dialog") if not ui.childlist["mainmenu_quit_confirm"] and show_dialog then tabview:hide() @@ -111,8 +115,11 @@ local function init_globals() tv_main:set_autosave_tab(true) tv_main:add(tabs.local_game) - tv_main:add(tabs.play_online) - tv_main:add(tabs.content) + -- On the web: single player only, without ContentDB + if PLATFORM ~= "Emscripten" then + tv_main:add(tabs.play_online) + tv_main:add(tabs.content) + end tv_main:add(tabs.about) tv_main:set_global_event_handler(main_event_handler) @@ -143,10 +150,12 @@ local function init_globals() -- synchronous, chain parents to only show one at a time local parent = tv_main parent = migrate_keybindings(parent) - check_reinstall_mtg(parent) + if PLATFORM ~= "Emscripten" then + check_reinstall_mtg(parent) - -- asynchronous, will only be shown if we're still on "maintab" - check_new_version() + -- asynchronous, will only be shown if we're still on "maintab" + check_new_version() + end end assert(os.execute == nil) diff --git a/builtin/mainmenu/tab_about.lua b/builtin/mainmenu/tab_about.lua index b0b10f534..218bb3917 100644 --- a/builtin/mainmenu/tab_about.lua +++ b/builtin/mainmenu/tab_about.lua @@ -88,7 +88,9 @@ return { core.formspec_escape(version.project .. " " .. version.string) .. "]" .. "button_url[1.5,4.1;2.5,0.8;homepage;luanti.org;https://www.luanti.org/]" - if PLATFORM == "Android" then + if PLATFORM == "Emscripten" then + -- no file manager on the web + elseif PLATFORM == "Android" then fs = fs .. "button[0.5,5.1;4.5,0.8;share_debug;" .. fgettext("Share debug log") .. "]" else fs = fs .. "tooltip[userdata;" .. diff --git a/builtin/mainmenu/tab_local.lua b/builtin/mainmenu/tab_local.lua index 1c318fd04..4cea13d60 100644 --- a/builtin/mainmenu/tab_local.lua +++ b/builtin/mainmenu/tab_local.lua @@ -114,18 +114,25 @@ function singleplayer_refresh_gamebar() btnbar:add_button(btn_name, text, image, tooltip) end - local plus_image = core.formspec_escape(defaulttexturedir .. "plus.png") - btnbar:add_button("game_open_cdb", "", plus_image, fgettext("Install games from ContentDB")) + if PLATFORM ~= "Emscripten" then + local plus_image = core.formspec_escape(defaulttexturedir .. "plus.png") + btnbar:add_button("game_open_cdb", "", plus_image, fgettext("Install games from ContentDB")) + end return true end local function get_disabled_settings(game) + -- On the web: single player only + local disabled_settings = {} + if PLATFORM == "Emscripten" then + disabled_settings["enable_server"] = false + core.settings:set_bool("enable_server", false) + end if not game then - return {} + return disabled_settings end local gameconfig = Settings(game.path .. "/game.conf") - local disabled_settings = {} if gameconfig then local disabled_settings_str = (gameconfig:get("disabled_settings") or ""):split() for _, value in pairs(disabled_settings_str) do diff --git a/irr/src/CIrrDeviceSDL.cpp b/irr/src/CIrrDeviceSDL.cpp index 139e98886..5e28c6151 100644 --- a/irr/src/CIrrDeviceSDL.cpp +++ b/irr/src/CIrrDeviceSDL.cpp @@ -1408,7 +1408,8 @@ u32 CIrrDeviceSDL::getFullscreenFlag(bool fullscreen) if (!fullscreen) return 0; #ifdef _IRR_EMSCRIPTEN_PLATFORM_ - return SDL_WINDOW_FULLSCREEN; + // The web page owns full screen + return 0; #else return SDL_WINDOW_FULLSCREEN_DESKTOP; #endif @@ -1419,6 +1420,10 @@ bool CIrrDeviceSDL::setFullscreen(bool fullscreen) { if (!Window) return false; +#ifdef _IRR_EMSCRIPTEN_PLATFORM_ + // The web page owns full screen + return false; +#endif #ifdef _IRR_USE_SDL3_ bool success = SDL_SetWindowFullscreen(Window, fullscreen); #else diff --git a/src/client/game_formspec.cpp b/src/client/game_formspec.cpp index b10e8dace..08f743ca4 100644 --- a/src/client/game_formspec.cpp +++ b/src/client/game_formspec.cpp @@ -416,9 +416,11 @@ void GameFormSpec::showPauseMenu() // TRANSLATORS: Pause menu button, try to keep the translation short << strgettext("Exit to Menu") << "]"; +#ifndef __EMSCRIPTEN__ // the web page is the game's window: nothing to exit to os << "button_exit[0," << (ypos += button_height + spacing) << ";3.5," << button_height << ";btn_exit_os;" // TRANSLATORS: Pause menu button, try to keep the translation short (OS = Operating System) << strgettext("Exit to OS") << "]"; +#endif os << "container_end[]"; if (!control_text.empty()) { diff --git a/src/client/inputhandler.cpp b/src/client/inputhandler.cpp index 0f86e1eac..45e25bbb9 100644 --- a/src/client/inputhandler.cpp +++ b/src/client/inputhandler.cpp @@ -3,6 +3,9 @@ // Copyright (C) 2010-2013 celeron55, Perttu Ahola // Copyright (C) 2017 nerzhul, Loic Blot +#ifdef __EMSCRIPTEN__ +#include +#endif #include "porting.h" #include "settings.h" #include "util/numeric.h" @@ -231,6 +234,13 @@ bool MyEventReceiver::OnEvent(const SEvent &event) if (keySettingHasMatch("keymap_fullscreen", keyCode)) { if (event.KeyInput.PressedDown && !fullscreen_is_down) { +#ifdef __EMSCRIPTEN__ + // The web page owns full screen + MAIN_THREAD_ASYNC_EM_ASM({ + if (typeof wg_toggle_fullscreen === "function") + wg_toggle_fullscreen(); + }); +#else IrrlichtDevice *device = RenderingEngine::get_raw_device(); bool new_fullscreen = !device->isFullscreen(); @@ -238,6 +248,7 @@ bool MyEventReceiver::OnEvent(const SEvent &event) if (device->setFullscreen(new_fullscreen)) { g_settings->setBool("fullscreen", new_fullscreen); } +#endif } fullscreen_is_down = event.KeyInput.PressedDown; return true; diff --git a/src/mainloop.cpp b/src/mainloop.cpp index a61a459ff..8def4b4d7 100644 --- a/src/mainloop.cpp +++ b/src/mainloop.cpp @@ -1907,3 +1907,117 @@ int main(int argc, char *argv[]) }, retval); return retval; } + +///////////////////////////////////////////////////////////////////////////// +// wasmga.me: the player's files for the play page +///////////////////////////////////////////////////////////////////////////// + +// The play page keeps minetest.conf and the worlds as the game's save. It asks +// with wg_request_files() and gets them in wg_files(ptr, len) (ptr is the +// page's to free), or nothing when no file changed since the last answer. The +// files are read on a thread of their own, as the browser thread must never +// wait on the file system, and between server steps, so a world comes whole. +// Laid out as, for each file: u32 path length, path, u32 size, bytes. + +extern "C" { + EMSCRIPTEN_KEEPALIVE + void wg_request_files(); +} + +std::mutex wg_files_mutex; + +static std::mutex wg_request_mutex; +static std::condition_variable wg_request_cond; +static bool wg_requested = false; + +struct WgFile { + std::string path; + std::string full; + off_t size; +}; + +static void wg_list(const std::string &dir, const std::string &prefix, + std::vector &out, std::string &sig) { + DIR *d = opendir(dir.c_str()); + if (!d) + return; + std::vector names; + while (struct dirent *e = readdir(d)) { + std::string n = e->d_name; + if (n != "." && n != "..") + names.push_back(n); + } + closedir(d); + std::sort(names.begin(), names.end()); + for (const std::string &n : names) { + std::string full = dir + "/" + n; + struct stat st; + if (stat(full.c_str(), &st) != 0) + continue; + if (S_ISDIR(st.st_mode)) { + wg_list(full, prefix + n + "/", out, sig); + } else if (S_ISREG(st.st_mode)) { + out.push_back({prefix + n, full, st.st_size}); + sig += prefix + n + ":" + std::to_string(st.st_size) + ":" + + std::to_string(st.st_mtim.tv_sec) + "." + + std::to_string(st.st_mtim.tv_nsec) + "\n"; + } + } +} + +static void wg_put32(std::string &out, uint32_t v) { + out.append(reinterpret_cast(&v), 4); +} + +static void wg_files_thread() { + std::string last; + for (;;) { + { + std::unique_lock lock(wg_request_mutex); + wg_request_cond.wait(lock, []() { return wg_requested; }); + wg_requested = false; + } + std::vector files; + std::string sig; + std::string out; + { + std::lock_guard lock(wg_files_mutex); + struct stat st; + std::string conf = LUANTI_ROOT "/minetest.conf"; + if (stat(conf.c_str(), &st) == 0 && S_ISREG(st.st_mode)) { + files.push_back({"minetest.conf", conf, st.st_size}); + sig += "minetest.conf:" + std::to_string(st.st_size) + ":" + + std::to_string(st.st_mtim.tv_sec) + "." + + std::to_string(st.st_mtim.tv_nsec) + "\n"; + } + wg_list(LUANTI_ROOT "/worlds", "worlds/", files, sig); + if (sig == last) + continue; + for (const WgFile &f : files) { + std::ifstream in(f.full, std::ios::binary); + std::string data((std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + wg_put32(out, f.path.size()); + out += f.path; + wg_put32(out, data.size()); + out += data; + } + } + last = sig; + char *buf = (char *)malloc(out.size() ? out.size() : 1); + if (!buf) + continue; + memcpy(buf, out.data(), out.size()); + MAIN_THREAD_ASYNC_EM_ASM({ wg_files($0, $1); }, buf, out.size()); + } +} + +void wg_request_files() { + static std::once_flag started; + std::call_once(started, []() { std::thread(wg_files_thread).detach(); }); + { + std::lock_guard lock(wg_request_mutex); + wg_requested = true; + } + wg_request_cond.notify_one(); +} diff --git a/src/mainloop.h b/src/mainloop.h index 82e2dce64..8aba50f98 100644 --- a/src/mainloop.h +++ b/src/mainloop.h @@ -3,3 +3,8 @@ #include #include +#include + +// wasmga.me: held while the server writes its world, so the play page's copy +// of the player's files (wg_request_files) never catches a world half written. +extern std::mutex wg_files_mutex; diff --git a/src/porting.h b/src/porting.h index beb5bd017..9f7311713 100644 --- a/src/porting.h +++ b/src/porting.h @@ -247,6 +247,8 @@ inline const char *getPlatformName() return #if defined(ANDROID) "Android" +#elif defined(__EMSCRIPTEN__) + "Emscripten" #elif defined(__linux__) "Linux" #elif defined(_WIN32) || defined(_WIN64) diff --git a/src/server.cpp b/src/server.cpp index 4a43deee0..944e464cf 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -75,6 +75,9 @@ #include #include #include +#ifdef __EMSCRIPTEN__ +#include "mainloop.h" +#endif class ClientNotFoundException : public BaseException { @@ -380,6 +383,10 @@ Server::~Server() if (m_emerge) m_emerge->stopThreads(); +#ifdef __EMSCRIPTEN__ + std::unique_lock wg_lock(wg_files_mutex); +#endif + if (m_env) { EnvAutoLock envlock(this); @@ -429,6 +436,9 @@ Server::~Server() // Write any changes before deletion. if (m_mod_storage_database) m_mod_storage_database->endSave(); +#ifdef __EMSCRIPTEN__ + wg_lock.unlock(); +#endif // Clean up files for (auto &it : m_media) { @@ -660,6 +670,9 @@ void Server::step() void Server::AsyncRunStep(float dtime, bool initial_step) { ZoneScoped; +#ifdef __EMSCRIPTEN__ + std::lock_guard wg_lock(wg_files_mutex); +#endif auto framemarker = FrameMarker("Server::AsyncRunStep()-frame").started(); if (!m_async_fatal_error.get().empty()) {