From 4a067931bed25f9c8fa540d39f8254b0fbf4f6e5 Mon Sep 17 00:00:00 2001 From: pizza1398 <259104093+pizza1398@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:39:37 +0100 Subject: [PATCH 1/3] Add OOT3DR's spoiler-log.css to romfs --- romfs/spoiler-log.css | 193 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 romfs/spoiler-log.css diff --git a/romfs/spoiler-log.css b/romfs/spoiler-log.css new file mode 100644 index 0000000..b140604 --- /dev/null +++ b/romfs/spoiler-log.css @@ -0,0 +1,193 @@ +:root { + padding: 1.5rem 3rem; + font-size: 16px; + font-family: monospace; +} + +spoiler-log::before { + display: block; + content: "Version: " attr(version) "\ASeed: " attr(seed) "\AHash: " attr(hash); + white-space: pre; + margin-bottom: 1em;; +} + +/* Collapsible content */ +.collapse { + display: block; + float: left; + appearance: none; + --checkbox-size: 2.5rem; +} + +:is(playthrough, entrance-playthrough) .collapse { + --checkbox-size: 1.3rem !important; + margin-left: 3rem; +} + +.collapse::before { + display: block; + content: "🞃"; + height: var(--checkbox-size); + width: var(--checkbox-size); + font-size: var(--checkbox-size); + line-height: var(--checkbox-size); + position: relative; + font-weight: bolder; +} + +.collapse:checked::before { + content: "🞂"; +} + +.collapse:checked + * > * { + display: none; +} + +/* Hidden sections */ +settings *, /* Making the `settings` itself tag be displayable but hiding its sub-elements so we can use `settings::before` to display the "Hide spoilers" text */ +excluded-locations, +enabled-tricks, +enabled-glitches, +required-trials, +song-notes, +enemies, +hints { + display: none; +} + +/* Headers */ +starting-inventory, +gold-skulltulas, +playthrough, +way-of-the-hero-locations, +entrance-playthrough, +all-locations, +master-quest-dungeons { + display: block; + border-collapse: collapse; +} + +:is(starting-inventory, +gold-skulltulas, +playthrough, +way-of-the-hero-locations, +entrance-playthrough, +all-locations, +master-quest-dungeons)::before { + display: block; + font-size: 2.5rem; +} + +starting-inventory::before { content: "Starting inventory:"; } +gold-skulltulas::before { content: "Gold Skulltulas:"; } +playthrough::before { content: "Playthrough:"; } +way-of-the-hero-locations::before { content: "Way of the Hero locations:"; } +entrance-playthrough::before { content: "Entrance playthrough:"; } +all-locations::before { content: "All locations:"; } +master-quest-dungeons::before { content: "Master Quest dungeons:"; } + +sphere { + display: block; + margin-left: 3rem; +} + +sphere::before { + display: block; + font-size: 1.5rem; + content: "Sphere " attr(level) ":"; +} + +/* Items */ +item, gs, location, dungeon, entrance { + display: table-row; + border-bottom: 1px solid black; +} + +:is(item, gs, location, dungeon, entrance):last-child { + border-bottom: none; +} + +:is(item, gs, location, dungeon, entrance)::before { + display: table-cell; + content: attr(name); + padding-right: 2rem; + padding-left: 3rem; + padding-top: 0.4rem; + padding-bottom: 0.4rem; + user-select: text; +} + +:is(item, gs, location, dungeon, entrance)::after { + display: table-cell; + content: ""; +} + +[price]::after { + display: table-cell; + content: " (" attr(price) " rupees)"; + margin-left: 1rem; +} + +/* Hide spoilers button */ +#hide-spoilers + *::before { + display: block; + font-size: 2.3rem; + content: "Hide spoilers"; + margin-bottom: 1rem; +} + +#hide-spoilers { + appearance: none; + display: block; + position: relative; + float: left; + margin-right: 1rem; + top: 0.2rem; +} + +#hide-spoilers::after { + display: inline-block; + content: ""; + width: 1.5rem; + height: 1.5rem; + background-color: #fff; + border-radius: 0.75rem; + position: absolute; + top: 0.25rem; + left: 0.25rem; + transition-property: left; + transition-duration: 0.3s; +} + +#hide-spoilers:checked::after { + left: 2.25rem;; +} + +#hide-spoilers::before { + display: inline-block; + content: ""; + width: 4rem; + height: 2rem; + background-color: #888; + border-radius: 1rem; + transition-property: background-color; + transition-duration: 0.3s; +} + +#hide-spoilers:checked::before { + background-color: #5252e4; +} + +/* Hidden spoilers */ +#hide-spoilers:checked ~ * :is(gs, location, dungeon, entrance) { + background-color: #000; +} + +/* Don't need to hide the location names for these */ +gs::before, all-locations location::before, entrance::before { + background-color: #fff; +} + +#hide-spoilers:checked ~ * :is(gs, location, dungeon, entrance):hover { + background-color: #fff; +} From b4e61b0a4e226e34b95a24d82c8cbac067e085e1 Mon Sep 17 00:00:00 2001 From: pizza1398 <259104093+pizza1398@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:53:08 +0100 Subject: [PATCH 2/3] Update utils to match OOT3DR - Convert RemoveLineBreaks to SanitizedString - Move CopyFile to utils --- source/include/utils.hpp | 11 ++++++-- source/patch.cpp | 33 +---------------------- source/preset.cpp | 8 +++--- source/spoiler_log.cpp | 6 ++--- source/utils.cpp | 58 +++++++++++++++++++++++++++++++++++++--- 5 files changed, 71 insertions(+), 45 deletions(-) diff --git a/source/include/utils.hpp b/source/include/utils.hpp index 3f5f698..e48bca7 100644 --- a/source/include/utils.hpp +++ b/source/include/utils.hpp @@ -1,6 +1,13 @@ #pragma once -#include #include +#include <3ds.h> + +/// Returns a new string with: +/// - Leading spaces removed. +/// - Line breaks replaced with spaces. +/// - Consecutive spaces removed. +std::string SanitizedString(std::string s); + +bool CopyFile(FS_Archive sdmcArchive, const char* dst, const char* src); -std::string RemoveLineBreaks(std::string s); diff --git a/source/patch.cpp b/source/patch.cpp index 325cf6e..731c15d 100644 --- a/source/patch.cpp +++ b/source/patch.cpp @@ -7,6 +7,7 @@ #include "spoiler_log.hpp" //#include "entrance.hpp" #include "item_location.hpp" +#include "utils.hpp" #include #include @@ -19,38 +20,6 @@ using FILEPtr = std::unique_ptr; -bool CopyFile(FS_Archive sdmcArchive, const char* dst, const char* src) { - Result res = 0; - Handle outFile; - u32 bytesWritten = 0; - // Delete dst if it exists - FSUSER_DeleteFile(sdmcArchive, fsMakePath(PATH_ASCII, dst)); - - // Open dst destination - if (!R_SUCCEEDED(res = FSUSER_OpenFile(&outFile, sdmcArchive, fsMakePath(PATH_ASCII, dst), FS_OPEN_WRITE | FS_OPEN_CREATE, 0))) { - return false; - } - - if (auto file = FILEPtr{std::fopen(src, "r"), std::fclose}) { - // obtain file size - fseek(file.get(), 0, SEEK_END); - const auto lSize = static_cast(ftell(file.get())); - rewind(file.get()); - - // copy file into the buffer - std::vector buffer(lSize); - fread(buffer.data(), 1, buffer.size(), file.get()); - - // Write the buffer to final destination - if (!R_SUCCEEDED(res = FSFILE_Write(outFile, &bytesWritten, 0, buffer.data(), buffer.size(), FS_WRITE_FLUSH))) { - return false; - } - } - - FSFILE_Close(outFile); - return true; -} - bool WritePatch(u32 patchOffset, s32 patchSize, char* patchDataPtr, Handle& code, u32& bytesWritten, u32& totalRW, char* buf) { //patch sizes greater than PATCH_SIZE_MAX have to be split up due to IPS patching specifications diff --git a/source/preset.cpp b/source/preset.cpp index 93af53f..1dadb10 100644 --- a/source/preset.cpp +++ b/source/preset.cpp @@ -120,7 +120,7 @@ bool SavePreset(std::string_view presetName, OptionCategory category) { } XMLElement* newSetting = rootNode->InsertNewChildElement("setting"); - newSetting->SetAttribute("name", RemoveLineBreaks(setting->GetName()).c_str()); + newSetting->SetAttribute("name", SanitizedString(setting->GetName()).c_str()); newSetting->SetText(setting->GetSelectedOptionText().c_str()); } } @@ -160,8 +160,8 @@ bool LoadPreset(std::string_view presetName, OptionCategory category) { } // Since presets are saved linearly, we can simply loop through the nodes as // we loop through the settings to find most of the matching elements. - const std::string& settingToFind = RemoveLineBreaks(setting->GetName()); - if (settingToFind == RemoveLineBreaks(curNode->Attribute("name"))) { + const std::string& settingToFind = SanitizedString(setting->GetName()); + if (settingToFind == SanitizedString(curNode->Attribute("name"))) { setting->SetSelectedIndexByString(curNode->GetText()); curNode = curNode->NextSiblingElement(); } else { @@ -170,7 +170,7 @@ bool LoadPreset(std::string_view presetName, OptionCategory category) { // next setting and element line up with each other. curNode = rootNode->FirstChildElement(); while (curNode != nullptr) { - if (settingToFind == RemoveLineBreaks(curNode->Attribute("name"))) { + if (settingToFind == SanitizedString(curNode->Attribute("name"))) { setting->SetSelectedIndexByString(curNode->GetText()); curNode = curNode->NextSiblingElement(); break; diff --git a/source/spoiler_log.cpp b/source/spoiler_log.cpp index d6a46be..765fa71 100644 --- a/source/spoiler_log.cpp +++ b/source/spoiler_log.cpp @@ -455,7 +455,7 @@ static void WriteSettings(tinyxml2::XMLDocument& spoilerLog, const bool printAll for (const Option* setting : *menu->settingsList) { if (printAll || (!setting->IsHidden() && setting->IsCategory(OptionCategory::Setting))) { auto node = parentNode->InsertNewChildElement("setting"); - node->SetAttribute("name", RemoveLineBreaks(setting->GetName()).c_str()); + node->SetAttribute("name", SanitizedString(setting->GetName()).c_str()); node->SetText(setting->GetSelectedOptionText().c_str()); } } @@ -474,7 +474,7 @@ static void WriteExcludedLocations(tinyxml2::XMLDocument& spoilerLog) { } tinyxml2::XMLElement* node = spoilerLog.NewElement("location"); - node->SetAttribute("name", RemoveLineBreaks(location->GetName()).c_str()); + node->SetAttribute("name", SanitizedString(location->GetName()).c_str()); parentNode->InsertEndChild(node); } @@ -564,7 +564,7 @@ static void WriteEnabledTricks(tinyxml2::XMLDocument& spoilerLog) { } auto node = parentNode->InsertNewChildElement("trick"); - node->SetAttribute("name", RemoveLineBreaks(setting->GetName()).c_str()); + node->SetAttribute("name", SanitizedString(setting->GetName()).c_str()); } if (!parentNode->NoChildren()) { diff --git a/source/utils.cpp b/source/utils.cpp index 1cbe8c0..854958b 100644 --- a/source/utils.cpp +++ b/source/utils.cpp @@ -1,7 +1,57 @@ +#include +#include +#include + #include "utils.hpp" -// Removes any line breaks from s. -std::string RemoveLineBreaks(std::string s) { - s.erase(std::remove(s.begin(), s.end(), '\n'), s.end()); +std::string SanitizedString(std::string s) { + // Remove leading spaces + while (s.size() > 0 && s[0] == ' ') { + s.erase(0, 1); + } + + // Replace line breaks with spaces + std::replace(s.begin(), s.end(), '\n', ' '); + + // Remove consecutive spaces + while (s.find(" ") != std::string::npos) { + s.erase(s.find(" "), 1); + } + return s; -} \ No newline at end of file +} + +using FILEPtr = std::unique_ptr; + +bool CopyFile(FS_Archive sdmcArchive, const char* dst, const char* src) { + Result res = 0; + Handle outFile; + u32 bytesWritten = 0; + // Delete dst if it exists + FSUSER_DeleteFile(sdmcArchive, fsMakePath(PATH_ASCII, dst)); + + // Open dst destination + if (!R_SUCCEEDED(res = FSUSER_OpenFile(&outFile, sdmcArchive, fsMakePath(PATH_ASCII, dst), + FS_OPEN_WRITE | FS_OPEN_CREATE, 0))) { + return false; + } + + if (auto file = FILEPtr{ std::fopen(src, "r"), std::fclose }) { + // obtain file size + fseek(file.get(), 0, SEEK_END); + const auto lSize = static_cast(ftell(file.get())); + rewind(file.get()); + + // copy file into the buffer + std::vector buffer(lSize); + fread(buffer.data(), 1, buffer.size(), file.get()); + + // Write the buffer to final destination + if (!R_SUCCEEDED(res = FSFILE_Write(outFile, &bytesWritten, 0, buffer.data(), buffer.size(), FS_WRITE_FLUSH))) { + return false; + } + } + + FSFILE_Close(outFile); + return true; +} From 7a24f1f533489556d1e4a06601d5a4dad746a6e1 Mon Sep 17 00:00:00 2001 From: pizza1398 <259104093+pizza1398@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:30:13 +0100 Subject: [PATCH 3/3] Add spoiler-log stylesheet feature from OOT3DR --- source/include/spoiler_log.hpp | 2 +- source/menu.cpp | 2 +- source/patch.cpp | 2 + source/spoiler_log.cpp | 73 +++++++++++++++++++++++++++++----- 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/source/include/spoiler_log.hpp b/source/include/spoiler_log.hpp index 980ebce..a9cfe42 100644 --- a/source/include/spoiler_log.hpp +++ b/source/include/spoiler_log.hpp @@ -9,7 +9,7 @@ using RandomizerHash = std::array; -void CreateLogDirectories(FS_Archive sdmcArchive); +void InitLogDirectories(FS_Archive sdmcArchive); void GenerateHash(); const RandomizerHash& GetRandomizerHash(); diff --git a/source/menu.cpp b/source/menu.cpp index ff3647e..21cc3e5 100644 --- a/source/menu.cpp +++ b/source/menu.cpp @@ -64,7 +64,7 @@ void MenuInit() { // Create directories FS_Archive sdmcArchive; if (R_SUCCEEDED(FSUSER_OpenArchive(&sdmcArchive, ARCHIVE_SDMC, fsMakePath(PATH_EMPTY, "")))) { - CreateLogDirectories(sdmcArchive); + InitLogDirectories(sdmcArchive); CreatePresetDirectories(sdmcArchive); FSUSER_CloseArchive(sdmcArchive); diff --git a/source/patch.cpp b/source/patch.cpp index 731c15d..96f0d38 100644 --- a/source/patch.cpp +++ b/source/patch.cpp @@ -479,5 +479,7 @@ bool WriteAllPatches() { FSUSER_CloseArchive(sdmcArchive); + romfsExit(); + return true; } diff --git a/source/spoiler_log.cpp b/source/spoiler_log.cpp index 765fa71..40d38e0 100644 --- a/source/spoiler_log.cpp +++ b/source/spoiler_log.cpp @@ -96,12 +96,15 @@ namespace { static RandomizerHash randomizerHash; static SpoilerData spoilerData; -void CreateLogDirectories(FS_Archive sdmcArchive) { +void InitLogDirectories(FS_Archive sdmcArchive) { std::vector dirs = { "/MM3DR/", "/MM3DR/Spoiler_Logs/", }; + const char* stylesheetSrc = "romfs:/spoiler-log.css"; + const char* stylesheetDest = "/MM3DR/Spoiler_Logs/spoiler-log.css"; + const auto printInfo = [&](int progress) { consoleClear(); printf("\x1b[10;10HCreating Log Directories"); @@ -113,6 +116,13 @@ void CreateLogDirectories(FS_Archive sdmcArchive) { FSUSER_CreateDirectory(sdmcArchive, fsMakePath(PATH_ASCII, dirs[i].c_str()), FS_ATTRIBUTE_DIRECTORY); printInfo(i + 1); } + + Result rc = romfsInit(); + if (rc) { + printf("\nromfsInit: %08lX\n", rc); + } + CopyFile(sdmcArchive, stylesheetDest, stylesheetSrc); + romfsExit(); } void GenerateHash() { @@ -443,6 +453,18 @@ static void WriteShuffledEntrance( } }*/ +// Create a checkbox that collapses the next section when checked +static tinyxml2::XMLElement* CreateCollapseCheckbox(tinyxml2::XMLDocument& spoilerLog, + const bool startCollapsed = true) { + auto collapseCheckbox = spoilerLog.NewElement("h:input"); + collapseCheckbox->SetAttribute("type", "checkbox"); + collapseCheckbox->SetAttribute("class", "collapse"); + if (startCollapsed) { + collapseCheckbox->SetAttribute("checked", ""); + } + return collapseCheckbox; +} + // Writes the settings (without excluded locations, starting inventory and tricks) to the spoilerLog document. static void WriteSettings(tinyxml2::XMLDocument& spoilerLog, const bool printAll = false) { auto parentNode = spoilerLog.NewElement("settings"); @@ -484,7 +506,7 @@ static void WriteExcludedLocations(tinyxml2::XMLDocument& spoilerLog) { } // Writes the starting inventory to the spoiler log, if there is any. -static void WriteStartingInventory(tinyxml2::XMLDocument& spoilerLog) { +static void WriteStartingInventory(tinyxml2::XMLDocument& spoilerLog, const bool collapsible = false) { auto parentNode = spoilerLog.NewElement("starting-inventory"); for (size_t i = 0; i < Settings::startingInventoryInventory.size(); ++i) { @@ -550,6 +572,9 @@ static void WriteStartingInventory(tinyxml2::XMLDocument& spoilerLog) { } if (!parentNode->NoChildren()) { + if (collapsible) { + spoilerLog.RootElement()->InsertEndChild(CreateCollapseCheckbox(spoilerLog)); + } spoilerLog.RootElement()->InsertEndChild(parentNode); } } @@ -574,10 +599,13 @@ static void WriteEnabledTricks(tinyxml2::XMLDocument& spoilerLog) { // Writes the intended playthrough to the spoiler log, separated into spheres. -static void WritePlaythrough(tinyxml2::XMLDocument& spoilerLog) { +static void WritePlaythrough(tinyxml2::XMLDocument& spoilerLog, const bool collapsible = false) { auto playthroughNode = spoilerLog.NewElement("playthrough"); for (uint i = 0; i < playthroughLocations.size(); ++i) { + if (collapsible) { + playthroughNode->InsertEndChild(CreateCollapseCheckbox(spoilerLog)); + } auto sphereNode = playthroughNode->InsertNewChildElement("sphere"); sphereNode->SetAttribute("level", i + 1); @@ -586,11 +614,14 @@ static void WritePlaythrough(tinyxml2::XMLDocument& spoilerLog) { } } + if (collapsible) { + spoilerLog.RootElement()->InsertEndChild(CreateCollapseCheckbox(spoilerLog)); + } spoilerLog.RootElement()->InsertEndChild(playthroughNode); } /* //Write the randomized entrance playthrough to the spoiler log, if applicable -static void WriteShuffledEntrances(tinyxml2::XMLDocument& spoilerLog) { +static void WriteShuffledEntrances(tinyxml2::XMLDocument& spoilerLog, const bool collapsible = false) { if (!Settings::ShuffleEntrances || noRandomEntrances) { return; } @@ -598,6 +629,9 @@ static void WriteShuffledEntrances(tinyxml2::XMLDocument& spoilerLog) { auto playthroughNode = spoilerLog.NewElement("entrance-playthrough"); for (uint i = 0; i < playthroughEntrances.size(); ++i) { + if (collapsible) { + playthroughNode->InsertEndChild(CreateCollapseCheckbox(spoilerLog)); + } auto sphereNode = playthroughNode->InsertNewChildElement("sphere"); sphereNode->SetAttribute("level", i + 1); @@ -606,11 +640,14 @@ static void WriteShuffledEntrances(tinyxml2::XMLDocument& spoilerLog) { } } + if (collapsible) { + spoilerLog.RootElement()->InsertEndChild(CreateCollapseCheckbox(spoilerLog)); + } spoilerLog.RootElement()->InsertEndChild(playthroughNode); } */ // Writes the WOTH locations to the spoiler log, if there are any. -static void WriteWayOfTheHeroLocation(tinyxml2::XMLDocument& spoilerLog) { +static void WriteWayOfTheHeroLocation(tinyxml2::XMLDocument& spoilerLog, const bool collapsible = false) { auto parentNode = spoilerLog.NewElement("way-of-the-hero-locations"); for (const LocationKey key : wothLocations) { @@ -618,6 +655,9 @@ static void WriteWayOfTheHeroLocation(tinyxml2::XMLDocument& spoilerLog) { } if (!parentNode->NoChildren()) { + if (collapsible) { + spoilerLog.RootElement()->InsertEndChild(CreateCollapseCheckbox(spoilerLog)); + } spoilerLog.RootElement()->InsertEndChild(parentNode); } } @@ -645,13 +685,16 @@ static void WriteHints(tinyxml2::XMLDocument& spoilerLog) { spoilerLog.RootElement()->InsertEndChild(parentNode); } -static void WriteAllLocations(tinyxml2::XMLDocument& spoilerLog) { +static void WriteAllLocations(tinyxml2::XMLDocument& spoilerLog, const bool collapsible = false) { auto parentNode = spoilerLog.NewElement("all-locations"); for (const LocationKey key : allLocations) { WriteLocation(parentNode, key, true); } + if (collapsible) { + spoilerLog.RootElement()->InsertEndChild(CreateCollapseCheckbox(spoilerLog)); + } spoilerLog.RootElement()->InsertEndChild(parentNode); } @@ -660,6 +703,7 @@ bool SpoilerLog_Write() { auto spoilerLog = tinyxml2::XMLDocument(false); spoilerLog.InsertEndChild(spoilerLog.NewDeclaration()); + spoilerLog.InsertEndChild(spoilerLog.NewDeclaration("xml-stylesheet href=\"spoiler-log.css\"")); auto rootNode = spoilerLog.NewElement("spoiler-log"); spoilerLog.InsertEndChild(rootNode); @@ -667,21 +711,28 @@ bool SpoilerLog_Write() { rootNode->SetAttribute("version", Settings::version.c_str()); rootNode->SetAttribute("seed", Settings::seed.c_str()); rootNode->SetAttribute("hash", GetRandomizerHashAsString().c_str()); + rootNode->SetAttribute("xmlns:h", "http://www.w3.org/1999/xhtml"); + + auto hideSpoilersCheckbox = spoilerLog.NewElement("h:input"); + hideSpoilersCheckbox->SetAttribute("type", "checkbox"); + hideSpoilersCheckbox->SetAttribute("checked", ""); + hideSpoilersCheckbox->SetAttribute("id", "hide-spoilers"); + rootNode->InsertEndChild(hideSpoilersCheckbox); WriteSettings(spoilerLog, true); WriteExcludedLocations(spoilerLog); - WriteStartingInventory(spoilerLog); //WriteEnabledTricks(spoilerLog); - WritePlaythrough(spoilerLog); - WriteWayOfTheHeroLocation(spoilerLog); + WriteStartingInventory(spoilerLog, true); + WritePlaythrough(spoilerLog, true); + WriteWayOfTheHeroLocation(spoilerLog, true); playthroughLocations.clear(); playthroughBeatable = false; wothLocations.clear(); WriteHints(spoilerLog); - // WriteShuffledEntrances(spoilerLog); - WriteAllLocations(spoilerLog); + // WriteShuffledEntrances(spoilerLog, true); + WriteAllLocations(spoilerLog, true); auto e = spoilerLog.SaveFile(GetSpoilerLogPath().c_str()); return e == tinyxml2::XML_SUCCESS;