Files
Shiip-of-Hakinian-Espanol/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp
Malkierian 1e1a47c263 Load metadata without LoadFile() on startup (#5817)
* Load metadata without `LoadFile` on startup.
Implement file type entry.

* Fix non-Windows?

* clang
2025-11-13 19:05:51 -07:00

411 lines
16 KiB
C++

#include "spoiler_log.hpp"
#include "../dungeon.h"
#include "../static_data.h"
#include "../settings.h"
#include "../entrance.h"
#include "random.hpp"
#include "../trial.h"
#include "hints.hpp"
#include "pool_functions.hpp"
#include "soh/Enhancements/randomizer/randomizer_check_objects.h"
#include "soh/Enhancements/randomizer/randomizer_entrance_tracker.h"
#include <nlohmann/json.hpp>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <map>
#include <set>
#include <string>
#include <sstream>
#include <unordered_map>
#include <vector>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <variables.h>
#include <ship/Context.h>
#include <soh/OTRGlobals.h>
#include <libultraship/bridge/consolevariablebridge.h>
using json = nlohmann::ordered_json;
using namespace Rando;
json jsonData;
std::map<RandomizerHintTextKey, Rando::ItemLocation*> hintedLocations;
extern std::array<std::string, 17> hintCategoryNames;
extern Region* GetHintRegion(uint32_t);
namespace {
std::string placementtxt;
} // namespace
void GenerateHash() {
auto ctx = Rando::Context::GetInstance();
std::string hash = ctx->GetHash();
// adds leading 0s to the hash string if it has less than 10 digits.
while (hash.length() < 10) {
hash = "0" + hash;
}
for (size_t i = 0, j = 0; i < ctx->hashIconIndexes.size(); i++, j += 2) {
int number = std::stoi(hash.substr(j, 2));
ctx->hashIconIndexes[i] = number;
}
}
static auto GetGeneralPath() {
return "./randomizer/haha.xml";
}
static auto GetSpoilerLogPath() {
return GetGeneralPath();
}
static auto GetPlacementLogPath() {
return GetGeneralPath();
}
// Writes the location to the specified node.
static void WriteLocation(std::string sphere, const RandomizerCheck locationKey, const bool withPadding = false) {
Rando::Location* location = Rando::StaticData::GetLocation(locationKey);
Rando::ItemLocation* itemLocation = Rando::Context::GetInstance()->GetItemLocation(locationKey);
switch (gSaveContext.language) {
case LANGUAGE_ENG:
default:
jsonData["playthrough"][sphere][location->GetName()] = itemLocation->GetPlacedItemName().GetEnglish();
break;
case LANGUAGE_GER:
jsonData["playthrough"][sphere][location->GetName()] = itemLocation->GetPlacedItemName().GetGerman();
break;
case LANGUAGE_FRA:
jsonData["playthrough"][sphere][location->GetName()] = itemLocation->GetPlacedItemName().GetFrench();
break;
}
}
// Writes a shuffled entrance to the specified node
static void WriteShuffledEntrance(std::string sphereString, Entrance* entrance) {
int16_t originalIndex = entrance->GetIndex();
int16_t destinationIndex = -1;
int16_t replacementIndex = entrance->GetReplacement()->GetIndex();
int16_t replacementDestinationIndex = -1;
std::string name = GetEntranceData(originalIndex)->source;
std::string text = GetEntranceData(replacementIndex)->destination;
// Track the reverse destination, useful for savewarp handling
if (entrance->GetReverse() != nullptr) {
destinationIndex = entrance->GetReverse()->GetIndex();
// When decouple is off we track the replacement's reverse destination, useful for recording visited entrances
if (!entrance->IsDecoupled()) {
replacementDestinationIndex = entrance->GetReplacement()->GetReverse()->GetIndex();
}
}
json entranceJson = json::object({
{ "type", entrance->GetType() },
{ "index", originalIndex },
{ "destination", destinationIndex },
{ "override", replacementIndex },
{ "overrideDestination", replacementDestinationIndex },
});
jsonData["entrances"].push_back(entranceJson);
// When decoupled entrances is off, handle saving reverse entrances
if (entrance->GetReverse() != nullptr && !entrance->IsDecoupled()) {
json reverseEntranceJson = json::object({
{ "type", entrance->GetReverse()->GetType() },
{ "index", replacementDestinationIndex },
{ "destination", replacementIndex },
{ "override", destinationIndex },
{ "overrideDestination", originalIndex },
});
jsonData["entrances"].push_back(reverseEntranceJson);
}
switch (gSaveContext.language) {
case LANGUAGE_ENG:
case LANGUAGE_GER:
case LANGUAGE_FRA:
default:
jsonData["entrancesMap"][sphereString][name] = text;
break;
}
}
// Writes the settings (without excluded locations, starting inventory and tricks) to the spoilerLog document.
static void WriteSettings() {
auto ctx = Rando::Context::GetInstance();
std::array<Rando::Option, RSK_MAX> options = Rando::Settings::GetInstance()->GetAllOptions();
for (const Rando::Option& option : options) {
if (option.GetName() != "") {
jsonData["settings"][option.GetName()] = option.GetOptionText(ctx->GetOption(option.GetKey()).Get());
}
}
}
// Removes any line breaks from s.
std::string RemoveLineBreaks(std::string s) {
s.erase(std::remove(s.begin(), s.end(), '\n'), s.end());
return s;
}
// Writes the excluded locations to the spoiler log, if there are any.
static void WriteExcludedLocations() {
auto ctx = Rando::Context::GetInstance();
for (size_t i = 1; i < Rando::Settings::GetInstance()->GetExcludeLocationsOptions().size(); i++) {
for (const auto& location : Rando::Settings::GetInstance()->GetExcludeLocationsOptions()[i]) {
if (ctx->GetLocationOption(static_cast<RandomizerCheck>(location->GetKey())).Get() == RO_LOCATION_INCLUDE) {
continue;
}
jsonData["excludedLocations"].push_back(RemoveLineBreaks(location->GetName()));
}
}
}
// Writes the starting inventory to the spoiler log, if there is any.
static void WriteStartingInventory() {
auto ctx = Rando::Context::GetInstance();
const Rando::OptionGroup& optionGroup = Rando::Settings::GetInstance()->GetOptionGroup(RSG_STARTING_INVENTORY);
for (const Rando::OptionGroup* subGroup : optionGroup.GetSubGroups()) {
if (subGroup->GetContainsType() == Rando::OptionGroupType::DEFAULT) {
for (Rando::Option* option : subGroup->GetOptions()) {
jsonData["settings"][option->GetName()] = option->GetOptionText(ctx->GetOption(option->GetKey()).Get());
}
}
}
}
// Writes the enabled tricks to the spoiler log, if there are any.
static void WriteEnabledTricks() {
auto ctx = Rando::Context::GetInstance();
for (const auto& setting : Rando::Settings::GetInstance()->GetOptionGroup(RSG_TRICKS).GetOptions()) {
if (ctx->GetTrickOption(static_cast<RandomizerTrick>(setting->GetKey())).IsNot(RO_GENERIC_ON)) {
continue;
}
jsonData["enabledTricks"].push_back(RemoveLineBreaks(setting->GetName()).c_str());
}
}
// Writes the Master Quest dungeons to the spoiler log, if there are any.
static void WriteMasterQuestDungeons() {
auto ctx = Rando::Context::GetInstance();
for (const auto* dungeon : ctx->GetDungeons()->GetDungeonList()) {
std::string dungeonName;
if (dungeon->IsVanilla()) {
continue;
}
jsonData["masterQuestDungeons"].push_back(dungeon->GetName());
}
}
// Writes the required trials to the spoiler log, if there are any.
static void WriteChosenOptions() {
auto ctx = Rando::Context::GetInstance();
for (const auto& trial : ctx->GetTrials()->GetTrialList()) {
if (trial->IsRequired()) {
std::string trialName = trial->GetName().GetForCurrentLanguage(MF_CLEAN);
jsonData["requiredTrials"].push_back(RemoveLineBreaks(trialName));
}
}
if (ctx->GetOption(RSK_SELECTED_STARTING_AGE).Is(RO_AGE_ADULT)) {
jsonData["SelectedStartingAge"] = "Adult";
} else {
jsonData["SelectedStartingAge"] = "Child";
}
}
// Writes the intended playthrough to the spoiler log, separated into spheres.
static void WritePlaythrough() {
auto ctx = Rando::Context::GetInstance();
for (uint32_t i = 0; i < ctx->playthroughLocations.size(); ++i) {
auto sphereNum = std::to_string(i);
std::string sphereString = "sphere ";
if (i < 10)
sphereString += "0";
sphereString += sphereNum;
for (const RandomizerCheck key : ctx->playthroughLocations[i]) {
WriteLocation(sphereString, key, true);
}
}
}
// Write the randomized entrance playthrough to the spoiler log, if applicable
static void WriteShuffledEntrances() {
auto ctx = Rando::Context::GetInstance();
for (uint32_t i = 0; i < ctx->GetEntranceShuffler()->playthroughEntrances.size(); ++i) {
auto sphereNum = std::to_string(i);
std::string sphereString = "sphere ";
if (i < 10)
sphereString += "0";
sphereString += sphereNum;
for (Entrance* entrance : ctx->GetEntranceShuffler()->playthroughEntrances[i]) {
WriteShuffledEntrance(sphereString, entrance);
}
}
}
Rando::ItemLocation* GetItemLocation(RandomizerGet item) {
auto ctx = Rando::Context::GetInstance();
return ctx->GetItemLocation(FilterFromPool(ctx->allLocations, [item, ctx](const RandomizerCheck loc) {
return ctx->GetItemLocation(loc)->GetPlacedRandomizerGet() == item;
})[0]);
}
static void WriteAllLocations() {
auto ctx = Rando::Context::GetInstance();
for (const RandomizerCheck key : ctx->allLocations) {
Rando::ItemLocation* location = ctx->GetItemLocation(key);
std::string placedItemName;
switch (gSaveContext.language) {
case 0:
default:
placedItemName = location->GetPlacedItemName().GetEnglish();
break;
case 1:
placedItemName = location->GetPlacedItemName().GetGerman();
break;
case 2:
placedItemName = location->GetPlacedItemName().GetFrench();
break;
}
// If it's a simple item (not an ice trap, doesn't have a price)
// just add the name of the item and move on
if (!location->HasCustomPrice() && location->GetPlacedRandomizerGet() != RG_ICE_TRAP) {
jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()] =
placedItemName;
continue;
}
// We're dealing with a complex item, build out the json object for it
jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()]["item"] =
placedItemName;
if (location->HasCustomPrice()) {
jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()]["price"] =
location->GetPrice();
}
if (location->IsAHintAccessible()) {
hintedLocations.emplace(Rando::StaticData::GetLocation(key)->GetHintKey(), location);
}
if (location->GetPlacedRandomizerGet() == RG_ICE_TRAP) {
switch (gSaveContext.language) {
case 0:
default:
jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()]
["model"] = Rando::StaticData::RetrieveItem(
ctx->overrides[location->GetRandomizerCheck()].LooksLike())
.GetName()
.GetEnglish();
jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()]
["trickName"] = ctx->overrides[location->GetRandomizerCheck()].GetTrickName().GetEnglish();
break;
case 1:
jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()]
["model"] = Rando::StaticData::RetrieveItem(
ctx->overrides[location->GetRandomizerCheck()].LooksLike())
.GetName()
.GetGerman();
jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()]
["trickName"] = ctx->overrides[location->GetRandomizerCheck()].GetTrickName().GetGerman();
break;
case 2:
jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()]
["model"] = Rando::StaticData::RetrieveItem(
ctx->overrides[location->GetRandomizerCheck()].LooksLike())
.GetName()
.GetFrench();
jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()]
["trickName"] = ctx->overrides[location->GetRandomizerCheck()].GetTrickName().GetFrench();
break;
}
}
}
}
const char* SpoilerLog_Write() {
auto ctx = Rando::Context::GetInstance();
jsonData.clear();
jsonData["version"] = (char*)gBuildVersion;
jsonData["fileType"] = FILE_TYPE_SPOILER;
jsonData["git_branch"] = (char*)gGitBranch;
jsonData["git_commit"] = (char*)gGitCommitHash;
jsonData["seed"] = ctx->GetSeedString();
jsonData["finalSeed"] = ctx->GetSeed();
// Write Hash
int index = 0;
for (uint8_t seed_value : ctx->hashIconIndexes) {
jsonData["file_hash"][index] = seed_value;
index++;
}
WriteSettings();
WriteExcludedLocations();
WriteStartingInventory();
WriteEnabledTricks();
WriteMasterQuestDungeons();
WriteChosenOptions();
WritePlaythrough();
ctx->playthroughLocations.clear();
ctx->playthroughBeatable = false;
ctx->WriteHintJson(jsonData);
WriteShuffledEntrances();
WriteAllLocations();
if (!std::filesystem::exists(Ship::Context::GetPathRelativeToAppDirectory("Randomizer"))) {
std::filesystem::create_directory(Ship::Context::GetPathRelativeToAppDirectory("Randomizer"));
}
std::string jsonString = jsonData.dump(4);
std::ostringstream fileNameStream;
for (uint8_t i = 0; i < ctx->hashIconIndexes.size(); i++) {
if (i) {
fileNameStream << '-';
}
if (ctx->hashIconIndexes[i] < 10) {
fileNameStream << '0';
}
fileNameStream << std::to_string(ctx->hashIconIndexes[i]);
}
std::string fileName = fileNameStream.str();
std::ofstream jsonFile(Ship::Context::GetPathRelativeToAppDirectory(
(std::string("Randomizer/") + fileName + std::string(".json")).c_str()));
jsonFile << std::setw(4) << jsonString << std::endl;
jsonFile.close();
CVarSetString(CVAR_GENERAL("SpoilerLog"), (std::string("./Randomizer/") + fileName + std::string(".json")).c_str());
// Note: probably shouldn't return this without making sure this string is stored somewhere, but
// this return value is currently only used in playthrough.cpp as a true/false. Even if the pointer
// is no longer valid it would still not be nullptr if the spoilerfile was written, so it works but
// should probably be changed for correctness later on.
return fileName.c_str();
}
void PlacementLog_Msg(std::string_view msg) {
placementtxt += msg;
}
void PlacementLog_Clear() {
placementtxt = "";
}