Major refactoring - internal server state machine implemented

This commit is contained in:
Vladislav Khorev committed 2026-10-29 00:09:16 +06:00
1 parent 71c654e5b6
commit 66e93d46f4
48 files changed
+3948 -3696

No files matched your search

+70 -44
View File
@@ -2,23 +2,10 @@
#include <ctime>
#include <iomanip>
#include <sstream>
#include <stdexcept>
namespace FRG {
// Saves made before the tutorial rework kept the dorm and the university tutorials
// as two state machines (tutorialStep 0..6 + uniIntTutorialState 0..4); map them
// onto the single TutorialStep sequence.
static TutorialStep migrateLegacyTutorialStep(int dormStep, int uniIntState)
{
if (dormStep <= 4) return static_cast<TutorialStep>(dormStep); // Step0..Step4 kept their values
switch (uniIntState) {
case 2: return TutorialStep::DarklandsHealth; // DarklandsActive
case 3: return TutorialStep::DarklandsAttack; // DarklandsStep13
case 4: return TutorialStep::Completed; // DarklandsFull
default: return TutorialStep::EnterDarklands; // Step10 / Step11
}
}
bool GameState::completeTutorialStep(TutorialStep step)
{
if (tutorialStep != step || step == TutorialStep::Completed) return false;
@@ -26,8 +13,29 @@ bool GameState::completeTutorialStep(TutorialStep step)
return true;
}
CharacterState* GameState::findCharacter(
const std::string& locationId, const std::string& characterId)
{
auto player = players.find(characterId);
if (player != players.end() && player->second) return player->second.get();
auto location = locations.find(locationId);
return location == locations.end()
? nullptr : location->second.findCharacter(characterId, players);
}
const CharacterState* GameState::findCharacter(
const std::string& locationId, const std::string& characterId) const
{
auto player = players.find(characterId);
if (player != players.end() && player->second) return player->second.get();
auto location = locations.find(locationId);
return location == locations.end()
? nullptr : location->second.findCharacter(characterId, players);
}
void GameState::save(nlohmann::json& out) const
{
out["schemaVersion"] = kSchemaVersion;
// Timestamp
std::time_t now = std::time(nullptr);
std::tm* tm = std::localtime(&now);
@@ -37,7 +45,7 @@ void GameState::save(nlohmann::json& out) const
// --- World ---
out["campaignId"] = campaignId;
out["currentLocationName"] = currentLocationName;
out["currentLocationId"] = currentLocationName;
out["isDarklands"] = isDarklands;
out["isNight"] = isNight;
out["isDawn"] = isDawn;
@@ -87,16 +95,21 @@ void GameState::save(nlohmann::json& out) const
questJournal.save(questJson);
out["questJournal"] = std::move(questJson);
// --- Players ---
out["players"] = nlohmann::json::object();
for (const auto& [id, player] : players) {
if (!player) continue;
nlohmann::json playerJson;
player->save(playerJson);
out["players"][id] = std::move(playerJson);
}
// --- 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;
loc->save(locJson);
locsJson[name] = std::move(locJson);
}
for (const auto& [name, location] : locations) {
nlohmann::json locJson;
location.save(locJson);
locsJson[name] = std::move(locJson);
}
out["locations"] = std::move(locsJson);
@@ -105,12 +118,16 @@ void GameState::save(nlohmann::json& out) const
void GameState::load(const nlohmann::json& in)
{
const int schemaVersion = in.value("schemaVersion", -1);
if (schemaVersion != kSchemaVersion)
throw std::runtime_error(
"Unsupported game-state schema version: " +
std::to_string(schemaVersion));
//logger() << "GameState::load step 1" << std::endl;
// --- World ---
// Literal default: saves made before campaigns existed belong to the main game.
campaignId = in.value("campaignId", std::string("main"));
currentLocationName = in.value("currentLocationName", currentLocationName);
campaignId = in.at("campaignId").get<std::string>();
currentLocationName = in.at("currentLocationId").get<std::string>();
isDarklands = in.value("isDarklands", isDarklands);
isNight = in.value("isNight", isNight);
isDawn = in.value("isDawn", isDawn);
@@ -142,11 +159,8 @@ void GameState::load(const nlohmann::json& in)
//logger() << "GameState::load step 5" << std::endl;
// --- Tutorial ---
if (in.contains("uniIntTutorialState")) {
tutorialStep = migrateLegacyTutorialStep(in.value("tutorialStep", 0), in["uniIntTutorialState"].get<int>());
} else {
tutorialStep = static_cast<TutorialStep>(in.value("tutorialStep", static_cast<int>(tutorialStep)));
}
tutorialStep = static_cast<TutorialStep>(
in.at("tutorialStep").get<int>());
tutorialPhoneChatScreenOpened = in.value("tutorialPhoneChatScreenOpened", tutorialPhoneChatScreenOpened);
tutorialJournalScreenOpened = in.value("tutorialJournalScreenOpened", tutorialJournalScreenOpened);
tutorialNeedOpenTaxiScreen = in.value("tutorialNeedOpenTaxiScreen", tutorialNeedOpenTaxiScreen);
@@ -193,20 +207,32 @@ 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;
// --- Players ---
const auto& playerValues = in.at("players");
if (!playerValues.is_object() || !playerValues.contains("player"))
throw std::runtime_error("Game state requires players.player");
for (const auto& [id, value] : playerValues.items()) {
auto& player = players[id];
if (!player) player = std::make_shared<CharacterState>();
player->load(value);
}
for (auto it = players.begin(); it != players.end();) {
if (!playerValues.contains(it->first)) it = players.erase(it);
else ++it;
}
auto it = locations.find(name);
if (it != locations.end() && it->second) {
it->second->load(locJson);
}
}
// --- Locations ---
const auto& locationValues = in.at("locations");
if (!locationValues.is_object() ||
!locationValues.contains(currentLocationName))
throw std::runtime_error(
"Game state does not contain current location '" +
currentLocationName + "'");
for (const auto& [id, value] : locationValues.items())
locations[id].load(value);
for (auto it = locations.begin(); it != locations.end();) {
if (!locationValues.contains(it->first)) it = locations.erase(it);
else ++it;
}
//logger() << "GameState::load step 11" << std::endl;