263 lines
10 KiB
C++
263 lines
10 KiB
C++
#include "SaveGame.h"
|
|
|
|
#include <algorithm>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
|
|
namespace {
|
|
|
|
std::vector<std::string> Split(const std::string& text, char separator)
|
|
{
|
|
std::vector<std::string> 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<std::string>& 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<Pet> ReadPet(const std::vector<std::string>& 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<std::string, std::string> 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<MonsterRespawnSave> 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<bool>(out);
|
|
}
|
|
|
|
std::optional<SaveState> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> parts = Split(ValueAfterEquals(line), '|');
|
|
const std::optional<Pet> pet = ReadPet(parts);
|
|
if (pet.has_value()) {
|
|
state.team.pets.push_back(*pet);
|
|
}
|
|
} else if (StartsWith(line, "stored_pet=")) {
|
|
const std::vector<std::string> parts = Split(ValueAfterEquals(line), '|');
|
|
const std::optional<Pet> pet = ReadPet(parts);
|
|
if (pet.has_value()) {
|
|
state.team.storage.push_back(*pet);
|
|
}
|
|
} else if (StartsWith(line, "pet_journal_entry=")) {
|
|
const std::vector<std::string> 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<std::string> 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<std::string> 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;
|
|
}
|