-
Notifications
You must be signed in to change notification settings - Fork 776
Feature/split songs #6627
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Feature/split songs #6627
Changes from 23 commits
9397b10
5feddec
2bd2ea4
5417774
3d0a0f6
0386051
6d49e4d
042e035
6a10537
bc2027e
72c849f
bd13ce5
11a4287
f8464be
10f462d
2340e5a
3e1aff7
2998034
600b8d3
39427cc
b78aac9
79b6882
1198703
7d146a6
2edee8d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -40,6 +42,10 @@ namespace { | |
| std::string placementtxt; | ||
| } // namespace | ||
|
|
||
| static std::string SafeJsonString(std::string value) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
|
|
@@ -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; | ||
| } | ||
| } | ||
|
|
@@ -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; | ||
| } | ||
| } | ||
|
|
@@ -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())); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -162,7 +172,7 @@ static void WriteExcludedLocations() { | |
| continue; | ||
| } | ||
|
|
||
| jsonData["excludedLocations"].push_back(RemoveLineBreaks(location->GetName())); | ||
| jsonData["excludedLocations"].push_back(SafeJsonString(RemoveLineBreaks(location->GetName()))); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -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())); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -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()))); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -200,7 +211,7 @@ static void WriteMasterQuestDungeons() { | |
| if (dungeon->IsVanilla()) { | ||
| continue; | ||
| } | ||
| jsonData["masterQuestDungeons"].push_back(dungeon->GetName()); | ||
| jsonData["masterQuestDungeons"].push_back(SafeJsonString(dungeon->GetName())); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -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)) { | ||
|
|
@@ -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"] = | ||
|
|
@@ -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; | ||
| } | ||
| } | ||
|
|
@@ -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) { | ||
|
|
||
There was a problem hiding this comment.
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?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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