working on refactoring

This commit is contained in:
Vladislav Khorev committed 2026-10-24 22:37:20 +06:00
1 parent 9b71ee447a
commit d5ef126264
12 files changed
+309 -219

No files matched your search

+149 -139
View File
@@ -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<void()> 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<void()> 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<std::function<void()>> 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<Location> 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<std::string> 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<Location>(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<float>(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);