Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 3 additions & 5 deletions code/src/loader.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions code/src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -33,6 +34,7 @@ void Randomizer_Init() {
Entrance_Init();
ItemOverride_Init();
Enemizer_Init();
OcarinaNotes_Init();
extDataInit();
irrstInit();

Expand Down
58 changes: 57 additions & 1 deletion code/src/ocarina_notes.c
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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]);
Expand Down
45 changes: 42 additions & 3 deletions code/src/ocarina_notes.h
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions code/src/settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,7 @@ typedef struct {
u8 hyperEnemies;
u8 freeCamera;
u8 randomGsLocations;
u8 randomSongNotes;

u8 faroresWindAnywhere;
u8 stickAsAdult;
Expand Down
1 change: 1 addition & 0 deletions romfs/spoiler-log.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ excluded-locations,
enabled-tricks,
enabled-glitches,
required-trials,
song-notes,
enemies,
hints {
display: none;
Expand Down
6 changes: 6 additions & 0 deletions source/descriptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 | //
Expand Down
1 change: 1 addition & 0 deletions source/descriptions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 3 additions & 0 deletions source/fill.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "shops.hpp"
#include "debug.hpp"
#include "enemizer.hpp"
#include "ocarina_notes.hpp"

#include <vector>
#include <unistd.h>
Expand Down Expand Up @@ -961,6 +962,7 @@ void VanillaFill() {
Location(loc)->PlaceVanillaItem();
}
Enemizer::RandomizeEnemies();
OcarinaNotes::GenerateSongList();
// If necessary, handle ER stuff
playthroughEntrances.clear();
if (ShuffleEntrances) {
Expand Down Expand Up @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions source/location_access/locacc_zoras_domain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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*/
[] {
Expand Down
31 changes: 19 additions & 12 deletions source/logic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "descriptions.hpp"
#include "enemizer.hpp"
#include "enemizer_logic.hpp"
#include "ocarina_notes.hpp"

using namespace Settings;

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
98 changes: 98 additions & 0 deletions source/ocarina_notes.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#include "ocarina_notes.hpp"
#include "settings.hpp"
#include "random.hpp"
#include <string.h>

namespace OcarinaNotes {

const std::array<std::string, OCARINA_BUTTON_MAX> ButtonNames = {
"L", "R", "Y", "X", "A",
};
const std::array<std::string, OCARINA_SONG_MAX> 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<char*>(song.buttons), song.length);
std::string otherSongStr(reinterpret_cast<char*>(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
Loading