#include "SaveGame.h" #include #include #include namespace { std::vector Split(const std::string& text, char separator) { std::vector parts; std::stringstream stream(text); std::string part; while (std::getline(stream, part, separator)) { parts.push_back(part); } return parts; } std::string Join(const std::vector& values, char separator) { std::ostringstream out; for (std::size_t i = 0; i < values.size(); ++i) { if (i > 0) { out << separator; } out << values[i]; } return out.str(); } std::string ValueAfterEquals(const std::string& line) { const std::size_t pos = line.find('='); return pos == std::string::npos ? std::string{} : line.substr(pos + 1); } bool StartsWith(const std::string& text, const std::string& prefix) { return text.rfind(prefix, 0) == 0; } void WritePet(std::ofstream& out, const std::string& key, const Pet& pet) { out << key << "=" << pet.name << "|" << pet.maxHp << "|" << pet.hp << "|" << pet.attack << "|" << pet.level << "|" << pet.exp << "|" << Join(pet.learnedSkillIds, ',') << "|" << pet.potential.hp << "|" << pet.potential.attack << "\n"; } std::optional ReadPet(const std::vector& parts) { if (parts.size() != 4 && parts.size() != 6 && parts.size() != 7 && parts.size() != 9) { return std::nullopt; } Pet pet = MakePet(parts[0], std::stoi(parts[1]), std::stoi(parts[3])); pet.hp = std::stoi(parts[2]); if (parts.size() >= 6) { pet.level = std::stoi(parts[4]); pet.exp = std::stoi(parts[5]); } if (parts.size() >= 7) { pet.learnedSkillIds = Split(parts[6], ','); if (pet.learnedSkillIds.size() == 1 && pet.learnedSkillIds.front().empty()) { pet.learnedSkillIds.clear(); } } if (parts.size() == 9) { pet.potential.hp = std::stoi(parts[7]); pet.potential.attack = std::stoi(parts[8]); } NormalizePetAfterLoad(pet); return pet; } QuestObjectiveProgress* FindObjective(QuestRuntime& runtime, const std::string& id) { const auto it = std::find_if(runtime.objectives.begin(), runtime.objectives.end(), [&](QuestObjectiveProgress& objective) { return objective.id == id; }); return it == runtime.objectives.end() ? nullptr : &*it; } std::string NewObjectiveIdForLegacyObjective(const std::string& legacyId) { static const std::map migrated = { {"arrive_tulimshar", "visit_tulimshar"}, {"capture_first", "catch_first_pet"}, {"reach_sandstorm", "visit_sandstorm"}, {"enter_mines", "visit_desert_mines"}, {"collect_three", "collect_first_pet_partner"}, {"collect_three_pets", "collect_first_pet_partner"}, {"snake_pit", "visit_snake_pit"}, }; const auto it = migrated.find(legacyId); return it == migrated.end() ? legacyId : it->second; } void ApplyLoadedQuestObjective(QuestRuntime& runtime, const std::string& id, int current, bool completed) { QuestObjectiveProgress* objective = FindObjective(runtime, NewObjectiveIdForLegacyObjective(id)); if (!objective) { return; } objective->current = std::max(objective->current, current); objective->completed = objective->completed || completed || objective->current >= objective->required; } } // namespace bool SaveGameToFile(const std::filesystem::path& path, const SaveState& state) { std::ofstream out(path); if (!out) { return false; } out << "MANA_SAVE_V1\n"; out << "map=" << state.mapName << "\n"; out << "player=" << state.playerX << "," << state.playerY << "\n"; out << "gold=" << state.gold << "\n"; out << "discovered=" << Join(state.discoveredMaps, '|') << "\n"; out << "collected=" << Join(state.collectedObjects, '|') << "\n"; out << "executed_dialogue_effects=" << Join(state.executedDialogueEffects, '|') << "\n"; out << "quest_states=" << state.questRuntime.states.RawStates().size() << "\n"; for (const auto& [questName, questState] : state.questRuntime.states.RawStates()) { out << "quest_state=" << questName << "|" << questState << "\n"; } const auto validMonsterRespawns = [&]() { std::vector valid; for (const MonsterRespawnSave& respawn : state.monsterRespawns) { if (!respawn.objectKey.empty() && respawn.remainingSeconds > 0.0) { valid.push_back(respawn); } } return valid; }(); out << "monster_respawns=" << validMonsterRespawns.size() << "\n"; for (const MonsterRespawnSave& respawn : validMonsterRespawns) { out << "monster_respawn=" << respawn.objectKey << "|" << respawn.remainingSeconds << "\n"; } out << "items=" << state.inventory.items.size() << "\n"; for (const InventoryItem& item : state.inventory.items) { out << "item=" << item.name << "|" << item.count << "\n"; } out << "pets=" << state.team.pets.size() << "\n"; for (const Pet& pet : state.team.pets) { WritePet(out, "pet", pet); } out << "stored_pets=" << state.team.storage.size() << "\n"; for (const Pet& pet : state.team.storage) { WritePet(out, "stored_pet", pet); } out << "pet_journal=" << state.petJournal.entries.size() << "\n"; for (const PetJournalEntry& entry : state.petJournal.entries) { if (!entry.speciesName.empty()) { out << "pet_journal_entry=" << entry.speciesName << "|" << entry.seenCount << "|" << entry.caughtCount << "\n"; } } out << "quest_objectives=" << state.questRuntime.objectives.size() << "\n"; for (const QuestObjectiveProgress& objective : state.questRuntime.objectives) { out << "quest_objective=" << objective.id << "|" << objective.current << "|" << (objective.completed ? "1" : "0") << "\n"; } return static_cast(out); } std::optional LoadGameFromFile(const std::filesystem::path& path) { std::ifstream in(path); if (!in) { return std::nullopt; } std::string line; if (!std::getline(in, line) || line != "MANA_SAVE_V1") { return std::nullopt; } SaveState state; state.questRuntime = MakeInitialTonoriQuestRuntime(); while (std::getline(in, line)) { if (StartsWith(line, "map=")) { state.mapName = ValueAfterEquals(line); } else if (StartsWith(line, "player=")) { const std::vector parts = Split(ValueAfterEquals(line), ','); if (parts.size() == 2) { state.playerX = std::stof(parts[0]); state.playerY = std::stof(parts[1]); } } else if (StartsWith(line, "gold=")) { state.gold = std::max(0, std::stoi(ValueAfterEquals(line))); } else if (StartsWith(line, "discovered=")) { state.discoveredMaps = Split(ValueAfterEquals(line), '|'); if (state.discoveredMaps.size() == 1 && state.discoveredMaps.front().empty()) { state.discoveredMaps.clear(); } } else if (StartsWith(line, "collected=")) { state.collectedObjects = Split(ValueAfterEquals(line), '|'); if (state.collectedObjects.size() == 1 && state.collectedObjects.front().empty()) { state.collectedObjects.clear(); } } else if (StartsWith(line, "executed_dialogue_effects=")) { state.executedDialogueEffects = Split(ValueAfterEquals(line), '|'); if (state.executedDialogueEffects.size() == 1 && state.executedDialogueEffects.front().empty()) { state.executedDialogueEffects.clear(); } } else if (StartsWith(line, "quest_state=") || StartsWith(line, "script_quest=")) { const std::vector parts = Split(ValueAfterEquals(line), '|'); if (parts.size() == 2 && !parts[0].empty()) { state.questRuntime.states.Set(parts[0], parts[1]); } } else if (StartsWith(line, "monster_respawn=")) { const std::vector parts = Split(ValueAfterEquals(line), '|'); if (parts.size() == 2 && !parts[0].empty()) { const double remaining = std::stod(parts[1]); if (remaining > 0.0) { state.monsterRespawns.push_back({parts[0], remaining}); } } } else if (StartsWith(line, "item=")) { const std::vector parts = Split(ValueAfterEquals(line), '|'); if (parts.size() == 2) { AddItem(state.inventory, parts[0], std::stoi(parts[1])); } } else if (StartsWith(line, "pet=")) { const std::vector parts = Split(ValueAfterEquals(line), '|'); const std::optional pet = ReadPet(parts); if (pet.has_value()) { state.team.pets.push_back(*pet); } } else if (StartsWith(line, "stored_pet=")) { const std::vector parts = Split(ValueAfterEquals(line), '|'); const std::optional pet = ReadPet(parts); if (pet.has_value()) { state.team.storage.push_back(*pet); } } else if (StartsWith(line, "pet_journal_entry=")) { const std::vector parts = Split(ValueAfterEquals(line), '|'); if (parts.size() == 3 && !parts[0].empty()) { state.petJournal.entries.push_back({parts[0], std::stoi(parts[1]), std::stoi(parts[2])}); } } else if (StartsWith(line, "quest_objective=")) { const std::vector parts = Split(ValueAfterEquals(line), '|'); if (parts.size() == 3 && !parts[0].empty()) { ApplyLoadedQuestObjective(state.questRuntime, parts[0], std::stoi(parts[1]), parts[2] == "1"); } } else if (StartsWith(line, "quest=")) { const std::vector parts = Split(ValueAfterEquals(line), '|'); if (parts.size() == 2 && !parts[0].empty()) { ApplyLoadedQuestObjective(state.questRuntime, parts[0], parts[1] == "1" ? 1 : 0, parts[1] == "1"); } } } if (state.mapName.empty()) { return std::nullopt; } return state; }