diff --git a/code/src/loader.c b/code/src/loader.c index 62ebb870..d2142ac3 100644 --- a/code/src/loader.c +++ b/code/src/loader.c @@ -27,11 +27,9 @@ void loader_main(void) { if (res < 0) svcBreak(1); - // Hacky solution to be able to edit gDrawItemTable, which is normally in RO data - res = svcControlProcessMemory(getCurrentProcessHandle(), 0x4D8000, 0x4D8000, 0x1000, MEMOP_PROT, - MEMPERM_READ | MEMPERM_WRITE); - // Same for gGearUsabilityTable - res = svcControlProcessMemory(getCurrentProcessHandle(), 0x4D4000, 0x4D4000, 0x1000, MEMOP_PROT, + // Hacky solution to be able to edit the following structs, which are normally in RO data: + // gGearUsabilityTable, gOcarinaMenuSongNoteSequences, gOcarinaMenuSongLengths + res = svcControlProcessMemory(getCurrentProcessHandle(), 0x4D4000, 0x4D4000, 0x2000, MEMOP_PROT, MEMPERM_READ | MEMPERM_WRITE); if (res < 0) diff --git a/code/src/main.c b/code/src/main.c index 730246e6..fe0d8e8c 100644 --- a/code/src/main.c +++ b/code/src/main.c @@ -17,6 +17,7 @@ #include "enemizer.h" #include "scene.h" #include "gloom.h" +#include "ocarina_notes.h" #include "z3D/z3D.h" #include "3ds/extdata.h" @@ -33,6 +34,7 @@ void Randomizer_Init() { Entrance_Init(); ItemOverride_Init(); Enemizer_Init(); + OcarinaNotes_Init(); extDataInit(); irrstInit(); diff --git a/code/src/ocarina_notes.c b/code/src/ocarina_notes.c index eb762c2b..3ba41bb2 100644 --- a/code/src/ocarina_notes.c +++ b/code/src/ocarina_notes.c @@ -2,6 +2,62 @@ #include "savefile.h" #include "settings.h" +// Values to be written to OcarinaNote.pitch for each button. +const u8 notePitches[5] = { + [OCARINA_BUTTON_L] = 0x2, [OCARINA_BUTTON_R] = 0x5, [OCARINA_BUTTON_Y] = 0x9, + [OCARINA_BUTTON_X] = 0xB, [OCARINA_BUTTON_A] = 0xE, +}; + +// Used to convert an OcarinaSongId to an index for the song list visible while playing the ocarina. +const u8 menuSongIds[12] = { + [OCARINA_SONG_MINUET] = 2, [OCARINA_SONG_BOLERO] = 6, [OCARINA_SONG_SERENADE] = 10, [OCARINA_SONG_REQUIEM] = 3, + [OCARINA_SONG_NOCTURNE] = 7, [OCARINA_SONG_PRELUDE] = 11, [OCARINA_SONG_SARIAS] = 8, [OCARINA_SONG_EPONAS] = 4, + [OCARINA_SONG_LULLABY] = 0, [OCARINA_SONG_SUNS] = 1, [OCARINA_SONG_TIME] = 5, [OCARINA_SONG_STORMS] = 9, +}; + +// Used to store the button sequence for each song with u32 values instead of u8, +// in order to override gOcarinaMenuSongNoteSequences +u32 rMenuSongsOverrides[12][8] = { 0 }; + +void OcarinaNotes_Init(void) { + if (gSettingsContext.randomSongNotes == OFF) { + return; + } + + for (u32 songId = 0; songId < 12; songId++) { + + OcarinaSongButtonSequence songBtns = gOcarinaSongButtons[songId]; + + // set playback data + OcarinaNote* songNotes = sOcarinaSongNotes[songId]; + u32 noteIndex = 0; + for (u32 btnIndex = 0; btnIndex < songBtns.length; btnIndex++) { + // set menu song override data + rMenuSongsOverrides[songId][btnIndex] = songBtns.buttons[btnIndex]; + + // add very short pause if this note is the same as the previous one + if (btnIndex > 0 && songBtns.buttons[btnIndex] == songBtns.buttons[btnIndex - 1]) { + songNotes[noteIndex].pitch = 0xFF; + songNotes[noteIndex].length = 0x1; + noteIndex++; + } + + songNotes[noteIndex].pitch = notePitches[songBtns.buttons[btnIndex]]; + songNotes[noteIndex].length = 0x24; // fixed note length + + noteIndex++; + } + + // Set final note to mark end of sequence + songNotes[noteIndex].pitch = 0xFF; + songNotes[noteIndex].length = 0; + + // override menu data + gOcarinaMenuSongLengths[menuSongIds[songId]] = songBtns.length; + gOcarinaMenuSongNoteSequences[menuSongIds[songId]] = rMenuSongsOverrides[songId]; + } +} + s32 OcarinaNotes_IsButtonOwned(OcarinaNoteButton button) { return (gSettingsContext.shuffleOcarinaButtons == OFF) || (gExtSaveData.extInf[EXTINF_OCARINA_BUTTONS] & (1 << button)); @@ -73,7 +129,7 @@ void OcarinaNotes_MoveButtons(void* spriteStruct, Vec2f* posOffset, u32 unk, u32 } u32 OcarinaNotes_HandleInputs(u32 ocarinaInputs) { - static const u32 btnShifts[5] = { 7, 9, 10, 11, 8 }; // not the same offsets as the btn_t struct + static const u32 btnShifts[5] = { 7, 9, 11, 10, 8 }; // not the same offsets as the btn_t struct u32 ownedBtnsMask = 0; for (OcarinaNoteButton btn = 0; btn < 5; btn++) { ownedBtnsMask |= (OcarinaNotes_IsButtonOwned(btn) << btnShifts[btn]); diff --git a/code/src/ocarina_notes.h b/code/src/ocarina_notes.h index ebb20873..a9a1e958 100644 --- a/code/src/ocarina_notes.h +++ b/code/src/ocarina_notes.h @@ -1,15 +1,15 @@ #ifndef _OCARINA_NOTES_H_ #define _OCARINA_NOTES_H_ -#include "z3D/z3D.h" -#include "input.h" +#include "../include/z3D/z3D.h" typedef enum OcarinaNoteButton { OCARINA_BUTTON_L, OCARINA_BUTTON_R, - OCARINA_BUTTON_X, OCARINA_BUTTON_Y, + OCARINA_BUTTON_X, OCARINA_BUTTON_A, + OCARINA_BUTTON_MAX, } OcarinaNoteButton; enum OcarinaSprites { @@ -60,8 +60,47 @@ enum OcarinaSprites { OCS_YELLOW_MARKER_5, }; // max 0x6C +typedef enum { + OCARINA_SONG_MINUET, + OCARINA_SONG_BOLERO, + OCARINA_SONG_SERENADE, + OCARINA_SONG_REQUIEM, + OCARINA_SONG_NOCTURNE, + OCARINA_SONG_PRELUDE, + OCARINA_SONG_SARIAS, + OCARINA_SONG_EPONAS, + OCARINA_SONG_LULLABY, + OCARINA_SONG_SUNS, + OCARINA_SONG_TIME, + OCARINA_SONG_STORMS, + OCARINA_SONG_MAX, +} OcarinaSongId; + +typedef struct { + /* 0x0 */ u8 length; + /* 0x1 */ u8 buttons[8]; +} OcarinaSongButtonSequence; // size = 0x9 + +typedef struct { + /* 0x0 */ u8 pitch; // number of semitones above middle C + /* 0x2 */ u16 length; // number of frames the note is sustained + /* 0x4 */ u8 volume; + /* 0x5 */ u8 vibrato; + /* 0x6 */ s8 bend; // frequency multiplicative offset from the pitch + /* 0x7 */ u8 bFlat4Flag; +} OcarinaNote; // size = 0x8 + #define OcarinaUIStruct (*((void**)GAME_ADDR(0x5093EC))) +// sequence of notes to check when a song has been played +#define gOcarinaSongButtons ((OcarinaSongButtonSequence*)GAME_ADDR(0x54C222)) +// sequence of note data used for the playbacks (2D array of 20 notes for each song) +#define sOcarinaSongNotes ((OcarinaNote(*)[20])GAME_ADDR(0x54B5F2)) +// sequence of notes to display on the ocarina song list menu (array of pointers to arrays) +#define gOcarinaMenuSongNoteSequences ((u32**)GAME_ADDR(0x4D541C)) +#define gOcarinaMenuSongLengths ((u32*)GAME_ADDR(0x4D53C8)) + +void OcarinaNotes_Init(void); s32 OcarinaNotes_IsButtonOwned(OcarinaNoteButton button); void OcarinaNotes_RegisterButtonOwned(OcarinaNoteButton button); diff --git a/code/src/settings.h b/code/src/settings.h index 67dfee1b..e042e38d 100644 --- a/code/src/settings.h +++ b/code/src/settings.h @@ -623,6 +623,7 @@ typedef struct { u8 hyperEnemies; u8 freeCamera; u8 randomGsLocations; + u8 randomSongNotes; u8 faroresWindAnywhere; u8 stickAsAdult; diff --git a/romfs/spoiler-log.css b/romfs/spoiler-log.css index 02b8ef28..b140604a 100644 --- a/romfs/spoiler-log.css +++ b/romfs/spoiler-log.css @@ -49,6 +49,7 @@ excluded-locations, enabled-tricks, enabled-glitches, required-trials, +song-notes, enemies, hints { display: none; diff --git a/source/descriptions.cpp b/source/descriptions.cpp index de19370e..4622669e 100644 --- a/source/descriptions.cpp +++ b/source/descriptions.cpp @@ -1392,6 +1392,12 @@ string_view gsLocGuaranteeNewDesc = "Excludes the original location from the "If no new locations are available, the original\n"// "will be used regardless."; // // +/*------------------------------ // +| RANDOM SONG NOTES | // +------------------------------*/ // +string_view randomSongNotesDesc = "Randomize the notes for each ocarina song.\n" // + "Regular songs will be 3 notes repeated twice.\n" // + "Warp songs will be between 5 and 8 notes."; // //--------------// /*------------------------------ // | DETAILED LOGIC EXPLANATIONS | // diff --git a/source/descriptions.hpp b/source/descriptions.hpp index 42f98567..a23ee177 100644 --- a/source/descriptions.hpp +++ b/source/descriptions.hpp @@ -432,6 +432,7 @@ extern string_view hyperEnemiesDesc; extern string_view freeCamDesc; extern string_view randomGsLocationsDesc; extern string_view gsLocGuaranteeNewDesc; +extern string_view randomSongNotesDesc; extern string_view ToggleAllTricksDesc; diff --git a/source/fill.cpp b/source/fill.cpp index 3c481729..20d95c61 100644 --- a/source/fill.cpp +++ b/source/fill.cpp @@ -15,6 +15,7 @@ #include "shops.hpp" #include "debug.hpp" #include "enemizer.hpp" +#include "ocarina_notes.hpp" #include #include @@ -961,6 +962,7 @@ void VanillaFill() { Location(loc)->PlaceVanillaItem(); } Enemizer::RandomizeEnemies(); + OcarinaNotes::GenerateSongList(); // If necessary, handle ER stuff playthroughEntrances.clear(); if (ShuffleEntrances) { @@ -1006,6 +1008,7 @@ int Fill() { // can validate the world using deku/hylian shields AddElementsToPool(ItemPool, GetMinVanillaShopItems(32)); // assume worst case shopsanity 4 Enemizer::RandomizeEnemies(); + OcarinaNotes::GenerateSongList(); Logic::LogicReset(); GetAccessibleLocations({}, SearchMode::ValidateWorld, "", false, false); if (!allLocationsReachable) { diff --git a/source/location_access/locacc_zoras_domain.cpp b/source/location_access/locacc_zoras_domain.cpp index 6b0bea6f..1573b741 100644 --- a/source/location_access/locacc_zoras_domain.cpp +++ b/source/location_access/locacc_zoras_domain.cpp @@ -39,8 +39,9 @@ void AreaTable_Init_ZorasDomain() { LocationAccess(ZR_MAGIC_BEAN_SALESMAN, { [] { return IsChild; } }), LocationAccess(ZR_FROGS_OCARINA_GAME, { [] { - return IsChild && CanPlay(ZeldasLullaby) && CanPlay(SariasSong) && CanPlay(SunsSong) && - CanPlay(EponasSong) && CanPlay(SongOfTime) && CanPlay(SongOfStorms); + return IsChild && OcarinaButtonsCount >= 5 && CanPlay(ZeldasLullaby) && + CanPlay(SariasSong) && CanPlay(SunsSong) && CanPlay(EponasSong) && + CanPlay(SongOfTime) && CanPlay(SongOfStorms); }, /*Glitched*/ [] { diff --git a/source/logic.cpp b/source/logic.cpp index 426d519c..3fe3257f 100644 --- a/source/logic.cpp +++ b/source/logic.cpp @@ -13,6 +13,7 @@ #include "descriptions.hpp" #include "enemizer.hpp" #include "enemizer_logic.hpp" +#include "ocarina_notes.hpp" using namespace Settings; @@ -743,6 +744,8 @@ bool CanDoGlitch(GlitchType glitch, GlitchDifficulty difficulty) { // Updates all logic helpers. Should be called whenever a non-helper is changed void UpdateHelpers() { + using namespace OcarinaNotes; + NumBottles = ((NoBottles) ? 0 : (Bottles + ((DeliverLetter) ? 1 : 0))); HasBottle = NumBottles >= 1; Slingshot = (ProgressiveBulletBag >= 1) && (BuySeed || AmmoCanDrop); @@ -762,18 +765,22 @@ void UpdateHelpers() { BiggoronSword = BiggoronSword || ProgressiveGiantKnife >= 2; OcarinaButtonsCount = OcarinaButtonL + OcarinaButtonR + OcarinaButtonX + OcarinaButtonY + OcarinaButtonA; - ZeldasLullaby = ZeldasLullaby_item && OcarinaButtonX && OcarinaButtonA && OcarinaButtonY; - SariasSong = SariasSong_item && OcarinaButtonR && OcarinaButtonY && OcarinaButtonX; - SunsSong = SunsSong_item && OcarinaButtonY && OcarinaButtonR && OcarinaButtonA; - SongOfStorms = SongOfStorms_item && OcarinaButtonL && OcarinaButtonR && OcarinaButtonA; - EponasSong = EponasSong_item && OcarinaButtonA && OcarinaButtonX && OcarinaButtonY; - SongOfTime = SongOfTime_item && OcarinaButtonY && OcarinaButtonL && OcarinaButtonR; - MinuetOfForest = MinuetOfForest_item && OcarinaButtonL && OcarinaButtonA && OcarinaButtonX && OcarinaButtonY; - BoleroOfFire = BoleroOfFire_item && OcarinaButtonR && OcarinaButtonL && OcarinaButtonY; - SerenadeOfWater = SerenadeOfWater_item && OcarinaButtonL && OcarinaButtonR && OcarinaButtonY && OcarinaButtonX; - RequiemOfSpirit = RequiemOfSpirit_item && OcarinaButtonL && OcarinaButtonR && OcarinaButtonY; - NocturneOfShadow = NocturneOfShadow_item && OcarinaButtonX && OcarinaButtonY && OcarinaButtonL && OcarinaButtonR; - PreludeOfLight = PreludeOfLight_item && OcarinaButtonA && OcarinaButtonY && OcarinaButtonX; + u8 OwnedButtonsMask = OcarinaButtonL << OCARINA_BUTTON_L | OcarinaButtonR << OCARINA_BUTTON_R | + OcarinaButtonX << OCARINA_BUTTON_X | OcarinaButtonY << OCARINA_BUTTON_Y | + OcarinaButtonA << OCARINA_BUTTON_A; + // To consider the song playable, check for the song item and that no required buttons are missing + ZeldasLullaby = ZeldasLullaby_item && !(SongRequiredButtons[OCARINA_SONG_LULLABY] & ~OwnedButtonsMask); + SariasSong = SariasSong_item && !(SongRequiredButtons[OCARINA_SONG_SARIAS] & ~OwnedButtonsMask); + SunsSong = SunsSong_item && !(SongRequiredButtons[OCARINA_SONG_SUNS] & ~OwnedButtonsMask); + SongOfStorms = SongOfStorms_item && !(SongRequiredButtons[OCARINA_SONG_STORMS] & ~OwnedButtonsMask); + EponasSong = EponasSong_item && !(SongRequiredButtons[OCARINA_SONG_EPONAS] & ~OwnedButtonsMask); + SongOfTime = SongOfTime_item && !(SongRequiredButtons[OCARINA_SONG_TIME] & ~OwnedButtonsMask); + MinuetOfForest = MinuetOfForest_item && !(SongRequiredButtons[OCARINA_SONG_MINUET] & ~OwnedButtonsMask); + BoleroOfFire = BoleroOfFire_item && !(SongRequiredButtons[OCARINA_SONG_BOLERO] & ~OwnedButtonsMask); + SerenadeOfWater = SerenadeOfWater_item && !(SongRequiredButtons[OCARINA_SONG_SERENADE] & ~OwnedButtonsMask); + RequiemOfSpirit = RequiemOfSpirit_item && !(SongRequiredButtons[OCARINA_SONG_REQUIEM] & ~OwnedButtonsMask); + NocturneOfShadow = NocturneOfShadow_item && !(SongRequiredButtons[OCARINA_SONG_NOCTURNE] & ~OwnedButtonsMask); + PreludeOfLight = PreludeOfLight_item && !(SongRequiredButtons[OCARINA_SONG_PRELUDE] & ~OwnedButtonsMask); ScarecrowSong = ScarecrowSong || FreeScarecrow || (ChildScarecrow && AdultScarecrow); Scarecrow = Hookshot && CanPlay(ScarecrowSong); diff --git a/source/ocarina_notes.cpp b/source/ocarina_notes.cpp new file mode 100644 index 00000000..94fa7446 --- /dev/null +++ b/source/ocarina_notes.cpp @@ -0,0 +1,98 @@ +#include "ocarina_notes.hpp" +#include "settings.hpp" +#include "random.hpp" +#include + +namespace OcarinaNotes { + +const std::array ButtonNames = { + "L", "R", "Y", "X", "A", +}; +const std::array SongNames = { + "Minuet of Forest", "Bolero of Fire", "Serenade of Water", "Requiem of Spirit", + "Nocturne of Shadow", "Prelude of Light", "Saria's Song", "Epona's Song", + "Zelda's Lullaby", "Sun's Song", "Song of Time", "Song of Storms", +}; + +// clang-format off +static constexpr u8 sVanillaRequiredButtons[OCARINA_SONG_MAX] = { + [OCARINA_SONG_MINUET] = 1 << OCARINA_BUTTON_L | 1 << OCARINA_BUTTON_A | 1 << OCARINA_BUTTON_X | 1 << OCARINA_BUTTON_Y, + [OCARINA_SONG_BOLERO] = 1 << OCARINA_BUTTON_R | 1 << OCARINA_BUTTON_L | 1 << OCARINA_BUTTON_Y, + [OCARINA_SONG_SERENADE] = 1 << OCARINA_BUTTON_L | 1 << OCARINA_BUTTON_R | 1 << OCARINA_BUTTON_Y | 1 << OCARINA_BUTTON_X, + [OCARINA_SONG_REQUIEM] = 1 << OCARINA_BUTTON_L | 1 << OCARINA_BUTTON_R | 1 << OCARINA_BUTTON_Y, + [OCARINA_SONG_NOCTURNE] = 1 << OCARINA_BUTTON_X | 1 << OCARINA_BUTTON_Y | 1 << OCARINA_BUTTON_L | 1 << OCARINA_BUTTON_R, + [OCARINA_SONG_PRELUDE] = 1 << OCARINA_BUTTON_A | 1 << OCARINA_BUTTON_Y | 1 << OCARINA_BUTTON_X, + [OCARINA_SONG_SARIAS] = 1 << OCARINA_BUTTON_R | 1 << OCARINA_BUTTON_Y | 1 << OCARINA_BUTTON_X, + [OCARINA_SONG_EPONAS] = 1 << OCARINA_BUTTON_A | 1 << OCARINA_BUTTON_X | 1 << OCARINA_BUTTON_Y, + [OCARINA_SONG_LULLABY] = 1 << OCARINA_BUTTON_X | 1 << OCARINA_BUTTON_A | 1 << OCARINA_BUTTON_Y, + [OCARINA_SONG_SUNS] = 1 << OCARINA_BUTTON_Y | 1 << OCARINA_BUTTON_R | 1 << OCARINA_BUTTON_A, + [OCARINA_SONG_TIME] = 1 << OCARINA_BUTTON_Y | 1 << OCARINA_BUTTON_L | 1 << OCARINA_BUTTON_R, + [OCARINA_SONG_STORMS] = 1 << OCARINA_BUTTON_L | 1 << OCARINA_BUTTON_R | 1 << OCARINA_BUTTON_A, +}; +// clang-format on + +OcarinaSongButtonSequence SongData[OCARINA_SONG_MAX]; +u8 SongRequiredButtons[OCARINA_SONG_MAX]; +u8 FrogSongNotes[FROG_SONG_LENGTH]; + +static void RandomizeNoteSequence(u8 noteSequence[], u8 songLength) { + for (u32 noteIndex = 0; noteIndex < songLength; noteIndex++) { + noteSequence[noteIndex] = Random(OCARINA_BUTTON_L, OCARINA_BUTTON_MAX); + } +} + +static bool IsValidSong(OcarinaSongButtonSequence song) { + // Check if this song contains or is contained by another song. + for (OcarinaSongButtonSequence otherSong : SongData) { + if (otherSong.length == 0) + break; + + std::string songStr(reinterpret_cast(song.buttons), song.length); + std::string otherSongStr(reinterpret_cast(otherSong.buttons), otherSong.length); + + if (songStr.find(otherSongStr) != std::string::npos || otherSongStr.find(songStr) != std::string::npos) { + return false; + } + } + return true; +} + +void GenerateSongList(void) { + // Reset structs + memset(&SongData, 0, sizeof(SongData)); + memcpy(&SongRequiredButtons, &sVanillaRequiredButtons, sizeof(SongRequiredButtons)); + + if (!Settings::RandomSongNotes) { + return; + } + // Generate random songs + for (u8 songId = OCARINA_SONG_MINUET; songId < OCARINA_SONG_MAX; songId++) { + for (u32 attempts = 0; attempts < 1000; attempts++) { + OcarinaSongButtonSequence randomSong = { 0 }; + if (songId <= OCARINA_SONG_PRELUDE) { + // warp songs: random length between 5 and 8 + randomSong.length = Random(5, 9); + RandomizeNoteSequence(randomSong.buttons, randomSong.length); + } else { + // regular songs: 3 notes repeated twice + RandomizeNoteSequence(randomSong.buttons, 3); + randomSong.length = 6; + randomSong.buttons[3] = randomSong.buttons[0]; + randomSong.buttons[4] = randomSong.buttons[1]; + randomSong.buttons[5] = randomSong.buttons[2]; + } + + if (IsValidSong(randomSong)) { + SongData[songId] = randomSong; + for (u32 i = 0; i < randomSong.length; i++) { + SongRequiredButtons[songId] |= (1 << randomSong.buttons[i]); + } + break; + } + } + } + + RandomizeNoteSequence(FrogSongNotes, FROG_SONG_LENGTH); +} + +} // namespace OcarinaNotes diff --git a/source/ocarina_notes.hpp b/source/ocarina_notes.hpp new file mode 100644 index 00000000..339ee5b4 --- /dev/null +++ b/source/ocarina_notes.hpp @@ -0,0 +1,18 @@ +#include +#include +#include "../code/src/ocarina_notes.h" + +#define FROG_SONG_LENGTH 14 + +namespace OcarinaNotes { + +extern const std::array ButtonNames; +extern const std::array SongNames; + +extern OcarinaSongButtonSequence SongData[OCARINA_SONG_MAX]; +extern u8 SongRequiredButtons[OCARINA_SONG_MAX]; +extern u8 FrogSongNotes[FROG_SONG_LENGTH]; + +void GenerateSongList(void); + +} // namespace OcarinaNotes diff --git a/source/patch.cpp b/source/patch.cpp index 99d17ac2..889e8083 100644 --- a/source/patch.cpp +++ b/source/patch.cpp @@ -11,6 +11,7 @@ #include "gold_skulltulas.hpp" #include "utils.hpp" #include "enemizer.hpp" +#include "ocarina_notes.hpp" #include #include @@ -661,6 +662,36 @@ bool WriteAllPatches() { } } + /*--------------------------------- + | Random Song Notes | + ---------------------------------*/ + + if (Settings::RandomSongNotes) { + using namespace OcarinaNotes; + + // Overwrite the game's gOcarinaSongButtons struct with the randomized songs. + // The other structs that hold song data will be modified at runtime via basepatch code, + // copying from gOcarinaSongButtons. + + const u32 OCARINASONGBUTTONS_ADDR = 0x0054C222; + + patchOffset = V_TO_P(OCARINASONGBUTTONS_ADDR); + patchSize = sizeof(SongData); + + if (!WritePatch(patchOffset, patchSize, (char*)&SongData, code, bytesWritten, totalRW, buf)) { + return false; + } + + const u32 FROGSONGNOTES_ADDR = 0x0054C8B0; + + patchOffset = V_TO_P(FROGSONGNOTES_ADDR); + patchSize = sizeof(FrogSongNotes); + + if (!WritePatch(patchOffset, patchSize, (char*)&FrogSongNotes, code, bytesWritten, totalRW, buf)) { + return false; + } + } + /*------------------------- | EOF | --------------------------*/ diff --git a/source/settings.cpp b/source/settings.cpp index 39ded1ff..00206d29 100644 --- a/source/settings.cpp +++ b/source/settings.cpp @@ -476,6 +476,7 @@ Option HyperEnemies = Option::Bool(2, "Hyper Enemies", {"Off", "On" Option FreeCamera = Option::Bool("Free Camera", {"Off", "On"}, {freeCamDesc}, OptionCategory::Setting, ON); Option RandomGsLocations = Option::Bool("Random GS Locations", {"Off", "On"}, {randomGsLocationsDesc}); Option GsLocGuaranteeNew = Option::Bool(2, "Guarantee New", {"Off", "On"}, {gsLocGuaranteeNewDesc}); +Option RandomSongNotes = Option::Bool("Random Ocarina Melodies",{"Off", "On"}, {randomSongNotesDesc}); std::vector gameplayOptions = { &FastBunnyHood, &KeepFWWarpPoint, @@ -495,6 +496,7 @@ std::vector gameplayOptions = { &FreeCamera, &RandomGsLocations, &GsLocGuaranteeNew, + &RandomSongNotes, }; // Excluded Locations (Individual definitions made in ItemLocation class) @@ -1576,6 +1578,7 @@ SettingsContext FillContext() { ctx.hyperEnemies = (HyperEnemies) ? 1 : 0; ctx.freeCamera = (FreeCamera) ? 1 : 0; ctx.randomGsLocations = (RandomGsLocations) ? 1 : 0; + ctx.randomSongNotes = (RandomSongNotes) ? 1 : 0; ctx.faroresWindAnywhere = (FaroresWindAnywhere) ? 1 : 0; ctx.stickAsAdult = (StickAsAdult) ? 1 : 0; diff --git a/source/settings.hpp b/source/settings.hpp index cf2d7797..a212a0a8 100644 --- a/source/settings.hpp +++ b/source/settings.hpp @@ -504,6 +504,7 @@ extern Option HyperEnemies; extern Option FreeCamera; extern Option RandomGsLocations; extern Option GsLocGuaranteeNew; +extern Option RandomSongNotes; extern bool HasNightStart; extern Option FaroresWindAnywhere; diff --git a/source/spoiler_log.cpp b/source/spoiler_log.cpp index 41fb2774..29ff3a9b 100644 --- a/source/spoiler_log.cpp +++ b/source/spoiler_log.cpp @@ -12,6 +12,7 @@ #include "shops.hpp" #include "gold_skulltulas.hpp" #include "enemizer.hpp" +#include "ocarina_notes.hpp" #include <3ds.h> #include @@ -567,6 +568,43 @@ static void WriteRequiredTrials(tinyxml2::XMLDocument& spoilerLog) { } } +static void InsertSongNode(tinyxml2::XMLElement* parentNode, std::string songName, u8 songLength, u8 notes[]) { + auto node = parentNode->InsertNewChildElement("song"); + node->SetAttribute("name", songName.c_str()); + + constexpr int16_t LONGEST_NAME = 18; // The longest song name. + // Insert a padding so we get a kind of table in the XML document. + int16_t requiredPadding = LONGEST_NAME - songName.length(); + if (requiredPadding >= 0) { + std::string padding(requiredPadding, ' '); + node->SetAttribute("_", padding.c_str()); + } + + std::string noteStr = ""; + for (u32 btnIndex = 0; btnIndex < songLength; btnIndex++) { + noteStr += OcarinaNotes::ButtonNames[notes[btnIndex]] + " "; + } + node->SetText(noteStr.c_str()); +} + +// Writes the randomized notes for each song. +static void WriteSongNotes(tinyxml2::XMLDocument& spoilerLog) { + using namespace OcarinaNotes; + if (!Settings::RandomSongNotes) { + return; + } + + auto parentNode = spoilerLog.NewElement("song-notes"); + + for (u8 songId = OCARINA_SONG_MINUET; songId < OCARINA_SONG_MAX; songId++) { + InsertSongNode(parentNode, SongNames[songId], SongData[songId].length, SongData[songId].buttons); + } + + InsertSongNode(parentNode, "Frog Choir Game", FROG_SONG_LENGTH, FrogSongNotes); + + spoilerLog.RootElement()->InsertEndChild(parentNode); +} + // Writes the area and a description of where any moved Gold Skulltulas are. static void WriteNewGsLocations(tinyxml2::XMLDocument& spoilerLog, const bool collapsible = false) { if (!Settings::RandomGsLocations) { @@ -826,6 +864,7 @@ bool SpoilerLog_Write() { } WriteMasterQuestDungeons(spoilerLog, true); WriteRequiredTrials(spoilerLog); + WriteSongNotes(spoilerLog); WriteNewGsLocations(spoilerLog, true); WritePlaythrough(spoilerLog, true); WriteWayOfTheHeroLocation(spoilerLog, true);