Skip to content
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9397b10
iAdd split ocarina songs mode with part-based progression.
RaccoonCloud Apr 24, 2026
5feddec
iUpdate split songs file headers with creator attribution.
RaccoonCloud Apr 24, 2026
2bd2ea4
Merge develop into feature/split-songs and resolve conflicts
RaccoonCloud Apr 24, 2026
5417774
Apply clang-format to split songs changes
RaccoonCloud Apr 24, 2026
3d0a0f6

RaccoonCloud Apr 26, 2026
0386051
Update split songs option text for progressive behavior.
RaccoonCloud Apr 26, 2026
6d49e4d
Randomizer: pickup textbox icons and progressive split songs
RaccoonCloud May 10, 2026
042e035
Fix clang-format in ItemMessages.cpp
RaccoonCloud May 10, 2026
6a10537
Randomizer: all song textbox icons work for progressive pickup
RaccoonCloud May 17, 2026
bc2027e
Simplify split songs to progressive-only RandInf tracking
RaccoonCloud Jun 4, 2026
72c849f
Address split songs PR review feedback
RaccoonCloud Jun 4, 2026
bd13ce5
Merge upstream develop into feature/split-songs
RaccoonCloud Jun 4, 2026
11a4287
Fix ItemMessages build after develop merge
RaccoonCloud Jun 4, 2026
f8464be
Bump libultraship to match upstream develop after merge
RaccoonCloud Jun 8, 2026
10f462d
Fix split song tracker texture lookup for Fast3dGui API
RaccoonCloud Jun 8, 2026
2340e5a
Fix seed generation crash when regenerating with split songs
RaccoonCloud Jun 8, 2026
3e1aff7
Fix split song part-1 grant and restore key ring icons
RaccoonCloud Jun 8, 2026
2998034
Simplify split songs to RandInf + progressive-only pattern.
RaccoonCloud Jun 9, 2026
600b8d3
Address split songs review: settings, pool, tracker, traps.
RaccoonCloud Jun 9, 2026
39427cc
Fix seed gen crash on progressive songs and clean up split songs menu…
RaccoonCloud Jun 9, 2026
b78aac9
Fix split song pickup icons and stabilize seed/save generation
RaccoonCloud Jun 15, 2026
79b6882
Fix Linux CI: use SaveData string overload for UTF-8 sanitization.
RaccoonCloud Jun 15, 2026
1198703
Merge upstream develop into feature/split-songs
RaccoonCloud Jun 15, 2026
7d146a6
Split PR scope per review: drop icon and UTF-8 side fixes.
RaccoonCloud Jun 18, 2026
2edee8d
Refactor split songs to PART items and static songData maps per review.
RaccoonCloud Jun 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 32 additions & 36 deletions soh/soh/Enhancements/randomizer/3drando/item_pool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -252,42 +252,38 @@ void GenerateItemPool() {
// add extra songs only if song shuffle is anywhere
if (ctx->GetOption(RSK_SHUFFLE_SONGS).IsNot(RO_SONG_SHUFFLE_OFF)) {
bool songAnywhere = ctx->GetOption(RSK_SHUFFLE_SONGS).Is(RO_SONG_SHUFFLE_ANYWHERE);
if (!ctx->GetOption(RSK_STARTING_ZELDAS_LULLABY).Get()) {
AddItemToPool(RG_ZELDAS_LULLABY, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere);
}
if (!ctx->GetOption(RSK_STARTING_EPONAS_SONG).Get()) {
AddItemToPool(RG_EPONAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere);
}
if (!ctx->GetOption(RSK_STARTING_SARIAS_SONG).Get()) {
AddItemToPool(RG_SARIAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere);
}
if (!ctx->GetOption(RSK_STARTING_SUNS_SONG).Get()) {
AddItemToPool(RG_SUNS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere);
}
if (!ctx->GetOption(RSK_STARTING_SONG_OF_TIME).Get()) {
AddItemToPool(RG_SONG_OF_TIME, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere);
}
if (!ctx->GetOption(RSK_STARTING_SONG_OF_STORMS).Get()) {
AddItemToPool(RG_SONG_OF_STORMS, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere);
}
if (!ctx->GetOption(RSK_STARTING_MINUET_OF_FOREST).Get()) {
AddItemToPool(RG_MINUET_OF_FOREST, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere);
}
if (!ctx->GetOption(RSK_STARTING_BOLERO_OF_FIRE).Get()) {
AddItemToPool(RG_BOLERO_OF_FIRE, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere);
}
if (!ctx->GetOption(RSK_STARTING_SERENADE_OF_WATER).Get()) {
AddItemToPool(RG_SERENADE_OF_WATER, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere);
}
if (!ctx->GetOption(RSK_STARTING_REQUIEM_OF_SPIRIT).Get()) {
AddItemToPool(RG_REQUIEM_OF_SPIRIT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere);
}
if (!ctx->GetOption(RSK_STARTING_NOCTURNE_OF_SHADOW).Get()) {
AddItemToPool(RG_NOCTURNE_OF_SHADOW, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere);
}
if (!ctx->GetOption(RSK_STARTING_PRELUDE_OF_LIGHT).Get()) {
AddItemToPool(RG_PRELUDE_OF_LIGHT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere);
}
const bool split = ctx->GetOption(RSK_SPLIT_OCARINA_SONGS).Get();
const int defaultPlentiful = songAnywhere ? 2 : 1;
auto addShuffledSong = [&](RandomizerGet fullSong, RandomizerGet progressive, bool hasStarting) {
if (hasStarting) {
return;
}
if (!split) {
AddItemToPool(fullSong, defaultPlentiful, 1, 1, 1, songAnywhere);
} else {
AddItemToPool(progressive, 3, 2, 2, 2, songAnywhere);
}
};
addShuffledSong(RG_ZELDAS_LULLABY, RG_PROGRESSIVE_ZELDAS_LULLABY,
ctx->GetOption(RSK_STARTING_ZELDAS_LULLABY).Get());
addShuffledSong(RG_EPONAS_SONG, RG_PROGRESSIVE_EPONAS_SONG, ctx->GetOption(RSK_STARTING_EPONAS_SONG).Get());
addShuffledSong(RG_SARIAS_SONG, RG_PROGRESSIVE_SARIAS_SONG, ctx->GetOption(RSK_STARTING_SARIAS_SONG).Get());
addShuffledSong(RG_SUNS_SONG, RG_PROGRESSIVE_SUNS_SONG, ctx->GetOption(RSK_STARTING_SUNS_SONG).Get());
addShuffledSong(RG_SONG_OF_TIME, RG_PROGRESSIVE_SONG_OF_TIME, ctx->GetOption(RSK_STARTING_SONG_OF_TIME).Get());
addShuffledSong(RG_SONG_OF_STORMS, RG_PROGRESSIVE_SONG_OF_STORMS,
ctx->GetOption(RSK_STARTING_SONG_OF_STORMS).Get());
addShuffledSong(RG_MINUET_OF_FOREST, RG_PROGRESSIVE_MINUET_OF_FOREST,
ctx->GetOption(RSK_STARTING_MINUET_OF_FOREST).Get());
addShuffledSong(RG_BOLERO_OF_FIRE, RG_PROGRESSIVE_BOLERO_OF_FIRE,
ctx->GetOption(RSK_STARTING_BOLERO_OF_FIRE).Get());
addShuffledSong(RG_SERENADE_OF_WATER, RG_PROGRESSIVE_SERENADE_OF_WATER,
ctx->GetOption(RSK_STARTING_SERENADE_OF_WATER).Get());
addShuffledSong(RG_REQUIEM_OF_SPIRIT, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT,
ctx->GetOption(RSK_STARTING_REQUIEM_OF_SPIRIT).Get());
addShuffledSong(RG_NOCTURNE_OF_SHADOW, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW,
ctx->GetOption(RSK_STARTING_NOCTURNE_OF_SHADOW).Get());
addShuffledSong(RG_PRELUDE_OF_LIGHT, RG_PROGRESSIVE_PRELUDE_OF_LIGHT,
ctx->GetOption(RSK_STARTING_PRELUDE_OF_LIGHT).Get());
} else {
ctx->PlaceItemInLocation(RC_SHEIK_IN_FOREST, RG_MINUET_OF_FOREST, false, true);
ctx->PlaceItemInLocation(RC_SHEIK_IN_CRATER, RG_BOLERO_OF_FIRE, false, true);
Expand Down
4 changes: 4 additions & 0 deletions soh/soh/Enhancements/randomizer/3drando/playthrough.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ int Playthrough_Init(uint32_t seed, std::set<RandomizerCheck> excludedLocations,
Random_Init(seed);

auto ctx = Rando::Context::GetInstance();
ctx->SetSeedGenerated(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why you added these?

@RaccoonCloud RaccoonCloud Jun 17, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you again for your help and patience with me (first timer — I kept messaging Cal).

Every time I ran a new seed or generated one, SoH was crashing because other things (item tracker, spoiler lookups) could read placement data while Fill() was still mid-reset — like a “seed ready” race.

I SetSeedGenerated(false) at the top of Playthrough_Init and only flip to true after SpoilerLog_Write() finishes, so nothing treats the seed as valid until the spoiler JSON is fully written. Before that, things were trying to use spoiler data that wasn’t ready yet and it’d just crash on generate. If there’s a better way I’ll relook and sort.

This showed up most with split songs because the tracker hits song progress during seed generation, but the flag lifecycle fix is general — it just enforces generate → write → consume order.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebuilt and tested split songs still works (first pickup = part / 1/2 on tracker, second = full song).
Pulled icon hooks and UTF-8 stuff into separate branches like you asked.
Also removed the logic scratch workaround progressive songs go through normal ApplyItemEffect now

ctx->overrides.clear();
ctx->ItemReset();
ctx->HintReset();
Expand Down Expand Up @@ -81,6 +82,9 @@ int Playthrough_Init(uint32_t seed, std::set<RandomizerCheck> excludedLocations,
SPDLOG_INFO("Writing Spoiler Log Done");
}

// Seed is only considered ready after spoiler JSON is fully written.
ctx->SetSeedGenerated(true);

ctx->playthroughLocations.clear();
ctx->playthroughBeatable = false;

Expand Down
166 changes: 93 additions & 73 deletions soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@

#include <ship/Context.h>
#include <soh/OTRGlobals.h>
#include <soh/util.h>

#include <libultraship/bridge/consolevariablebridge.h>
#include <spdlog/spdlog.h>

using json = nlohmann::ordered_json;
using namespace Rando;
Expand All @@ -40,6 +42,10 @@ namespace {
std::string placementtxt;
} // namespace

static std::string SafeJsonString(std::string value) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This JSON stuff looks useful, but like with the icon fixes from before might be better in it's own PR unless it relates to the functionality of split songs.

@RaccoonCloud RaccoonCloud Jun 17, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really anything to do with the split-songs logic it seemed to be a language spelling or non-english text (curly quotes, accents, etc.) was producing invalid JSON in the spoiler log, which then broke SoH on Seed generation so SafeJsonString is a thin wrapper: SohUtils::SanitizeUtf8 on every string before it hits jsonData.

I bundled it here because I kept hitting it while testing split songs with FR/DE UI, but happy to move this and the SaveManager string overload into a PR or if you’d rather it separately? I was scared of messing up alot of preset code and stuff you all have in place and it being other languages I panicked abit

return SohUtils::SanitizeUtf8(std::move(value));
}

void GenerateHash() {
auto ctx = Rando::Context::GetInstance();
std::string hash = ctx->GetHash();
Expand Down Expand Up @@ -73,13 +79,16 @@ static void WriteLocation(std::string sphere, const RandomizerCheck locationKey,
switch (gSaveContext.language) {
case LANGUAGE_ENG:
default:
jsonData["playthrough"][sphere][location->GetName()] = itemLocation->GetPlacedItemName().GetEnglish();
jsonData["playthrough"][sphere][location->GetName()] =
SafeJsonString(itemLocation->GetPlacedItemName().GetEnglish());
break;
case LANGUAGE_GER:
jsonData["playthrough"][sphere][location->GetName()] = itemLocation->GetPlacedItemName().GetGerman();
jsonData["playthrough"][sphere][location->GetName()] =
SafeJsonString(itemLocation->GetPlacedItemName().GetGerman());
break;
case LANGUAGE_FRA:
jsonData["playthrough"][sphere][location->GetName()] = itemLocation->GetPlacedItemName().GetFrench();
jsonData["playthrough"][sphere][location->GetName()] =
SafeJsonString(itemLocation->GetPlacedItemName().GetFrench());
break;
}
}
Expand Down Expand Up @@ -130,7 +139,7 @@ static void WriteShuffledEntrance(std::string sphereString, Entrance* entrance)
case LANGUAGE_GER:
case LANGUAGE_FRA:
default:
jsonData["entrancesMap"][sphereString][name] = text;
jsonData["entrancesMap"][sphereString][name] = SafeJsonString(text);
break;
}
}
Expand All @@ -141,7 +150,8 @@ static void WriteSettings() {
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());
jsonData["settings"][option.GetName()] =
SafeJsonString(option.GetOptionText(ctx->GetOption(option.GetKey()).Get()));
}
}
}
Expand All @@ -162,7 +172,7 @@ static void WriteExcludedLocations() {
continue;
}

jsonData["excludedLocations"].push_back(RemoveLineBreaks(location->GetName()));
jsonData["excludedLocations"].push_back(SafeJsonString(RemoveLineBreaks(location->GetName())));
}
}
}
Expand All @@ -174,7 +184,8 @@ static void WriteStartingInventory() {
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());
jsonData["settings"][option->GetName()] =
SafeJsonString(option->GetOptionText(ctx->GetOption(option->GetKey()).Get()));
}
}
}
Expand All @@ -188,7 +199,7 @@ static void WriteEnabledTricks() {
if (ctx->GetTrickOption(static_cast<RandomizerTrick>(setting->GetKey())).IsNot(RO_GENERIC_ON)) {
continue;
}
jsonData["enabledTricks"].push_back(RemoveLineBreaks(setting->GetName()).c_str());
jsonData["enabledTricks"].push_back(SafeJsonString(RemoveLineBreaks(setting->GetName())));
}
}

Expand All @@ -200,7 +211,7 @@ static void WriteMasterQuestDungeons() {
if (dungeon->IsVanilla()) {
continue;
}
jsonData["masterQuestDungeons"].push_back(dungeon->GetName());
jsonData["masterQuestDungeons"].push_back(SafeJsonString(dungeon->GetName()));
}
}

Expand All @@ -210,7 +221,7 @@ static void WriteChosenOptions() {
for (const auto& trial : ctx->GetTrials()->GetTrialList()) {
if (trial->IsRequired()) {
std::string trialName = trial->GetName().GetForCurrentLanguage(MF_CLEAN);
jsonData["requiredTrials"].push_back(RemoveLineBreaks(trialName));
jsonData["requiredTrials"].push_back(SafeJsonString(RemoveLineBreaks(trialName)));
}
}
if (ctx->GetOption(RSK_SELECTED_STARTING_AGE).Is(RO_AGE_ADULT)) {
Expand Down Expand Up @@ -276,13 +287,13 @@ static void WriteAllLocations() {
if (!location->HasCustomPrice() && location->GetPlacedRandomizerGet() != RG_ICE_TRAP) {

jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()] =
placedItemName;
SafeJsonString(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;
SafeJsonString(placedItemName);

if (location->HasCustomPrice()) {
jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()]["price"] =
Expand All @@ -297,30 +308,33 @@ static void WriteAllLocations() {
case 0:
default:
jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()]
["model"] = Rando::StaticData::RetrieveItem(
ctx->overrides[location->GetRandomizerCheck()].LooksLike())
.GetName()
.GetEnglish();
["model"] = SafeJsonString(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();
["trickName"] = SafeJsonString(
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();
["model"] = SafeJsonString(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();
["trickName"] = SafeJsonString(
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();
["model"] = SafeJsonString(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();
["trickName"] = SafeJsonString(
ctx->overrides[location->GetRandomizerCheck()].GetTrickName().GetFrench());
break;
}
}
Expand All @@ -330,59 +344,65 @@ static void WriteAllLocations() {
void 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();
try {
jsonData.clear();
hintedLocations.clear();

jsonData["version"] = (char*)gBuildVersion;
jsonData["fileType"] = FILE_TYPE_SPOILER;
jsonData["git_branch"] = (char*)gGitBranch;
jsonData["git_commit"] = (char*)gGitCommitHash;
jsonData["seed"] = SafeJsonString(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++;
}

ctx->playthroughLocations.clear();
ctx->playthroughBeatable = false;
WriteSettings();
WriteExcludedLocations();
WriteStartingInventory();
WriteEnabledTricks();
WriteMasterQuestDungeons();
WriteChosenOptions();
WritePlaythrough();

ctx->WriteHintJson(jsonData);
WriteShuffledEntrances();
WriteAllLocations();
ctx->playthroughLocations.clear();
ctx->playthroughBeatable = false;

if (!std::filesystem::exists(Ship::Context::GetPathRelativeToAppDirectory("Randomizer"))) {
std::filesystem::create_directory(Ship::Context::GetPathRelativeToAppDirectory("Randomizer"));
}
ctx->WriteHintJson(jsonData);
WriteShuffledEntrances();
WriteAllLocations();

std::string jsonString = jsonData.dump(4);
std::ostringstream fileNameStream;
for (uint8_t i = 0; i < ctx->hashIconIndexes.size(); i++) {
if (i) {
fileNameStream << '-';
if (!std::filesystem::exists(Ship::Context::GetPathRelativeToAppDirectory("Randomizer"))) {
std::filesystem::create_directory(Ship::Context::GetPathRelativeToAppDirectory("Randomizer"));
}
if (ctx->hashIconIndexes[i] < 10) {
fileNameStream << '0';

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]);
}
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());
} catch (const std::exception& e) {
SPDLOG_ERROR("SpoilerLog_Write failed: {}", e.what());
}
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());
}

void PlacementLog_Msg(std::string_view msg) {
Expand Down
Loading