From d5ef126264a4c398108dfff8495ee6115aec3482 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Sat, 24 Oct 2026 22:37:20 +0600 Subject: [PATCH] working on refactoring --- src/Campaign.cpp | 17 ++ src/Campaign.h | 9 ++ src/Game.cpp | 288 ++++++++++++++++++---------------- src/Game.h | 48 +++--- src/GameState.cpp | 8 + src/GameState.h | 7 +- src/Location.cpp | 110 ++++++++----- src/Location.h | 11 +- src/MenuManager.cpp | 12 +- src/MenuManager.h | 4 +- src/render/TextureManager.cpp | 7 +- src/render/TextureManager.h | 7 +- 12 files changed, 309 insertions(+), 219 deletions(-) diff --git a/src/Campaign.cpp b/src/Campaign.cpp index 1734a0a..811a564 100644 --- a/src/Campaign.cpp +++ b/src/Campaign.cpp @@ -44,6 +44,7 @@ namespace FRG { out.lightsJsonPath = j.value("lights", ""); if (j.contains("playerPosition")) out.playerPosition = readVec3(j["playerPosition"]); out.lockInteractiveObjectsDuringTutorial = j.value("lockInteractiveObjectsDuringTutorial", false); + out.loadingScreenPath = j.value("loadingScreen", ""); if (j.contains("taxi") && j["taxi"].is_object()) { const json& t = j["taxi"]; @@ -71,6 +72,18 @@ namespace FRG { return titles.empty() ? id : titles.begin()->second; } + const LocationConfig* CampaignConfig::findLocation(const std::string& locationId) const { + for (const auto& loc : locations) { + if (loc.id == locationId) return &loc; + } + return nullptr; + } + + std::string CampaignConfig::loadingScreenFor(const std::string& locationId) const { + const LocationConfig* loc = findLocation(locationId); + return (loc && !loc->loadingScreenPath.empty()) ? loc->loadingScreenPath : loadingScreenPath; + } + CampaignRegistry& CampaignRegistry::instance() { static CampaignRegistry reg; return reg; @@ -97,6 +110,10 @@ namespace FRG { c.questsPath = entry.value("quests", ""); c.tutorial = entry.value("tutorial", false); c.loadingScreenPath = entry.value("loadingScreen", std::string(CampaignConfig::kDefaultLoadingScreen)); + for (const std::string& path : c.locationConfigPaths) { + LocationConfig loc; + if (LocationConfig::fromFile(path, zipFile, loc)) c.locations.push_back(std::move(loc)); + } campaigns_.push_back(std::move(c)); } diff --git a/src/Campaign.h b/src/Campaign.h index 7e4a188..973e1d2 100644 --- a/src/Campaign.h +++ b/src/Campaign.h @@ -36,6 +36,9 @@ namespace FRG { // Interactive objects stay locked until the tutorial reaches "pick up an item". bool lockInteractiveObjectsDuringTutorial = false; + // UI layout shown while this location loads; empty = the campaign's loading screen. + std::string loadingScreenPath; + // Returns false (and logs) when the file can't be read or parsed. static bool fromFile(const std::string& path, const std::string& zipFile, LocationConfig& out); }; @@ -45,6 +48,8 @@ namespace FRG { std::string id; std::map titles; // language code -> title std::vector locationConfigPaths; + // Parsed from locationConfigPaths when the registry loads. + std::vector locations; std::string startLocation; std::string startMusic; std::string startStatePath; @@ -56,6 +61,10 @@ namespace FRG { static constexpr const char* kDefaultLoadingScreen = "resources/w/ui/screen_loading.json"; std::string title(Language lang) const; + // Returns nullptr if the campaign has no such location. + const LocationConfig* findLocation(const std::string& locationId) const; + // The location's own loading screen, else the campaign's. + std::string loadingScreenFor(const std::string& locationId) const; }; // Campaigns listed in resources/config/campaigns.json, in display order. diff --git a/src/Game.cpp b/src/Game.cpp index fff1ff4..231237e 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -328,7 +328,7 @@ namespace FRG }); // Campaigns are only listed here; their locations are built on demand when the - // player starts or loads one (see loadCampaignThen). MenuManager::setup() needs + // player starts or loads one (see enterLocationThen). MenuManager::setup() needs // the list to preload each campaign's loading screen. CampaignRegistry::instance().loadFromJson("resources/config/campaigns.json", CONST_ZIP_FILE); @@ -369,8 +369,6 @@ namespace FRG }; menuManager.callTaxiFunc = [this]() { - // The taxi hint has served its purpose once a taxi is on its way. - for (auto& [name, loc] : gameState.locations) loc->onPlayerTaxiRequired = nullptr; if (currentLocation()) currentLocation()->onTaxiCalled(); }; }); @@ -385,8 +383,10 @@ namespace FRG } menuManager.startGameFunc = [this](const std::string& campaignId) { - auto start = [this, campaignId]() { - loadCampaignThen(campaignId, [this]() { + const CampaignConfig* config = CampaignRegistry::instance().find(campaignId); + const std::string startLocation = config ? config->startLocation : std::string(); + auto start = [this, campaignId, startLocation]() { + enterLocationThen(campaignId, startLocation, [this]() { performResetToInitialState(); applyTutorialSetting(); const CampaignConfig& campaign = activeCampaign(); @@ -397,12 +397,12 @@ namespace FRG menuManager.lastMusicTrack = campaign.startMusic; }); }; - if (campaignLocations.count(campaignId)) { + if (isLocationLoaded(campaignId, startLocation)) { // Already loaded: the reset runs right away, so keep it off the menu // button's call stack. mainThreadHandler.EnqueueMainThreadTask(start); } else { - // Not loaded: this only shows the loading screen and queues the build + // Not loaded: this only shows the loading screen and queues the load // steps, so start it now -- deferring it would draw one more frame of // the main menu before the loading screen appears. start(); @@ -480,20 +480,6 @@ namespace FRG loadStepsDone = 0; } - void Game::rearmOneShotCallbacks(LocationMap& locations) { - for (auto& [name, loc] : locations) { - loc->onPlayerTaxiRequired = [this]() { - menuManager.tutorialShowTaxiHint(); - }; - } - } - - void Game::activateCampaign(const std::string& campaignId) { - gameState.campaignId = campaignId; - gameState.locations = campaignLocations[campaignId]; - gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE); - } - const CampaignConfig& Game::activeCampaign() const { const CampaignConfig* campaign = CampaignRegistry::instance().find(gameState.campaignId); if (!campaign) campaign = CampaignRegistry::instance().find(CampaignRegistry::kDefaultCampaignId); @@ -504,55 +490,130 @@ namespace FRG return *campaign; } - void Game::loadCampaignThen(const std::string& requestedId, std::function then) { - const CampaignConfig* campaign = CampaignRegistry::instance().find(requestedId); + bool Game::isLocationLoaded(const std::string& campaignId, const std::string& locationId) const { + return player && loadedCampaignId == campaignId && gameState.locations.count(locationId) > 0; + } + + void Game::enterLocationThen(const std::string& requestedCampaignId, const std::string& requestedLocationId, + std::function then) + { + const CampaignConfig* campaign = CampaignRegistry::instance().find(requestedCampaignId); if (!campaign) { - std::cerr << "[campaign] Unknown campaign '" << requestedId << "', using '" + std::cerr << "[campaign] Unknown campaign '" << requestedCampaignId << "', using '" << CampaignRegistry::kDefaultCampaignId << "'" << std::endl; campaign = CampaignRegistry::instance().find(CampaignRegistry::kDefaultCampaignId); if (!campaign) return; } const std::string campaignId = campaign->id; + const std::string locationId = campaign->findLocation(requestedLocationId) + ? requestedLocationId : campaign->startLocation; - if (campaignLocations.count(campaignId)) { - activateCampaign(campaignId); + if (isLocationLoaded(campaignId, locationId)) { then(); return; } - // Build it behind the campaign's loading screen, one location per loading step. - // The screen is hidden when the sequence completes (see runAsLoadingSequence). - menuManager.showLoadingScreen(campaign->loadingScreenPath); + // Behind the location's loading screen: release what's loaded first, then load the + // new data, then continue. The screen is hidden when the sequence completes (see + // runAsLoadingSequence). + menuManager.showLoadingScreen(campaign->loadingScreenFor(locationId)); std::vector> steps; - steps.push_back([this, campaignId]() { - unloadCampaigns(); // only one campaign is kept in memory - campaignLocations[campaignId]; - gameState.globalFloats["player_hp"] = 200; // read by location scripts as they load - }); - for (const std::string& configPath : campaign->locationConfigPaths) { - steps.push_back([this, campaignId, configPath]() { - LocationConfig config; - if (LocationConfig::fromFile(configPath, CONST_ZIP_FILE, config)) - campaignLocations[campaignId][config.id] = createLocation(config); - }); + if (loadedCampaignId != campaignId) { + steps.push_back([this]() { unloadCampaign(); }); + steps.push_back([this, campaignId]() { loadCampaign(campaignId); }); + } else { + steps.push_back([this]() { unloadLocation(true); }); } - steps.push_back([this, campaignId, then]() { - LocationMap& locations = campaignLocations[campaignId]; - wireCampaignLocations(locations); - if (shadowMap) { - for (auto& [name, loc] : locations) loc->shadowMap = shadowMap; - } - activateCampaign(campaignId); - then(); - }); + steps.push_back([this, locationId]() { loadLocation(locationId); }); + steps.push_back([then]() { then(); }); menuManager.runAsLoadingSequence(std::move(steps)); } - void Game::unloadCampaigns() { + void Game::loadCampaign(const std::string& campaignId) { + loadedCampaignId = campaignId; + gameState.campaignId = campaignId; + gameState.storedLocationStates.clear(); + gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE); + gameState.globalFloats["player_hp"] = 200; // read by location scripts as they load + // The player stays loaded for the whole campaign; locations come and go around him. + player = Location::createPlayer(renderer, audioPlayer.get()); + if (player) + player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); }; + } + + void Game::unloadCampaign() { + unloadLocation(false); + player.reset(); + loadedCampaignId.clear(); + gameState.storedLocationStates.clear(); + renderer.textureManager.UnloadUnused(); // the player's textures + } + + void Game::loadLocation(const std::string& locationId) { + const LocationConfig* config = activeCampaign().findLocation(locationId); + if (!config) { + std::cerr << "[campaign] Campaign '" << gameState.campaignId << "' has no location '" + << locationId << "'" << std::endl; + return; + } + std::shared_ptr loc = createLocation(*config); + wireLocation(loc.get()); + if (shadowMap) loc->shadowMap = shadowMap; + // Back to how the player left it (or to the start state's snapshot of it). + auto stored = gameState.storedLocationStates.find(locationId); + if (stored != gameState.storedLocationStates.end()) loc->load(stored->second); + gameState.locations[locationId] = loc; + } + + void Game::unloadLocation(bool storeState) { + for (auto& [name, loc] : gameState.locations) { + if (!storeState) continue; + nlohmann::json locJson; + loc->save(locJson); + gameState.storedLocationStates[name] = std::move(locJson); + } gameState.locations.clear(); - campaignLocations.clear(); - // Drop the cache entries of the textures the campaign loaded; UI images stay. - renderer.textureManager.UnloadNonResident("resources/w/ui"); + // Textures only the location's models and characters used are freed now. + renderer.textureManager.UnloadUnused(); + } + + void Game::teleportPlayer(const std::string& destName, const Eigen::Vector3f& destPos, float destRotY) { + logger() << "[TELEPORT] " << " -> " << destName << std::endl; + if (!activeCampaign().findLocation(destName)) { + std::cerr << "[TELEPORT] Unknown destination location: " << destName << std::endl; + return; + } + if (currentLocation()) + { + currentLocation()->scriptEngine.callLocationExitCallback(); + currentLocation()->dialogueSystem.stopDialogue(); + } + // Runs from inside the current location's update(); the location itself is + // released by a later loading step. + enterLocationThen(gameState.campaignId, destName, [this, destName, destPos, destRotY]() { + gameState.currentLocationName = destName; + Location* loc = currentLocation(); + if (!loc) return; + if (loc->player) { + loc->player->state.position = destPos; + loc->player->setTarget(destPos); + loc->player->stopInPlace(); + loc->player->state.facingAngle = destRotY; + loc->player->state.targetFacingAngle = destRotY; + } + loc->state.cameraAzimuth = destRotY; + loc->state.isDarklands = gameState.isDarklands; + loc->state.isNight = gameState.isNight; + loc->state.isDawn = gameState.isDawn; + loc->scriptEngine.callLocationEnterCallback(); + + //Reset taxi + loc->state.taxiCar = loc->state.taxiCarInitialState; + loc->state.taxiArrivedCallbackCalled = false; + gameState.taxiIsCalled = false; + + menuManager.onLocationChanged(destName); + }); } void Game::performFullReset() { @@ -570,28 +631,22 @@ namespace FRG gameState.tutorialPhoneChatScreenOpened = false; gameState.tutorialJournalScreenOpened = false; gameState.tutorialNeedOpenTaxiScreen = false; + gameState.tutorialTaxiHintDone = false; for (int i = 0; i < 3; ++i) { gameState.chatUnread[i] = true; gameState.chatPreviewMsg[i].clear(); gameState.chatHistory[i].clear(); } gameState.inventory.clear(); + gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE); - const CampaignConfig& campaign = activeCampaign(); - gameState.questJournal.loadFromFile(campaign.questsPath, CONST_ZIP_FILE); - - LocationMap& locations = campaignLocations[campaign.id]; - locations.clear(); - for (const std::string& configPath : campaign.locationConfigPaths) { - LocationConfig config; - if (LocationConfig::fromFile(configPath, CONST_ZIP_FILE, config)) - locations[config.id] = createLocation(config); - } - wireCampaignLocations(locations); - gameState.locations = locations; - if (shadowMap) { - for (auto& [name, loc] : locations) loc->shadowMap = shadowMap; - } + // Rebuild the loaded location from its config; the others have no stored state + // any more, so they are built fresh when the player first goes there. + gameState.storedLocationStates.clear(); + std::vector loadedIds; + for (auto& [name, loc] : gameState.locations) loadedIds.push_back(name); + unloadLocation(false); + for (const std::string& id : loadedIds) loadLocation(id); // Developer workflow: uncomment to (re)generate the campaign's start-state JSON // from this freshly-rebuilt state after changing gameplay/content JSON or Lua. @@ -625,16 +680,9 @@ namespace FRG // must be cleared explicitly before loading it. gameState.globalInts.clear(); gameState.globalFloats.clear(); - gameState.load(root); // updates existing Location objects in place -- no - // destruction, no re-parsing JSON/nav files, no new - // sol2 state (GameState::load only touches map entries - // that already exist). + gameState.load(root); // applies to the loaded location in place and stores + // the other locations' states for when they load. gameState.campaignId = campaignId; // the snapshot may predate campaigns - // Locations survive this reset, so any one-shot callbacks they consumed - // during the previous playthrough (taxi request) must be - // re-armed here -- wireCampaignLocations() normally does this, but doesn't - // run on this fast path. - rearmOneShotCallbacks(gameState.locations); return; } catch (const std::exception& e) { logger() << "[reset] Failed to parse " << path << ": " << e.what() @@ -686,6 +734,7 @@ namespace FRG params.triggerZonesJsonPath = config.triggerZonesJsonPath; params.lightsJsonPath = config.lightsJsonPath; params.playerPosition = config.playerPosition; + params.player = player; auto loc = std::make_shared(renderer, gameState.inventory); loc->setup(params, &gameState.questJournal, audioPlayer.get()); @@ -705,8 +754,6 @@ namespace FRG loc->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) { menuManager.setChatUnread(idx, unread, msg); }; - if (loc->player) - loc->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); }; for (auto& npc : loc->npcs) { if (npc && npc->state.canAttack) { npc->state.onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); }; @@ -744,59 +791,26 @@ namespace FRG return loc; } - void Game::wireCampaignLocations(LocationMap& locations) + void Game::wireLocation(Location* loc) { - rearmOneShotCallbacks(locations); - - // Teleport callbacks: destination name and position come from the teleport zone data. - auto teleportCallback = [this](const std::string& destName, const Eigen::Vector3f& destPos, float destRotY) { - logger() << "[TELEPORT] " << " -> " << destName << std::endl; - auto it = gameState.locations.find(destName); - if (it == gameState.locations.end()) { - std::cerr << "[TELEPORT] Unknown destination location: " << destName << std::endl; - return; - } - if (currentLocation()) - { - currentLocation()->scriptEngine.callLocationExitCallback(); - currentLocation()->dialogueSystem.stopDialogue(); - } - gameState.currentLocationName = destName; - Location* loc = currentLocation(); - if (loc->player) { - loc->player->state.position = destPos; - loc->player->setTarget(destPos); - loc->player->stopInPlace(); - loc->player->state.facingAngle = destRotY; - loc->player->state.targetFacingAngle = destRotY; - } - loc->state.cameraAzimuth = destRotY; - loc->state.isDarklands = gameState.isDarklands; - loc->state.isNight = gameState.isNight; - loc->state.isDawn = gameState.isDawn; - loc->scriptEngine.callLocationEnterCallback(); - - //Reset taxi - loc->state.taxiCar = loc->state.taxiCarInitialState; - loc->state.taxiArrivedCallbackCalled = false; - gameState.taxiIsCalled = false; - - menuManager.onLocationChanged(destName); + loc->onPlayerTaxiRequired = [this]() { + menuManager.tutorialShowTaxiHint(); }; - for (auto& [name, loc] : locations) { - loc->onTeleport = teleportCallback; - } + // Teleport zones: destination name and position come from the teleport zone data. + loc->onTeleport = [this](const std::string& destName, const Eigen::Vector3f& destPos, float destRotY) { + teleportPlayer(destName, destPos, destRotY); + }; // Share the global int store with all dialogue runtimes so flags set via // dialogue JSON and via Lua set_dialogue_flag all see the same state. - for (auto& [name, loc] : locations) { + { loc->dialogueSystem.setGlobalFlagStore(&gameState.globalInts); } - // Wire tutorial advance callbacks for all locations. + // Wire tutorial advance callbacks. // completeTutorialStep() only acts on the current step, so sharing is safe. - for (auto& [name, loc] : locations) { + { loc->dialogueSystem.setOnDialogueAdvanced([this]() { gameState.completeTutorialStep(TutorialStep::AdvanceDialogue); }); @@ -807,14 +821,14 @@ namespace FRG } // Wire chat-bubble callback so dynamic bubbles appear as dialogue lines are shown. - for (auto& [name, loc] : locations) { + { loc->dialogueSystem.setOnChatBubbleReady([this](const std::string& text, bool incoming) { menuManager.onChatBubbleReady(text, incoming); }); } // Wire cutscene HUD: show skip button when cutscene starts, restore HUD when it ends. - for (auto& [name, loc] : locations) { + { loc->dialogueSystem.setOnCutsceneStarted([this]() { menuManager.onCutsceneStarted(); }); @@ -823,12 +837,11 @@ namespace FRG }); } - // Wire HP-change callbacks so all player instances update the health bar HUD. - for (auto& [name, locPtr] : locations) { - if (locPtr->player) { - // Raw pointer: the location owns this player, so a shared_ptr here would be - // a reference cycle that keeps unloaded locations alive. - Location* loc = locPtr.get(); + // Wire HP-change callbacks so the player updates the health bar HUD. + { + if (loc->player) { + // Raw pointer: the player belongs to the location (and outlives it), so a + // shared_ptr here would keep unloaded locations alive. Rewired on every load. loc->player->state.onHpChanged = [this, loc](float hp, float maxHp, int attackerIndex) { menuManager.updateHealthBar(hp, maxHp); @@ -868,9 +881,7 @@ namespace FRG CheckGlError(__FILE__, __LINE__); logger() << "Shadow map initialized" << std::endl; - for (auto& [campaignId, locations] : campaignLocations) { - for (auto& [name, loc] : locations) loc->shadowMap = shadowMap; - } + for (auto& [name, loc] : gameState.locations) loc->shadowMap = shadowMap; } void Game::destroyShadowMapAndProparateToLocations() @@ -880,9 +891,7 @@ namespace FRG shadowMap.reset(); CheckGlError(__FILE__, __LINE__); - for (auto& [campaignId, locations] : campaignLocations) { - for (auto& [name, loc] : locations) loc->shadowMap.reset(); - } + for (auto& [name, loc] : gameState.locations) loc->shadowMap.reset(); CheckGlError(__FILE__, __LINE__); } @@ -1142,7 +1151,7 @@ namespace FRG if (!menuManager.isMainMenuOpen()) menuManager.update(static_cast(delta)); - if (currentLocation()) + if (loadingCompleted && currentLocation()) { currentLocation()->update(delta); } @@ -1825,9 +1834,10 @@ namespace FRG return; } - // The save's campaign may not be loaded yet; the state is applied once it is. + // The save's campaign and location may not be loaded yet; the state is applied once they are. const std::string campaignId = root.value("campaignId", std::string(CampaignRegistry::kDefaultCampaignId)); - loadCampaignThen(campaignId, [this, root]() { + const std::string locationId = root.value("currentLocationName", std::string()); + enterLocationThen(campaignId, locationId, [this, root]() { gameState.load(root); playMusicForLoadedGame(); menuManager.enterGameplay(true); diff --git a/src/Game.h b/src/Game.h index 64685a2..2fca701 100644 --- a/src/Game.h +++ b/src/Game.h @@ -128,46 +128,50 @@ namespace FRG { void loadGame(int slot); void playMusicForLoadedGame(); SaveSlotInfo readSlotInfo(int slot) const; - // Locations of the loaded campaign (campaign id -> locations); at most one entry, - // empty until the player starts or loads a game. gameState.locations is a copy - // of the active campaign's entry. - std::unordered_map campaignLocations; + // Loading model: one campaign at a time, and within it only the location the player + // is in (gameState.locations). Leaving a location stores its state in + // gameState.storedLocationStates and releases it; the player is created once per + // campaign and shared by its locations. + std::string loadedCampaignId; + std::shared_ptr player; + + bool isLocationLoaded(const std::string& campaignId, const std::string& locationId) const; + // Runs `then` once `locationId` of `campaignId` is the loaded location. If it isn't, + // it is loaded first behind its loading screen, releasing the previous location + // (or, for another campaign, the whole previous campaign). + void enterLocationThen(const std::string& campaignId, const std::string& locationId, + std::function then); + // Starts the campaign: its quests and the player. No location is loaded yet. + void loadCampaign(const std::string& campaignId); + void unloadCampaign(); + // Builds the campaign's location and restores its stored state, if any. + void loadLocation(const std::string& locationId); + // Releases the loaded location, keeping its state in storedLocationStates if asked. + void unloadLocation(bool storeState); + // Teleport zones: moves the player to another location of the campaign. + void teleportPlayer(const std::string& destName, const Eigen::Vector3f& destPos, float destRotY); // Builds one location from its config and wires the callbacks every location gets. std::shared_ptr createLocation(const LocationConfig& config); - // Wires the callbacks that span a campaign's locations (teleports, dialogue, HUD...). - void wireCampaignLocations(LocationMap& locations); - // Runs `then` with the campaign active. If its locations aren't loaded, they are - // built first behind the campaign's loading screen (one loading step per - // location) and any other loaded campaign is released. - void loadCampaignThen(const std::string& campaignId, std::function then); - // Releases every loaded campaign's locations and the textures only they used. - void unloadCampaigns(); - // Makes a loaded campaign the played one: its locations and its quest definitions. - void activateCampaign(const std::string& campaignId); + // Wires the location's callbacks into the game (teleports, dialogue, HUD...). + void wireLocation(Location* loc); const CampaignConfig& activeCampaign() const; void createShadowMapAndProparateToLocations(); void destroyShadowMapAndProparateToLocations(); // Reset entry point used by "Start New Game": loads the active campaign's start-state - // JSON into its existing Location objects, else falls back to performFullReset(). + // JSON (into the loaded location, storing the rest), else falls back to performFullReset(). // Must only ever run from a deferred task (see mainThreadHandler.EnqueueMainThreadTask), // never synchronously from a Lua/cutscene call stack. void performResetToInitialState(); - // Rebuilds the active campaign's locations from scratch. + // Rebuilds the loaded location from scratch and forgets the other locations' states. void performFullReset(); // Applies the campaign's tutorial flag and MenuManager::tutorialEnabledSetting to // the freshly reset state (new games only). void applyTutorialSetting(); void saveInitialState(); - // (Re-)arms one-shot gameplay callbacks (taxi request) that - // wireCampaignLocations() sets up but that get consumed (set to nullptr) once used - // during play. Must be called after gameState.load() on the fast reset path, - // since that path keeps the existing Location objects instead of recreating them. - void rearmOneShotCallbacks(LocationMap& locations); - int64_t getSyncTimeMs(); void processTickCount(); void drawScene(); diff --git a/src/GameState.cpp b/src/GameState.cpp index 6551a92..2f88eaf 100644 --- a/src/GameState.cpp +++ b/src/GameState.cpp @@ -61,6 +61,7 @@ void GameState::save(nlohmann::json& out) const out["tutorialPhoneChatScreenOpened"] = tutorialPhoneChatScreenOpened; out["tutorialJournalScreenOpened"] = tutorialJournalScreenOpened; out["tutorialNeedOpenTaxiScreen"] = tutorialNeedOpenTaxiScreen; + out["tutorialTaxiHintDone"] = tutorialTaxiHintDone; // --- Phone / chat --- out["chatUnread"] = { chatUnread[0], chatUnread[1], chatUnread[2] }; @@ -87,7 +88,9 @@ void GameState::save(nlohmann::json& out) const out["questJournal"] = std::move(questJson); // --- Locations --- + // Unloaded locations keep their last stored state; loaded ones are saved live. nlohmann::json locsJson = nlohmann::json::object(); + for (const auto& [name, stored] : storedLocationStates) locsJson[name] = stored; for (const auto& [name, loc] : locations) { if (loc) { nlohmann::json locJson; @@ -147,6 +150,7 @@ void GameState::load(const nlohmann::json& in) tutorialPhoneChatScreenOpened = in.value("tutorialPhoneChatScreenOpened", tutorialPhoneChatScreenOpened); tutorialJournalScreenOpened = in.value("tutorialJournalScreenOpened", tutorialJournalScreenOpened); tutorialNeedOpenTaxiScreen = in.value("tutorialNeedOpenTaxiScreen", tutorialNeedOpenTaxiScreen); + tutorialTaxiHintDone = in.value("tutorialTaxiHintDone", false); //logger() << "GameState::load step 6" << std::endl; @@ -190,9 +194,13 @@ void GameState::load(const nlohmann::json& in) logger() << "GameState::load step 10" << std::endl; // --- Locations --- + // Every location's state is stored (applied when the location loads); the ones + // already loaded get it right away. + storedLocationStates.clear(); if (in.contains("locations") && in["locations"].is_object()) { for (const auto& [name, locJson] : in["locations"].items()) { //logger() << "GameState::load step 10.1: " << name << std::endl; + storedLocationStates[name] = locJson; auto it = locations.find(name); if (it != locations.end() && it->second) { diff --git a/src/GameState.h b/src/GameState.h index c2e7cd0..b6a8c98 100644 --- a/src/GameState.h +++ b/src/GameState.h @@ -36,9 +36,13 @@ using LocationMap = std::unordered_map>; struct GameState : public ISaveable { // --- World --- - // Campaign being played (see CampaignRegistry); `locations` holds that campaign's locations. + // Campaign being played (see CampaignRegistry). std::string campaignId = "main"; + // The campaign's loaded location(s); only the one the player is in is kept loaded. LocationMap locations; + // Saved state (Location::save) of the campaign's locations that are not loaded, + // applied when one is loaded again. save()/load() cover both maps. + std::unordered_map storedLocationStates; std::string currentLocationName; bool isDarklands = false; bool isNight = false; @@ -63,6 +67,7 @@ struct GameState : public ISaveable { bool tutorialPhoneChatScreenOpened = false; // "open the messenger" hint is done bool tutorialJournalScreenOpened = false; // "open the journal" hint is done bool tutorialNeedOpenTaxiScreen = false; // "call a taxi" hint is showing + bool tutorialTaxiHintDone = false; // a taxi was ordered; never show the hint again bool isTutorialActive() const { return tutorialStep != TutorialStep::Completed; } // Moves to the next step if `step` is the current one; returns whether it did. diff --git a/src/Location.cpp b/src/Location.cpp index 268340c..5aa63aa 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -56,6 +56,18 @@ namespace FRG { } + Location::~Location() + { + // The player is shared with the next location and outlives this one: drop + // everything in him that points into this location: its path planner, the HP + // callback Game wires with this location's pointer, and a pending walk-to callback. + if (player) { + player->setPathPlanner(nullptr); + player->state.onHpChanged = nullptr; + player->state.onArrivedCallback = nullptr; + } + } + // ---- Pointer-to-index helpers ---- InteractiveObject* Location::getTargetInteractiveObject() @@ -101,53 +113,14 @@ namespace FRG interactiveObjects = GameObjectLoader::loadAndCreateInteractiveObjects(params.interactiveObjectsJsonPath, renderer, CONST_ZIP_FILE); SDL_PumpEvents(); - //auto playerTexture = std::make_shared(CreateTextureDataFromPng("resources/w/gg/IMG_20260413_182354_992.png", CONST_ZIP_FILE)); - //auto playerTexture = renderer.textureManager.LoadFromPng("resources/w/gg/UniV_Grid_2K_Base_color.png", CONST_ZIP_FILE); - auto playerTexture = renderer.textureManager.LoadFromPng("resources/w/gg/UniV_Grid_2K_Base_color001.png", CONST_ZIP_FILE); - auto sparkTexture = renderer.textureManager.LoadFromPng("resources/w/spark.png", CONST_ZIP_FILE); - player = std::make_unique(); - - player->loadBinaryAnimation(AnimationState::STAND, "resources/w/gg/new2/gg_stand_idle001_small.anim", CONST_ZIP_FILE); - player->loadBinaryAnimation(AnimationState::WALK, "resources/w/gg/new2/gg_walk001_small.anim", CONST_ZIP_FILE); - player->loadBinaryAnimation(AnimationState::STAND_TO_ACTION, "resources/w/gg/new2/gg_stand_to_action001_small.anim", CONST_ZIP_FILE); - - player->loadBinaryAnimation(AnimationState::ACTION_ATTACK, "resources/w/gg/new2/gg_action_chop001_small.anim", CONST_ZIP_FILE); - player->loadBinaryAnimation(AnimationState::ACTION_ATTACK_2, "resources/w/gg/new2/gg_action_stab001_small.anim", CONST_ZIP_FILE); - player->loadBinaryAnimation(AnimationState::ACTION_IDLE, "resources/w/gg/new2/gg_action_idle002_small.anim", CONST_ZIP_FILE); - player->loadBinaryAnimation(AnimationState::ACTION_TO_STAND, "resources/w/gg/new2/gg_action_to_stand001_small.anim", CONST_ZIP_FILE); - player->loadBinaryAnimation(AnimationState::ACTION_TO_DEATH, "resources/w/gg/new2/gg_die001_small.anim", CONST_ZIP_FILE); - player->loadBinaryAnimation(AnimationState::DEATH_IDLE, "resources/w/gg/new2/gg_die_idle001_small.anim", CONST_ZIP_FILE); - - - player->weaponTexture = renderer.textureManager.LoadFromPng("resources/w/gg/Props.png", CONST_ZIP_FILE); - player->weaponMesh.data = LoadModelFromBinFile("resources/w/gg/knife002.txt.bin", CONST_ZIP_FILE); - player->weaponMesh.data.Move(Vector3f(0, -0.3, 0)); - player->weaponMesh.data.Scale(0.2f); - player->weaponMesh.RefreshVBO(); - player->weaponAttachBoneName = "RightHand"; - player->weaponInitialRotation = Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitZ()).toRotationMatrix(); - player->weaponInitialPosition = Eigen::Vector3f(0, 0.09, 0.016); - - player->setTexture(playerTexture); - player->state.walkSpeed = 3.0f; - player->state.rotationSpeed = 8.0f; - - player->state.modelScale = 1.f; - player->state.modelCorrectionRotation = Eigen::Quaternionf( - Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY()) - * - Eigen::AngleAxisf(-M_PI*0.5, Eigen::Vector3f::UnitX()) - - ); - - player->state.canAttack = true; - player->state.isPlayer = true; + player = params.player ? params.player : createPlayer(renderer, audioPlayer); + // A shared player arrives from another location; until setupNavigation() below + // gives him this location's planner, he must not plan with anything else. + player->setPathPlanner(nullptr); player->state.position = params.playerPosition; player->setTarget(params.playerPosition); - player->setupHitSparks(sparkTexture); - player->audioPlayer = audioPlayer; logger() << "Load resurces step 9" << std::endl; SDL_PumpEvents(); @@ -229,6 +202,57 @@ namespace FRG } + std::shared_ptr Location::createPlayer(Renderer& renderer, AudioPlayerAsync* audioPlayer) + { + //auto playerTexture = std::make_shared(CreateTextureDataFromPng("resources/w/gg/IMG_20260413_182354_992.png", CONST_ZIP_FILE)); + //auto playerTexture = renderer.textureManager.LoadFromPng("resources/w/gg/UniV_Grid_2K_Base_color.png", CONST_ZIP_FILE); + auto playerTexture = renderer.textureManager.LoadFromPng("resources/w/gg/UniV_Grid_2K_Base_color001.png", CONST_ZIP_FILE); + + auto sparkTexture = renderer.textureManager.LoadFromPng("resources/w/spark.png", CONST_ZIP_FILE); + + auto player = std::make_shared(); + + player->loadBinaryAnimation(AnimationState::STAND, "resources/w/gg/new2/gg_stand_idle001_small.anim", CONST_ZIP_FILE); + player->loadBinaryAnimation(AnimationState::WALK, "resources/w/gg/new2/gg_walk001_small.anim", CONST_ZIP_FILE); + player->loadBinaryAnimation(AnimationState::STAND_TO_ACTION, "resources/w/gg/new2/gg_stand_to_action001_small.anim", CONST_ZIP_FILE); + + player->loadBinaryAnimation(AnimationState::ACTION_ATTACK, "resources/w/gg/new2/gg_action_chop001_small.anim", CONST_ZIP_FILE); + player->loadBinaryAnimation(AnimationState::ACTION_ATTACK_2, "resources/w/gg/new2/gg_action_stab001_small.anim", CONST_ZIP_FILE); + player->loadBinaryAnimation(AnimationState::ACTION_IDLE, "resources/w/gg/new2/gg_action_idle002_small.anim", CONST_ZIP_FILE); + player->loadBinaryAnimation(AnimationState::ACTION_TO_STAND, "resources/w/gg/new2/gg_action_to_stand001_small.anim", CONST_ZIP_FILE); + player->loadBinaryAnimation(AnimationState::ACTION_TO_DEATH, "resources/w/gg/new2/gg_die001_small.anim", CONST_ZIP_FILE); + player->loadBinaryAnimation(AnimationState::DEATH_IDLE, "resources/w/gg/new2/gg_die_idle001_small.anim", CONST_ZIP_FILE); + + + player->weaponTexture = renderer.textureManager.LoadFromPng("resources/w/gg/Props.png", CONST_ZIP_FILE); + player->weaponMesh.data = LoadModelFromBinFile("resources/w/gg/knife002.txt.bin", CONST_ZIP_FILE); + player->weaponMesh.data.Move(Vector3f(0, -0.3, 0)); + player->weaponMesh.data.Scale(0.2f); + player->weaponMesh.RefreshVBO(); + player->weaponAttachBoneName = "RightHand"; + player->weaponInitialRotation = Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitZ()).toRotationMatrix(); + player->weaponInitialPosition = Eigen::Vector3f(0, 0.09, 0.016); + + player->setTexture(playerTexture); + player->state.walkSpeed = 3.0f; + player->state.rotationSpeed = 8.0f; + + player->state.modelScale = 1.f; + player->state.modelCorrectionRotation = Eigen::Quaternionf( + Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY()) + * + Eigen::AngleAxisf(-M_PI*0.5, Eigen::Vector3f::UnitX()) + + ); + + player->state.canAttack = true; + player->state.isPlayer = true; + player->setupHitSparks(sparkTexture); + player->audioPlayer = audioPlayer; + + return player; + } + void Location::loadTeleportZones(const std::string& jsonPath, const char* zipFile) { if (jsonPath.empty()) return; diff --git a/src/Location.h b/src/Location.h index baa4643..a773adb 100644 --- a/src/Location.h +++ b/src/Location.h @@ -60,12 +60,17 @@ namespace FRG std::string triggerZonesJsonPath; std::string lightsJsonPath; Eigen::Vector3f playerPosition = Eigen::Vector3f::Zero(); + // Player shared by all locations of a campaign (see Location::createPlayer); + // setup() creates a private one when this is null. + std::shared_ptr player; }; class Location : public ISaveable { public: Location(Renderer& iRenderer, Inventory& iInventory); + // Detaches the (shared, longer-lived) player from this location. + ~Location(); // ---- All serialisable runtime state ---- LocationState state; @@ -74,9 +79,13 @@ namespace FRG std::vector interactiveObjects; - std::unique_ptr player; + // Shared: the same player moves from location to location and outlives them. + std::shared_ptr player; std::vector> npcs; + // Builds the player character (models, animations, weapon, textures). + static std::shared_ptr createPlayer(Renderer& renderer, AudioPlayerAsync* audioPlayer); + std::vector navigationMaps; PathFinder* navigation = nullptr; diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index c67112a..2f4242d 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -326,9 +326,14 @@ namespace FRG { loadSavedGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_load.json", zipFile); campaignScreenRoot = loadLocalizedUi("resources/w/ui/screen_campaign.json", zipFile); loadingScreenRoots_.clear(); + auto preloadLoadingScreen = [this, &zipFile](const std::string& path) { + if (!path.empty() && !loadingScreenRoots_.count(path)) + loadingScreenRoots_[path] = loadLocalizedUi(path, zipFile); + }; for (const CampaignConfig& campaign : CampaignRegistry::instance().all()) { - if (!loadingScreenRoots_.count(campaign.loadingScreenPath)) - loadingScreenRoots_[campaign.loadingScreenPath] = loadLocalizedUi(campaign.loadingScreenPath, zipFile); + preloadLoadingScreen(campaign.loadingScreenPath); + for (const LocationConfig& location : campaign.locations) + preloadLoadingScreen(location.loadingScreenPath); } saveGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_save.json", zipFile); } @@ -1535,6 +1540,7 @@ namespace FRG { }); uiManager.setButtonCallback("mapGo", [this](const std::string&) { gameState_.tutorialNeedOpenTaxiScreen = false; + gameState_.tutorialTaxiHintDone = true; if (gameState_.taxiIsCalled == false) { @@ -1693,7 +1699,7 @@ namespace FRG { void MenuManager::tutorialShowTaxiHint() { logger() << "tutorialShowTaxiHint" << std::endl; - if (!gameState_.isTutorialActive()) return; + if (!gameState_.isTutorialActive() || gameState_.tutorialTaxiHintDone) return; gameState_.tutorialNeedOpenTaxiScreen = true; // cleared when the taxi is ordered refreshHudVisibility(); } diff --git a/src/MenuManager.h b/src/MenuManager.h index 1afa482..3a354a0 100644 --- a/src/MenuManager.h +++ b/src/MenuManager.h @@ -71,8 +71,8 @@ namespace FRG { // Called by Game once a save has been applied. void enterGameplay(bool isFromLoad = false); - // Campaign loading screen (layout from CampaignConfig::loadingScreenPath, preloaded - // with the UI). Game shows it before building a campaign and hides it when done. + // Loading screen (a campaign's or a location's layout, all preloaded with the UI). + // Game shows it before loading a campaign or location and hides it when done. void showLoadingScreen(const std::string& layoutPath); void hideLoadingScreen(); bool isLoadingScreenShown() const { return loadingScreenShown_; } diff --git a/src/render/TextureManager.cpp b/src/render/TextureManager.cpp index 75bbd6d..5b05bb1 100644 --- a/src/render/TextureManager.cpp +++ b/src/render/TextureManager.cpp @@ -558,12 +558,11 @@ namespace FRG for (const auto& [key, tex] : textureMap) residentKeys.insert(key); } - void TextureManager::UnloadNonResident(const std::string& residentPrefix) + void TextureManager::UnloadUnused() { size_t unloaded = 0; for (auto it = textureMap.begin(); it != textureMap.end(); ) { - const bool keep = residentKeys.count(it->first) - || (!residentPrefix.empty() && it->first.compare(0, residentPrefix.size(), residentPrefix) == 0); + const bool keep = residentKeys.count(it->first) || it->second.use_count() > 1; if (keep) { ++it; } @@ -572,7 +571,7 @@ namespace FRG ++unloaded; } } - logger() << "Unloaded " << unloaded << " non-resident textures" << std::endl; + logger() << "Unloaded " << unloaded << " unused textures" << std::endl; } } \ No newline at end of file diff --git a/src/render/TextureManager.h b/src/render/TextureManager.h index 5b1ff23..322bb03 100644 --- a/src/render/TextureManager.h +++ b/src/render/TextureManager.h @@ -82,10 +82,9 @@ namespace FRG // Marks every texture cached so far as resident, so UnloadNonResident() keeps it. void MarkAllResident(); - // Drops cached textures that aren't resident and whose key doesn't start with - // residentPrefix (e.g. UI images, which are reloaded under the same prefix). A - // texture is freed once nothing else holds it. - void UnloadNonResident(const std::string& residentPrefix = ""); + // Frees cached textures nothing else uses any more (only the cache holds them), + // except resident ones. Call after releasing the objects that used them. + void UnloadUnused(); private: std::unordered_map> textureMap;