最终整理版
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user