diff --git a/src/supertux/game_manager.cpp b/src/supertux/game_manager.cpp index 1bf6175cd..97c73b8e9 100644 --- a/src/supertux/game_manager.cpp +++ b/src/supertux/game_manager.cpp @@ -25,6 +25,7 @@ #include "supertux/screen.hpp" #include "supertux/screen_fade.hpp" #include "supertux/screen_manager.hpp" +#include "supertux/wg_snapshot.hpp" #include "supertux/world.hpp" #include "util/log.hpp" #include "util/reader.hpp" @@ -130,6 +131,7 @@ GameManager::start_worldmap(const World& world, const std::string& worldmap_file if (!Editor::current()) m_savegame->get_profile().set_last_world(world.get_basename()); + wg::world_dir = world.get_basedir(); ScreenManager::current()->push_screen(std::move(worldmap)); return true; diff --git a/src/supertux/screen_manager.cpp b/src/supertux/screen_manager.cpp index 5190cc5d1..fbde3a9a9 100644 --- a/src/supertux/screen_manager.cpp +++ b/src/supertux/screen_manager.cpp @@ -30,6 +30,7 @@ #include "supertux/console.hpp" #include "supertux/constants.hpp" #include "supertux/controller_hud.hpp" +#include "supertux/d_scope.hpp" #include "supertux/debug.hpp" #include "supertux/game_manager.hpp" #include "supertux/game_session.hpp" @@ -40,6 +41,12 @@ #include "supertux/resources.hpp" #include "supertux/screen_fade.hpp" #include "supertux/sector.hpp" +#include "supertux/savegame.hpp" +#include "supertux/title_screen.hpp" +#include "supertux/wg_snapshot.hpp" +#include "supertux/world.hpp" +#include "worldmap/worldmap.hpp" +#include "worldmap/worldmap_sector.hpp" #include "util/log.hpp" #include "video/compositor.hpp" #include "video/drawing_context.hpp" @@ -47,6 +54,9 @@ #include #include #include +#include +#include +#include #ifdef __EMSCRIPTEN__ #include @@ -610,8 +620,158 @@ ScreenManager::handle_screen_switch() } } +namespace { +bool g_snapshot_requested = false; +int g_snapshot_count = 0; +bool g_restore_requested = false; +bool g_restore_open_map = false; +} // namespace + +#ifdef __EMSCRIPTEN__ +extern "C" { +EMSCRIPTEN_KEEPALIVE void wg_request_snapshot() { g_snapshot_requested = true; } +EMSCRIPTEN_KEEPALIVE int wg_snapshot_count() { return g_snapshot_count; } +EMSCRIPTEN_KEEPALIVE void wg_restore() { g_restore_requested = true; } +} +#endif + +namespace wg { + +static bool write_snapshot(ScreenManager& sm) +{ + const auto& stack = sm.get_screen_stack(); + Screen* top = stack.empty() ? nullptr : stack.back().get(); + auto* session = dynamic_cast(top); + auto* map = worldmap::WorldMap::current(); + Snapshot s; + // Between the worldmap and a level (its intro, a cutscene): wait. + if (map && !world_dir.empty() && !session && !dynamic_cast(top)) + return false; + if (map && !world_dir.empty()) + { + s.kind = "map"; + s.world = world_dir; + s.map = map->get_filename(); + s.map_sector = map->get_sector().get_name(); + if (session && !session->has_active_sequence()) + { + const auto& players = session->get_current_sector().get_players(); + if (players.empty()) + return false; + Player& player = *players[0]; + // Somewhere he can start again from: not mid-jump, not falling. + if (!player.on_ground() || !player.is_alive()) + return false; + s.kind = "level"; + s.level = session->get_level_file(); + s.sector = session->get_current_sector().get_name(); + s.pos = player.get_pos(); + s.coins = player.get_status().coins; + s.bonus = static_cast(player.get_bonus()); + } + } + mkdir("/wg", 0777); + std::ofstream out(SNAPSHOT_FILE, std::ios::trunc); + out << "supertux-snapshot 1\n" + << "kind " << s.kind << "\n" + << "world " << s.world << "\n" + << "map " << s.map << "\n" + << "mapsector " << s.map_sector << "\n" + << "level " << s.level << "\n" + << "sector " << s.sector << "\n" + << "pos " << s.pos.x << " " << s.pos.y << "\n" + << "coins " << s.coins << "\n" + << "bonus " << s.bonus << "\n"; + return true; +} + +static bool read_snapshot(Snapshot& s) +{ + std::ifstream in(RESTORE_FILE); + std::string line; + if (!std::getline(in, line) || line != "supertux-snapshot 1") + return false; + while (std::getline(in, line)) + { + const auto space = line.find(' '); + if (space == std::string::npos) + continue; + const std::string key = line.substr(0, space), value = line.substr(space + 1); + if (key == "kind") s.kind = value; + else if (key == "world") s.world = value; + else if (key == "map") s.map = value; + else if (key == "mapsector") s.map_sector = value; + else if (key == "level") s.level = value; + else if (key == "sector") s.sector = value; + else if (key == "pos") std::istringstream(value) >> s.pos.x >> s.pos.y; + else if (key == "coins") s.coins = std::stoi(value); + else if (key == "bonus") s.bonus = std::stoi(value); + } + return s.kind == "none" || !s.world.empty(); +} + +void tick() +{ + auto& sm = *ScreenManager::current(); + if (g_snapshot_requested && write_snapshot(sm)) + { + g_snapshot_requested = false; + g_snapshot_count++; + } + if (sm.has_pending_fadeout()) + return; + if (bonus_pending) + { + // Growing looks for room above Tux in his sector, so it runs once the level is up. + const auto& stack = sm.get_screen_stack(); + auto* session = stack.empty() ? nullptr : dynamic_cast(stack.back().get()); + if (session) + { + bonus_pending = false; + BIND_SECTOR(session->get_current_sector()); + const auto& players = session->get_current_sector().get_players(); + if (!players.empty()) + players[0]->set_bonus(static_cast(pending.bonus), false, false); + } + } + if (g_restore_requested) + { + g_restore_requested = false; + Snapshot s; + if (!read_snapshot(s) || s.kind == "none") + return; + pending = s; + enter_pending = false; + bonus_pending = false; + // Back to the title screen first; the worldmap opens on the next frame. + MenuManager::instance().clear_menu_stack(); + for (size_t i = 1; i < sm.get_screen_stack().size(); i++) + sm.pop_screen(); + g_restore_open_map = true; + return; + } + if (g_restore_open_map && sm.get_screen_stack().size() == 1) + { + g_restore_open_map = false; + try + { + auto world = World::from_directory(pending.world); + if (GameManager::current()->start_worldmap(*world, pending.map, pending.map_sector)) + enter_pending = pending.kind == "level"; + } + catch (const std::exception& e) + { + log_warning << "Couldn't resume: " << e.what() << std::endl; + } + } +} + +} // namespace wg + void ScreenManager::loop_iter() { + wg::tick(); + auto now = std::chrono::steady_clock::now(); auto nsecs = std::chrono::duration_cast(now - last_time).count(); elapsed_time += 1e-9f * static_cast(nsecs); diff --git a/src/supertux/wg_snapshot.hpp b/src/supertux/wg_snapshot.hpp new file mode 100644 index 000000000..18825ac14 --- /dev/null +++ b/src/supertux/wg_snapshot.hpp @@ -0,0 +1,41 @@ +// wasmga.me: the play page's Resume snapshots (snapshot.diff). +// +// wg_request_snapshot() asks for one; it is written to SNAPSHOT_FILE at the +// top of a frame, once Tux stands on the ground, and wg_snapshot_count() goes +// up. wg_restore() loads RESTORE_FILE: the screens above the title close, +// the snapshot's worldmap opens, and the level it was in starts again with +// Tux where he stood, with the coins and power-up he had. +#pragma once + +#include + +#include "math/vector.hpp" + +namespace wg { + +inline const char* const SNAPSHOT_FILE = "/wg/snapshot.txt"; +inline const char* const RESTORE_FILE = "/wg/restore.txt"; + +struct Snapshot +{ + std::string kind = "none"; // none, map or level + std::string world, map, map_sector, level, sector; + Vector pos; + int coins = 0; + int bonus = 0; +}; + +/** The folder of the world last opened on a worldmap (World::get_basedir()). */ +inline std::string world_dir; +/** The level the opening worldmap enters, once its sector is up. */ +inline bool enter_pending = false; +/** Set while that level is being entered from the worldmap. */ +inline bool entering = false; +/** Set until Tux, in the level just entered, has the snapshot's power-up. */ +inline bool bonus_pending = false; +inline Snapshot pending; + +/** Runs at the top of every frame. */ +void tick(); + +} // namespace wg diff --git a/src/worldmap/worldmap_sector.cpp b/src/worldmap/worldmap_sector.cpp index 0351be611..5556cfe1e 100644 --- a/src/worldmap/worldmap_sector.cpp +++ b/src/worldmap/worldmap_sector.cpp @@ -42,6 +42,7 @@ #include "supertux/screen_manager.hpp" #include "supertux/shrinkfade.hpp" #include "supertux/tile.hpp" +#include "supertux/wg_snapshot.hpp" #include "util/file_system.hpp" #include "worldmap/camera.hpp" #include "worldmap/level_tile.hpp" @@ -332,6 +333,23 @@ WorldMapSector::update(float dt_sec) } } + // A Resume snapshot's level: Tux goes to its tile and enters it. + if (wg::enter_pending && !m_tux->is_moving()) + { + wg::enter_pending = false; + for (const auto& tile : get_objects_by_type()) + { + if (m_parent.m_levels_path + tile.get_level_filename() == wg::pending.level) + { + m_tux->set_tile_pos(tile.get_tile_pos()); + m_parent.m_enter_level = true; + m_parent.start_level(true); + wg::entering = true; + break; + } + } + } + { if (m_parent.m_enter_level && !m_tux->is_moving()) { @@ -364,7 +382,17 @@ WorldMapSector::update(float dt_sec) m_parent.m_really_enter_level = false; m_parent.m_skip_cutscene = false; } + const bool resuming = wg::entering; + if (resuming) + { + wg::entering = false; + m_parent.get_savegame().get_player_status().coins = wg::pending.coins; + game_session->set_start_pos(wg::pending.sector, wg::pending.pos); + } game_session->restart_level(); + // The power-up goes on once the level's sector is the current one (wg::tick). + if (resuming) + wg::bonus_pending = true; // update state and savegame m_parent.save_state();