From e7185d889e924696f6f7fd91afa03528f673bc4c Mon Sep 17 00:00:00 2001
From: Wiseguy <68165316+Mr-Wiseguy@users.noreply.github.com>
Date: Tue, 30 Dec 2025 21:41:30 -0500
Subject: [PATCH] Note saving (#30)
* WIP note saving implementation
* Fix note saving affecting other files
* Prevent note collection in demo playback for safety
* Cache note saving enabled while in the lair or file select to prevent it changing in levels with notes
* Prevent "Grunty's magic" note dialog from being shown if note saving is enabled
* Implement dynamically spawned note saving
* Properly clear loaded save extension data when score status is cleared
* Hook up menu for note saving
---
assets/config_menu/general.rml | 41 ++-
include/banjo_config.h | 14 +
patches/graphics_patches.c | 4 +
patches/init_patches.c | 25 +-
patches/load_patches.c | 7 +-
patches/marker_extension_patches.c | 8 +
patches/misc_funcs.h | 1 +
patches/note_saving.c | 462 +++++++++++++++++++++++++++++
patches/note_saving.h | 17 ++
patches/patches.h | 5 +
patches/prop_extension_patches.c | 18 +-
patches/save_extension.h | 24 ++
patches/save_extensions.c | 169 +++++++++++
patches/syms.ld | 4 +-
src/game/config.cpp | 2 +
src/game/recomp_api.cpp | 4 +
src/main/main.cpp | 3 +-
src/ui/ui_config.cpp | 13 +
18 files changed, 801 insertions(+), 20 deletions(-)
create mode 100644 patches/note_saving.c
create mode 100644 patches/note_saving.h
create mode 100644 patches/save_extension.h
create mode 100644 patches/save_extensions.c
diff --git a/assets/config_menu/general.rml b/assets/config_menu/general.rml
index 671f747..ccbf236 100644
--- a/assets/config_menu/general.rml
+++ b/assets/config_menu/general.rml
@@ -208,7 +208,7 @@
data-checked="analog_camera_invert_mode"
value="InvertNone"
id="analog_camera_inversion_none"
- style="nav-up: #analog_cam_enabled;"
+ style="nav-up: #analog_cam_enabled; nav-down: #note_saving_enabled"
/>
@@ -220,7 +220,7 @@
data-checked="analog_camera_invert_mode"
value="InvertX"
id="analog_camera_inversion_x"
- style="nav-up: #analog_cam_disabled;"
+ style="nav-up: #analog_cam_disabled; nav-down: #note_saving_disabled"
/>
@@ -232,7 +232,7 @@
data-checked="analog_camera_invert_mode"
value="InvertY"
id="analog_camera_inversion_y"
- style="nav-up: #analog_cam_disabled;"
+ style="nav-up: #analog_cam_disabled; nav-down: #note_saving_disabled"
/>
@@ -244,11 +244,41 @@
data-checked="analog_camera_invert_mode"
value="InvertBoth"
id="analog_camera_inversion_both"
- style="nav-up: #analog_cam_disabled;"
+ style="nav-up: #analog_cam_disabled; nav-down: #note_saving_disabled"
/>
+
+
+
@@ -290,6 +320,9 @@
Inverts the camera controls for the analog camera if it's enabled. None is the default.
+
+ Note saving DESCRIPTION TODO.
+
diff --git a/include/banjo_config.h b/include/banjo_config.h
index b9d7ac5..3e8057c 100644
--- a/include/banjo_config.h
+++ b/include/banjo_config.h
@@ -59,6 +59,20 @@ namespace banjo {
AnalogCamMode get_analog_cam_mode();
void set_analog_cam_mode(AnalogCamMode mode);
+ enum class NoteSavingMode {
+ On,
+ Off,
+ OptionCount
+ };
+
+ NLOHMANN_JSON_SERIALIZE_ENUM(banjo::NoteSavingMode, {
+ {banjo::NoteSavingMode::On, "On"},
+ {banjo::NoteSavingMode::Off, "Off"}
+ });
+
+ NoteSavingMode get_note_saving_mode();
+ void set_note_saving_mode(NoteSavingMode mode);
+
void open_quit_game_prompt();
};
diff --git a/patches/graphics_patches.c b/patches/graphics_patches.c
index ac54439..179ce97 100644
--- a/patches/graphics_patches.c
+++ b/patches/graphics_patches.c
@@ -1,6 +1,7 @@
#include "patches.h"
#include "ultra_extensions.h"
#include "misc_funcs.h"
+#include "note_saving.h"
#include "core1/core1.h"
#include "core1/vimgr.h"
@@ -94,6 +95,9 @@ RECOMP_PATCH void game_draw(s32 arg0){
// @recomp Advance the frame used as reference by the dynamic camera target changes for analog camera.
recomp_advance_dynamic_camera_targets();
+ // @recomp Update note saving state.
+ note_saving_update();
+
// @recomp Track the original values.
Mtx* mtx_start = mtx;
Vtx* vtx_start = vtx;
diff --git a/patches/init_patches.c b/patches/init_patches.c
index f67deab..6e40cac 100644
--- a/patches/init_patches.c
+++ b/patches/init_patches.c
@@ -1,24 +1,25 @@
#include "patches.h"
+#include "note_saving.h"
#include "transform_ids.h"
#include "bk_api.h"
#include "misc_funcs.h"
+#include "core1/core1.h"
+#include "core1/main.h"
+#include "core1/vimgr.h"
+
RECOMP_DECLARE_EVENT(recomp_on_init());
-void core1_init();
-void sns_write_payload_over_heap();
-void mainLoop();
+// @recomp Patched to perform some initialization after core2 has been loaded.
+RECOMP_PATCH void dummy_func_8025AFB0(void) {
+ // @recomp Initialize note saving data before the init event is run.
+ // This will allow mods to override it as necessary.
+ init_note_saving();
-RECOMP_PATCH void mainThread_entry(void *arg) {
// @recomp Register actor extension data and call the init event.
recomp_on_init();
- core1_init();
- sns_write_payload_over_heap();
-
- while (1) {
- // @recomp Reset the tracked projection IDs.
- reset_projection_ids();
- mainLoop();
- }
+ // @recomp Calculate the note start indices for each map after the init event.
+ // This allows the start indices to account for any changes made by mods.
+ calculate_map_start_note_indices();
}
diff --git a/patches/load_patches.c b/patches/load_patches.c
index 869fd52..679e5d0 100644
--- a/patches/load_patches.c
+++ b/patches/load_patches.c
@@ -3,6 +3,7 @@
#include "functions.h"
#include "bk_api.h"
#include "object_extension_funcs.h"
+#include "note_saving.h"
extern ActorMarker *D_8036E7C8;
extern u8 D_80383428[0x1C];
@@ -96,6 +97,7 @@ void hotpatch_intro_opa_map_model(BKModelBin* model_bin) {
// This includes:
// * Resetting all extended marker data and skip interpolation for the next frame.
// * Hotpatching the map model for the title cutscene to fix ultrawide effects.
+// * Resetting the spawned static note count.
RECOMP_PATCH void func_803329AC(void){
s32 i;
@@ -122,4 +124,7 @@ RECOMP_PATCH void func_803329AC(void){
// as the marker ID tracking gets reset here.
recomp_clear_all_object_data(EXTENSION_TYPE_MARKER);
set_all_interpolation_skipped(TRUE);
-}
\ No newline at end of file
+
+ // @recomp Run note saving map load code.
+ note_saving_on_map_load();
+}
diff --git a/patches/marker_extension_patches.c b/patches/marker_extension_patches.c
index 8ca6198..b9150ab 100644
--- a/patches/marker_extension_patches.c
+++ b/patches/marker_extension_patches.c
@@ -4,6 +4,7 @@
#include "functions.h"
#include "object_extension_funcs.h"
#include "bk_api.h"
+#include "note_saving.h"
// Array of handles for ActorMarker instances.
// Normally the game only has at most 0xE0 ActorMarker instances, but this is larger to account for mods increasing
@@ -15,6 +16,8 @@ extern u8 D_80383428[0x1C];
void func_8032F3D4(s32 arg0[3], ActorMarker *marker, s32 arg2);
ActorMarker * func_80332A60(void);
+extern Actor *suLastBaddie;
+
// @recomp Patched to create extension data for the marker.
RECOMP_PATCH ActorMarker * marker_init(s32 *pos, MarkerDrawFunc draw_func, int arg2, int marker_id, int arg4){
ActorMarker * marker = func_80332A60();
@@ -64,6 +67,11 @@ RECOMP_PATCH ActorMarker * marker_init(s32 *pos, MarkerDrawFunc draw_func, int a
// @recomp Set the marker's handle.
u32 index = marker - D_8036E7C8;
marker_handles[index] = recomp_create_object_data(EXTENSION_TYPE_MARKER, marker_id);
+
+ // @recomp If this is a note marker, initialize it. Make sure that this marker belongs to suLastBaddie.
+ if (marker->id == MARKER_5F_MUSIC_NOTE && suLastBaddie && suLastBaddie->actor_info->markerId == marker_id) {
+ note_saving_handle_dynamic_note(suLastBaddie, marker);
+ }
return marker;
}
diff --git a/patches/misc_funcs.h b/patches/misc_funcs.h
index ed305df..2d9ffb3 100644
--- a/patches/misc_funcs.h
+++ b/patches/misc_funcs.h
@@ -8,5 +8,6 @@ DECLARE_FUNC(void, recomp_puts, const char* data, u32 size);
DECLARE_FUNC(void, recomp_exit);
DECLARE_FUNC(void, recomp_error, const char* str);
DECLARE_FUNC(u64, recomp_xxh3, void* data, u32 size);
+DECLARE_FUNC(s32, recomp_get_note_saving_enabled);
#endif
diff --git a/patches/note_saving.c b/patches/note_saving.c
new file mode 100644
index 0000000..89fc110
--- /dev/null
+++ b/patches/note_saving.c
@@ -0,0 +1,462 @@
+#include "patches.h"
+#include "bk_api.h"
+#include "misc_funcs.h"
+#include "save_extension.h"
+#include "note_saving.h"
+
+#include "functions.h"
+#include "prop.h"
+#include "core2/timedfunc.h"
+
+// Vanilla declarations.
+typedef struct map_info{
+ s16 map_id;
+ s16 level_id;
+ char* name;
+}MapInfo;
+
+extern struct {
+ s32 unk0;
+ s32 unk4;
+ u8 unk8[0x25];
+} gFileProgressFlags;
+
+MapInfo * func_8030AD00(enum map_e map_id);
+enum map_e map_get(void);
+enum level_e level_get(void);
+void progressDialog_showDialogMaskZero(s32);
+void fxSparkle_musicNote(s16 position[3]);
+void item_inc(enum item_e item);
+void item_adjustByDiffWithoutHud(enum item_e item, s32 diff);
+bool func_802FADD4(enum item_e item_id);
+s32 item_getCount(enum item_e item);
+s32 jiggyscore_leveltotal(s32 lvl);
+void itemPrint_reset(void);
+s32 bitfield_get_bit(u8 *array, s32 index);
+
+extern s32 D_80385F30[0x2C];
+extern s32 D_80385FE8;
+
+extern struct {
+ Cube *cubes;
+ f32 margin;
+ s32 min[3];
+ s32 max[3];
+ s32 stride[2];
+ s32 cubeCnt;
+ s32 unk2C;
+ s32 width[3];
+ Cube *unk3C; // fallback cube?
+ Cube *unk40; // some other fallback cube?
+ s32 unk44; // index of some sort
+} sCubeList;
+
+// New declarations.
+typedef struct {
+ u16 static_note_count;
+ u16 dynamic_note_count;
+ u16 start_note_index;
+ u16 level_id;
+} MapNoteData;
+
+MapNoteData map_note_data[512]; // One entry per map, with room for extras in case any mods add additional maps.
+u16 level_note_counts[256]; // One entry per level, with room for extras in case any mods add additional levels.
+
+typedef struct {
+ u32 note_index;
+} NoteSavingExtensionData;
+
+PropExtensionId note_saving_prop_extension_id;
+
+// Note saving can only savely be changed while in the lair, so this value is only updated when in the lair.
+bool note_saving_enabled_cached = FALSE;
+// Override for allowing mods to disable note saving.
+bool note_saving_override_disabled = FALSE;
+
+u32 spawned_static_note_count = 0;
+
+// Per-map values containing the number of dynamic notes to despawn for the current session.
+// This is calculated when a new level is entered for every map in the level.
+u8 map_dynamic_note_despawn_counts[512];
+
+bool recomp_in_demo_playback_game_mode();
+
+void init_note_saving() {
+ note_saving_prop_extension_id = bkrecomp_extend_prop_all(sizeof(NoteSavingExtensionData));
+
+ // Collected from map data.
+ map_note_data[MAP_2_MM_MUMBOS_MOUNTAIN].static_note_count = 85;
+ map_note_data[MAP_2_MM_MUMBOS_MOUNTAIN].dynamic_note_count = 5;
+ map_note_data[MAP_5_TTC_BLUBBERS_SHIP].static_note_count = 8;
+ map_note_data[MAP_6_TTC_NIPPERS_SHELL].static_note_count = 6;
+ map_note_data[MAP_7_TTC_TREASURE_TROVE_COVE].static_note_count = 82;
+ map_note_data[MAP_A_TTC_SANDCASTLE].static_note_count = 4;
+ map_note_data[MAP_B_CC_CLANKERS_CAVERN].static_note_count = 72;
+ map_note_data[MAP_C_MM_TICKERS_TOWER].static_note_count = 6;
+ map_note_data[MAP_D_BGS_BUBBLEGLOOP_SWAMP].static_note_count = 83;
+ map_note_data[MAP_D_BGS_BUBBLEGLOOP_SWAMP].dynamic_note_count = 5;
+ map_note_data[MAP_E_MM_MUMBOS_SKULL].static_note_count = 4;
+ map_note_data[MAP_10_BGS_MR_VILE].static_note_count = 6;
+ map_note_data[MAP_11_BGS_TIPTUP].static_note_count = 6;
+ map_note_data[MAP_12_GV_GOBIS_VALLEY].static_note_count = 70;
+ map_note_data[MAP_13_GV_MEMORY_GAME].static_note_count = 4;
+ map_note_data[MAP_14_GV_SANDYBUTTS_MAZE].static_note_count = 7;
+ map_note_data[MAP_15_GV_WATER_PYRAMID].static_note_count = 4;
+ map_note_data[MAP_16_GV_RUBEES_CHAMBER].static_note_count = 8;
+ map_note_data[MAP_1A_GV_INSIDE_JINXY].static_note_count = 7;
+ map_note_data[MAP_1B_MMM_MAD_MONSTER_MANSION].static_note_count = 47;
+ map_note_data[MAP_1C_MMM_CHURCH].static_note_count = 10;
+ map_note_data[MAP_1D_MMM_CELLAR].static_note_count = 4;
+ map_note_data[MAP_21_CC_WITCH_SWITCH_ROOM].static_note_count = 6;
+ map_note_data[MAP_22_CC_INSIDE_CLANKER].static_note_count = 16;
+ map_note_data[MAP_23_CC_GOLDFEATHER_ROOM].static_note_count = 6;
+ map_note_data[MAP_24_MMM_TUMBLARS_SHED].static_note_count = 4;
+ map_note_data[MAP_25_MMM_WELL].static_note_count = 7;
+ map_note_data[MAP_26_MMM_NAPPERS_ROOM].static_note_count = 8;
+ map_note_data[MAP_27_FP_FREEZEEZY_PEAK].static_note_count = 82;
+ map_note_data[MAP_29_MMM_NOTE_ROOM].static_note_count = 9;
+ map_note_data[MAP_2D_MMM_BEDROOM].static_note_count = 4;
+ map_note_data[MAP_2F_MMM_WATERDRAIN_BARREL].static_note_count = 5;
+ map_note_data[MAP_30_MMM_MUMBOS_SKULL].static_note_count = 2;
+ map_note_data[MAP_31_RBB_RUSTY_BUCKET_BAY].static_note_count = 43;
+ map_note_data[MAP_34_RBB_ENGINE_ROOM].static_note_count = 16;
+ map_note_data[MAP_35_RBB_WAREHOUSE].static_note_count = 4;
+ map_note_data[MAP_37_RBB_CONTAINER_1].static_note_count = 8;
+ map_note_data[MAP_38_RBB_CONTAINER_3].static_note_count = 4;
+ map_note_data[MAP_39_RBB_CREW_CABIN].static_note_count = 4;
+ map_note_data[MAP_3B_RBB_STORAGE_ROOM].static_note_count = 5;
+ map_note_data[MAP_3C_RBB_KITCHEN].static_note_count = 5;
+ map_note_data[MAP_3D_RBB_NAVIGATION_ROOM].static_note_count = 4;
+ map_note_data[MAP_3F_RBB_CAPTAINS_CABIN].static_note_count = 3;
+ map_note_data[MAP_40_CCW_HUB].static_note_count = 4;
+ map_note_data[MAP_43_CCW_SPRING].static_note_count = 16;
+ map_note_data[MAP_44_CCW_SUMMER].static_note_count = 16;
+ map_note_data[MAP_45_CCW_AUTUMN].static_note_count = 37;
+ map_note_data[MAP_46_CCW_WINTER].static_note_count = 16;
+ map_note_data[MAP_48_FP_MUMBOS_SKULL].static_note_count = 6;
+ map_note_data[MAP_4C_CCW_AUTUMN_MUMBOS_SKULL].static_note_count = 4;
+ map_note_data[MAP_53_FP_CHRISTMAS_TREE].static_note_count = 12;
+ map_note_data[MAP_5C_CCW_AUTUMN_ZUBBA_HIVE].static_note_count = 4;
+ map_note_data[MAP_60_CCW_AUTUMN_NABNUTS_HOUSE].static_note_count = 3;
+ map_note_data[MAP_8B_RBB_ANCHOR_ROOM].static_note_count = 4;
+}
+
+RECOMP_EXPORT void bkrecomp_notesaving_clear_all_map_note_counts() {
+ for (u32 i = 0; i < ARRLEN(map_note_data); i++) {
+ map_note_data[i].static_note_count = 0;
+ map_note_data[i].dynamic_note_count = 0;
+ }
+}
+
+RECOMP_EXPORT void bkrecomp_notesaving_set_map_static_note_count(u32 map_id, u16 static_note_count) {
+ if (map_id >= ARRLEN(map_note_data)) {
+ recomp_error("Mod error: Attempted to set static note count of an invalid map ID\n");
+ }
+ map_note_data[map_id].static_note_count = static_note_count;
+}
+
+RECOMP_EXPORT void bkrecomp_notesaving_set_map_dynamic_note_count(u32 map_id, u16 dynamic_note_count) {
+ if (map_id >= ARRLEN(map_note_data)) {
+ recomp_error("Mod error: Attempted to set dynamic note count of an invalid map ID\n");
+ }
+ map_note_data[map_id].dynamic_note_count = dynamic_note_count;
+}
+
+RECOMP_EXPORT void bkrecomp_notesaving_force_disabled(bool disabled) {
+ note_saving_override_disabled = disabled;
+}
+
+// Notes are always saved, but this function controls whether to use the saved data to prevent notes from spawning and adjust the note score.
+RECOMP_EXPORT s32 bkrecomp_note_saving_enabled() {
+ return recomp_get_note_saving_enabled();
+}
+
+void calculate_map_start_note_indices() {
+ for (u32 map_id = 0; map_id < ARRLEN(map_note_data); map_id++) {
+ MapNoteData* note_data = &map_note_data[map_id];
+ MapInfo* info = func_8030AD00(map_id);
+ if (info != NULL) {
+ note_data->level_id = info->level_id;
+ note_data->start_note_index = level_note_counts[info->level_id];
+ level_note_counts[info->level_id] += note_data->static_note_count + note_data->dynamic_note_count;
+ }
+ else {
+ note_data->level_id = 0;
+ }
+ }
+}
+
+s32 level_id_to_level_array_index(enum level_e level_id) {
+ switch (level_id) {
+ case LEVEL_1_MUMBOS_MOUNTAIN:
+ return 0;
+ case LEVEL_2_TREASURE_TROVE_COVE:
+ return 1;
+ case LEVEL_3_CLANKERS_CAVERN:
+ return 2;
+ case LEVEL_4_BUBBLEGLOOP_SWAMP:
+ return 3;
+ case LEVEL_5_FREEZEEZY_PEAK:
+ return 4;
+ case LEVEL_7_GOBIS_VALLEY:
+ return 5;
+ case LEVEL_A_MAD_MONSTER_MANSION:
+ return 6;
+ case LEVEL_9_RUSTY_BUCKET_BAY:
+ return 7;
+ case LEVEL_8_CLICK_CLOCK_WOOD:
+ return 8;
+ default:
+ return -1;
+ }
+}
+
+bool is_note_collected(enum map_e map_id, enum level_e level_id, u8 note_index) {
+ if (map_id >= ARRLEN(map_note_data)) {
+ return FALSE;
+ }
+
+ s32 level_array_index = level_id_to_level_array_index(level_id);
+ if (level_array_index == -1 || level_array_index >= ARRLEN(loaded_file_extension_data.level_notes)) {
+ return FALSE;
+ }
+
+ MapNoteData *note_data = &map_note_data[map_id];
+ note_index += note_data->start_note_index;
+
+ u32 byte_index = note_index / 8;
+ u32 bit_index = note_index % 8;
+
+ return (loaded_file_extension_data.level_notes[level_array_index].bytes[byte_index] & (1 << bit_index)) != 0;
+}
+
+void set_note_collected(enum map_e map_id, enum level_e level_id, u8 note_index) {
+ if (map_id >= ARRLEN(map_note_data)) {
+ return;
+ }
+
+ s32 level_array_index = level_id_to_level_array_index(level_id);
+ if (level_array_index == -1 || level_array_index >= ARRLEN(loaded_file_extension_data.level_notes)) {
+ return;
+ }
+
+ MapNoteData *note_data = &map_note_data[map_id];
+ note_index += note_data->start_note_index;
+
+ u32 byte_index = note_index / 8;
+ u32 bit_index = note_index % 8;
+
+ loaded_file_extension_data.level_notes[level_array_index].bytes[byte_index] |= (1 << bit_index);
+}
+
+void collect_dynamic_note(enum map_e map_id, enum level_e level_id) {
+ if (map_id < ARRLEN(map_note_data)) {
+ MapNoteData *map_data = &map_note_data[map_id];
+ s32 start_note_index = map_data->static_note_count + map_data->start_note_index;
+ s32 map_dynamic_note_count = map_data->dynamic_note_count;
+
+ // Set the first unset dynamic note bit for this map.
+ for (s32 i = 0; i < map_dynamic_note_count; i++) {
+ s32 note_index = start_note_index + i;
+ if (!is_note_collected(map_id, level_id, note_index)) {
+ set_note_collected(map_id, level_id, note_index);
+ break;
+ }
+ }
+ }
+}
+
+s32 dynamic_note_collected_count(enum map_e map_id) {
+ s32 ret = 0;
+ if (map_id < ARRLEN(map_note_data)) {
+ MapNoteData *map_data = &map_note_data[map_id];
+ s32 start_note_index = map_data->static_note_count + map_data->start_note_index;
+ s32 map_dynamic_note_count = map_data->dynamic_note_count;
+
+ // Check the dynamic note bits for this map.
+ for (s32 i = 0; i < map_dynamic_note_count; i++) {
+ s32 note_index = start_note_index + i;
+ if (is_note_collected(map_id, map_data->level_id, note_index)) {
+ ret++;
+ }
+ }
+ }
+ return ret;
+}
+
+void note_saving_on_map_load() {
+ spawned_static_note_count = 0;
+
+ // Prevent the note score passed dialog from running if note saving is enabled.
+ if (note_saving_enabled_cached) {
+ // This flag controls whether Bottles will tell you when you pass your note score.
+ levelSpecificFlags_set(LEVEL_FLAG_34_UNKNOWN, TRUE);
+ }
+}
+
+void note_saving_update() {
+ // When in the lair or file select, update the cached note saving enabled state.
+ if (level_get() == LEVEL_6_LAIR || map_get() == MAP_91_FILE_SELECT) {
+ if (note_saving_override_disabled) {
+ note_saving_enabled_cached = FALSE;
+ }
+ else {
+ note_saving_enabled_cached = bkrecomp_note_saving_enabled();
+ }
+ }
+}
+
+void note_saving_handle_static_note(Cube *c, Prop *p) {
+
+ // If note saving is enabled, check if this note has been collected and remove it if so.
+ if (note_saving_enabled_cached) {
+ if (is_note_collected(map_get(), level_get(), spawned_static_note_count)) {
+ // Clear the note's alive bit.
+ p->spriteProp.unk8_4 = FALSE;
+ }
+ }
+
+ NoteSavingExtensionData* note_data = (NoteSavingExtensionData*)bkrecomp_get_extended_prop_data(c, p, note_saving_prop_extension_id);
+ note_data->note_index = spawned_static_note_count;
+
+ spawned_static_note_count++;
+}
+
+void note_saving_handle_dynamic_note(Actor *actor, ActorMarker *marker) {
+ if (note_saving_enabled_cached) {
+ s32 map_id = map_get();
+ if (map_id < ARRLEN(map_dynamic_note_despawn_counts)) {
+ if (map_dynamic_note_despawn_counts[map_id] > 0) {
+ map_dynamic_note_despawn_counts[map_id]--;
+ // Clear the note's alive bit so it doesn't draw for good measure.
+ marker->propPtr->unk8_4 = FALSE;
+ // Set the actor as despawned.
+ actor->despawn_flag = TRUE;
+ }
+ }
+ }
+}
+
+bool prop_in_cube(Cube *c, Prop *p) {
+ s32 prop_index = p - c->prop2Ptr;
+ if (prop_index >= 0 && prop_index < c->prop2Cnt) {
+ return TRUE;
+ }
+ return FALSE;
+}
+
+Cube *find_cube_for_prop(Prop *p) {
+ for (s32 i = 0; i < sCubeList.cubeCnt; i++) {
+ Cube *cur_cube = &sCubeList.cubes[i];
+ if (prop_in_cube(cur_cube, p)) {
+ return cur_cube;
+ }
+ }
+
+ if (prop_in_cube(sCubeList.unk3C, p)) {
+ return sCubeList.unk3C;
+ }
+
+ if (prop_in_cube(sCubeList.unk40, p)) {
+ return sCubeList.unk40;
+ }
+
+ return NULL;
+}
+
+// @recomp Patched to track collected notes.
+RECOMP_PATCH void __baMarker_resolveMusicNoteCollision(Prop *arg0) {
+ // @recomp Set that the note was collected if this isn't demo playback.
+ if (!recomp_in_demo_playback_game_mode()) {
+ // Check if this is an actor prop and collect a dynamic note if so.
+ if (arg0->is_actor) {
+ collect_dynamic_note(map_get(), level_get());
+ }
+ // Otherwise, make sure this is a sprite prop and use the prop data.
+ else if (!arg0->is_3d) {
+ Cube *prop_cube = find_cube_for_prop(arg0);
+ if (prop_cube != NULL) {
+ NoteSavingExtensionData* note_data = (NoteSavingExtensionData*)bkrecomp_get_extended_prop_data(prop_cube, arg0, note_saving_prop_extension_id);
+ set_note_collected(map_get(), level_get(), note_data->note_index);
+ }
+ }
+ }
+
+ if (!func_802FADD4(ITEM_1B_VILE_VILE_SCORE)) {
+ item_inc(ITEM_C_NOTE);
+ } else {
+ item_adjustByDiffWithoutHud(ITEM_C_NOTE, 1);
+ }
+ if (item_getCount(ITEM_C_NOTE) < 100) {
+ coMusicPlayer_playMusic(COMUSIC_9_NOTE_COLLECTED, 16000);
+ timedFunc_set_1(0.75f, progressDialog_showDialogMaskZero, FILEPROG_3_MUSIC_NOTE_TEXT);
+ }
+ fxSparkle_musicNote(arg0->unk4);
+}
+
+s32 get_collected_note_count(enum level_e level) {
+ s32 level_array_index = level_id_to_level_array_index(level);
+ if (level_array_index == -1) {
+ return 0;
+ }
+
+ s32 count = 0;
+ for (int i = 0; i < ARRLEN(loaded_file_extension_data.level_notes[0].bytes); i++) {
+ u8 cur_byte = loaded_file_extension_data.level_notes[level_array_index].bytes[i];
+ count += __builtin_popcount(cur_byte);
+ }
+
+ return count;
+}
+
+// @recomp Patched to restore the saved note count when entering a level and reset the per-map collected dynamic note counts.
+RECOMP_PATCH void itemscore_levelReset(enum level_e level){
+ int i;
+
+ for(i = 0; i < 6; i++){
+ D_80385F30[ITEM_0_HOURGLASS_TIMER + i] = 0;
+ D_80385F30[ITEM_6_HOURGLASS + i] = 0;
+ }
+
+ D_80385F30[ITEM_C_NOTE] = 0;
+ D_80385F30[ITEM_E_JIGGY] = jiggyscore_leveltotal(level);
+ D_80385F30[ITEM_12_JINJOS] = 0;
+ D_80385F30[ITEM_17_AIR] = 3600;
+ D_80385F30[ITEM_18_GOLD_BULLIONS] = 0;
+ D_80385F30[ITEM_19_ORANGE] = 0;
+ D_80385F30[ITEM_23_ACORNS] = 0;
+ D_80385F30[ITEM_1A_PLAYER_VILE_SCORE] = 0;
+ D_80385F30[ITEM_1B_VILE_VILE_SCORE] = 0;
+ D_80385F30[ITEM_1F_GREEN_PRESENT] = 0;
+ D_80385F30[ITEM_20_BLUE_PRESENT] = 0;
+ D_80385F30[ITEM_21_RED_PRESENT] = 0;
+ D_80385F30[ITEM_22_CATERPILLAR] = 0;
+ itemPrint_reset();
+ D_80385FE8 = 1;
+
+ // @recomp If note saving is currently enabled, set load the note count for the current level.
+ if (note_saving_enabled_cached) {
+ D_80385F30[ITEM_C_NOTE] = get_collected_note_count(level);
+ }
+
+ // @recomp Set the number of dynamic notes to respawn for each map in the level.
+ for (s32 map_id = 0; map_id < ARRLEN(map_note_data); map_id++) {
+ MapNoteData *map_data = &map_note_data[map_id];
+ if (map_data->level_id == level) {
+ map_dynamic_note_despawn_counts[map_id] = dynamic_note_collected_count(map_id);
+ }
+ else {
+ map_dynamic_note_despawn_counts[map_id] = 0;
+ }
+ }
+}
+
+// @recomp Patched to return true for FILEPROG_99_PAST_50_NOTE_DOOR_TEXT if note saving is enabled.
+// That flag controls whether to show the "Grunty's magic stops you from taking the notes..." dialog.
+RECOMP_PATCH bool fileProgressFlag_get(enum file_progress_e index) {
+ if (note_saving_enabled_cached && index == FILEPROG_99_PAST_50_NOTE_DOOR_TEXT) {
+ return TRUE;
+ }
+
+ return bitfield_get_bit(gFileProgressFlags.unk8, index);
+}
diff --git a/patches/note_saving.h b/patches/note_saving.h
new file mode 100644
index 0000000..ff9c974
--- /dev/null
+++ b/patches/note_saving.h
@@ -0,0 +1,17 @@
+#ifndef __NOTE_SAVING_H__
+#define __NOTE_SAVING_H__
+
+#include "prop.h"
+
+void init_note_saving();
+void calculate_map_start_note_indices();
+
+// Notes are always saved, but this function controls whether to use the saved data to prevent notes from spawning and adjust the note score.
+bool note_saving_enabled();
+
+void note_saving_on_map_load();
+void note_saving_update();
+void note_saving_handle_static_note(Cube *c, Prop *p);
+void note_saving_handle_dynamic_note(Actor *actor, ActorMarker *marker);
+
+#endif
diff --git a/patches/patches.h b/patches/patches.h
index 4b636ba..cb6fa38 100644
--- a/patches/patches.h
+++ b/patches/patches.h
@@ -20,6 +20,7 @@
#define osCreateMesgQueue osCreateMesgQueue_recomp
void osWriteBackDCacheAll(void);
#define bzero bzero_recomp
+#define bcopy bcopy_recomp
#define osDpSetStatus osDpSetStatus_recomp
#define malloc malloc_recomp
#define free free_recomp
@@ -40,6 +41,10 @@ void osWriteBackDCacheAll(void);
#pragma GCC diagnostic pop
#include "rt64_extended_gbi.h"
+#ifndef ARRLEN
+# define ARRLEN(x) ((s32)(sizeof(x) / sizeof(x[0])))
+#endif
+
#ifndef gEXFillRectangle
#define gEXFillRectangle(cmd, lorigin, rorigin, ulx, uly, lrx, lry) \
G_EX_COMMAND2(cmd, \
diff --git a/patches/prop_extension_patches.c b/patches/prop_extension_patches.c
index 4be27c1..7850945 100644
--- a/patches/prop_extension_patches.c
+++ b/patches/prop_extension_patches.c
@@ -6,6 +6,7 @@
#include "bk_api.h"
#include "core2/coords.h"
#include "core2/file.h"
+#include "note_saving.h"
// Max props per cube, limited by cube->prop2Cnt which is only 6 bits.
#define CUBE_MAX_PROPS 63
@@ -32,6 +33,7 @@ s32 func_803058C0(f32 arg0);
void code_A5BC0_initCubePropActorProp(Cube*);
void func_80332B2C(ActorMarker * arg0);
void bitfield_free(s32 *arg0);
+void func_8032DE78(SpriteProp *sprite_prop, enum asset_e *sprite_id_ptr);
extern struct {
Cube *cubes;
@@ -178,10 +180,24 @@ RECOMP_PATCH void __code7AF80_initCubeFromFile(Cube *cube, File* file_ptr) {
}
}
- // @recomp Initialize prop handles after loading the cube.
+ // @recomp Initialize prop handles after loading the cube and handle note saving.
for (u32 i = 0; i < cube->prop2Cnt; i++) {
// TODO prop subtypes.
cube_handle->prop_handles[i] = recomp_create_object_data(EXTENSION_TYPE_PROP, 0);
+
+ // Check if this prop is a sprite prop.
+ Prop *prop = &cube->prop2Ptr[i];
+ if (!prop->is_3d && !prop->is_actor) {
+ // Check if this sprite prop is a musical note.
+
+ // Get the asset ID for this sprite prop.
+ enum asset_e sprite_prop_asset_id;
+ func_8032DE78(&prop->spriteProp, &sprite_prop_asset_id);
+
+ if (sprite_prop_asset_id == ASSET_6D6_MODEL_MUSIC_NOTE) {
+ note_saving_handle_static_note(cube, prop);
+ }
+ }
}
}
diff --git a/patches/save_extension.h b/patches/save_extension.h
new file mode 100644
index 0000000..51921cb
--- /dev/null
+++ b/patches/save_extension.h
@@ -0,0 +1,24 @@
+#ifndef __SAVE_EXTENSION_H__
+#define __SAVE_EXTENSION_H__
+
+typedef struct {
+ u8 bytes[32]; // 32*8 = 256 bits per level, enough to account for mods with unused vanilla rooms.
+} LevelNotes;
+
+// This struct must be 256 bytes to add up to 1536 bytes, which adds to the original 512 bytes of save data to equal exactly 2048 bytes.
+typedef struct {
+ LevelNotes level_notes[9];
+ u8 padding[32]; // Reserved for future use.
+} SaveFileExtensionData;
+
+_Static_assert(sizeof(SaveFileExtensionData) == 320, "SaveExtensionData must be 256 bytes");
+
+typedef struct {
+ u8 padding[256];
+} SaveGlobalExtensionData;
+
+_Static_assert(sizeof(SaveGlobalExtensionData) == 256, "save_global_extension_data must be 512 bytes");
+
+extern SaveFileExtensionData loaded_file_extension_data;
+
+#endif
diff --git a/patches/save_extensions.c b/patches/save_extensions.c
new file mode 100644
index 0000000..85f1286
--- /dev/null
+++ b/patches/save_extensions.c
@@ -0,0 +1,169 @@
+#include "patches.h"
+#include "save.h"
+#include "core1/eeprom.h"
+#include "save_extension.h"
+
+// Vanilla declarations.
+extern SaveData gameFile_saveData[4]; //save_data
+extern s8 gameFile_GameIdToFileIdMap[4]; //gamenum to filenum
+extern s32 D_80383F04;
+
+s32 savedata_8033CA2C(s32 filenum, SaveData *save_data);
+int savedata_8033CC98(s32 filenum, u8 *buffer);
+int savedata_8033CCD0(s32 filenum);
+void __gameFile_8033CE14(s32 gamenum);
+void savedata_clear(SaveData *savedata);
+void savedata_update_crc(void *buffer, u32 size);
+void saveData_load(SaveData *savedata);
+void saveData_create(SaveData *savedata);
+
+void bsStoredState_clear(void);
+void func_8031FFAC(void);
+void item_setItemsStartCounts(void);
+void jiggyscore_clearAll(void);
+void honeycombscore_clear(void);
+void mumboscore_clear(void);
+void volatileFlag_clear(void);
+void func_802D6344(void);
+
+
+// New declarations.
+
+// One entry for each file and the backup.
+SaveFileExtensionData save_file_extension_data[4];
+SaveFileExtensionData loaded_file_extension_data;
+
+SaveGlobalExtensionData save_global_extension_data;
+
+_Static_assert(sizeof(save_file_extension_data) + sizeof(save_global_extension_data) == 1536, "Save extension data must be 1536 bytes");
+
+// Place save file extensions after the 4K eeprom range.
+#define SAVE_FILE_EXTENSION_OFFSET (EEPROM_BLOCK_SIZE * EEPROM_MAXBLOCKS)
+// Number of blocks per save file extension slot.
+#define SAVE_FILE_EXTENSION_DATA_BLOCK_COUNT (sizeof(SaveFileExtensionData) / EEPROM_BLOCK_SIZE)
+// Place the global extension data after teh save file extensions
+#define SAVE_GLOBAL_EXTENSION_OFFSET (SAVE_FILE_EXTENSION_OFFSET + sizeof(save_file_extension_data))
+
+_Static_assert(sizeof(SaveFileExtensionData) % EEPROM_BLOCK_SIZE == 0, "SaveFileExtensionData size must be a multiple of EEPROM_BLOCK_SIZE");
+_Static_assert(SAVE_GLOBAL_EXTENSION_OFFSET % EEPROM_BLOCK_SIZE == 0, "SAVE_GLOBAL_EXTENSION_OFFSET must be a multiple of EEPROM_BLOCK_SIZE");
+
+#define SAVE_FILE_EXTENSION_OFFSET_BLOCKS (SAVE_FILE_EXTENSION_OFFSET / EEPROM_BLOCK_SIZE)
+#define SAVE_GLOBAL_EXTENSION_OFFSET_BLOCKS (SAVE_GLOBAL_EXTENSION_OFFSET / EEPROM_BLOCK_SIZE)
+
+// @recomp Patched to load the corresponding file number's extension data.
+RECOMP_PATCH int __gameFile_8033CD90(s32 filenum){
+ int i;
+ s32 tmp_v1;
+ void *save_data_ptr;
+ save_data_ptr = &gameFile_saveData[filenum];
+ // @recomp Get a pointer to the extension data for this file number.
+ SaveFileExtensionData *extension_ptr = &save_file_extension_data[filenum];
+ i = 3;
+ do{
+ // @recomp Also load the extension data.
+ eeprom_readBlocks(0, SAVE_FILE_EXTENSION_OFFSET_BLOCKS + SAVE_FILE_EXTENSION_DATA_BLOCK_COUNT * filenum, extension_ptr, SAVE_FILE_EXTENSION_DATA_BLOCK_COUNT);
+
+ // Read save data from eeprom for file
+ tmp_v1 = savedata_8033CA2C(filenum, save_data_ptr);
+
+ if(!tmp_v1)
+ break;
+ i--;
+ }while(i != 0);
+ if(tmp_v1) {
+ savedata_clear(save_data_ptr);
+ // @recomp Also clear the extension data.
+ bzero(save_data_ptr, sizeof(*save_data_ptr));
+ }
+ return tmp_v1;
+}
+
+// @recomp Patched to save the corresponding file number's extension data.
+RECOMP_PATCH s32 gameFile_8033CFD4(s32 gamenum){
+ s32 next;
+ s32 filenum;
+ u32 i = 3;
+ s32 eeprom_error;
+ SaveData *save_data;
+
+
+ filenum = D_80383F04;
+ next = gameFile_GameIdToFileIdMap[gamenum];
+ gameFile_GameIdToFileIdMap[gamenum] = D_80383F04;
+ bcopy(&gameFile_saveData[next], &gameFile_saveData[filenum], 0xF*8);
+ // @recomp Also copy the extension data from the next slot.
+ bcopy(&save_file_extension_data[next], &save_file_extension_data[filenum], sizeof(save_file_extension_data[0]));
+
+ save_data = gameFile_saveData + filenum;
+
+ // @recomp Get a pointer to the extension data for this file number.
+ SaveFileExtensionData *extension_ptr = &save_file_extension_data[filenum];
+
+ save_data->slotIndex = gamenum + 1;
+ savedata_update_crc(save_data, sizeof(SaveData));
+ for(eeprom_error = 1; eeprom_error && i > 0; i--){//L8033D070
+ // @recomp Also save the extension data.
+ eeprom_writeBlocks(0, SAVE_FILE_EXTENSION_OFFSET_BLOCKS + SAVE_FILE_EXTENSION_DATA_BLOCK_COUNT * filenum, extension_ptr, SAVE_FILE_EXTENSION_DATA_BLOCK_COUNT);
+
+ eeprom_error = savedata_8033CC98(filenum, save_data);
+ if(!eeprom_error){
+ __gameFile_8033CE14(gamenum);
+ }
+ }
+ if(!eeprom_error){
+ for(i = 3; i > 0; i--){//L8033D070
+ eeprom_error = savedata_8033CCD0(next);
+ if(!eeprom_error)
+ break;
+ }
+ }
+ if(eeprom_error){
+ gameFile_GameIdToFileIdMap[gamenum] = next;
+ }
+ else{
+ D_80383F04 = next;
+ }
+ return eeprom_error;
+}
+
+// @recomp Patched to clear extended file data.
+RECOMP_PATCH void gameFile_clear(s32 gamenum){
+ s32 filenum = gameFile_GameIdToFileIdMap[gamenum];
+ savedata_clear(&gameFile_saveData[filenum]);
+
+ // @recomp Clear the extended file data for this file number.
+ bzero(&save_file_extension_data[filenum], sizeof(SaveFileExtensionData));
+}
+
+// @recomp Patched to load extended file data.
+RECOMP_PATCH void gameFile_load(s32 gamenum){
+ s32 filenum = gameFile_GameIdToFileIdMap[gamenum];
+ saveData_load(&gameFile_saveData[filenum]);
+
+ // @recomp Load the extended file data for this file number.
+ memcpy(&loaded_file_extension_data, &save_file_extension_data[filenum], sizeof(SaveFileExtensionData));
+}
+
+// @recomp Patched to save extended file data.
+RECOMP_PATCH void gameFile_save(s32 gamenum){
+ s32 filenum = gameFile_GameIdToFileIdMap[gamenum];
+ saveData_create(&gameFile_saveData[filenum]);
+
+ // @recomp Save the extended file data for this file number.
+ memcpy(&save_file_extension_data[filenum], &loaded_file_extension_data, sizeof(SaveFileExtensionData));
+}
+
+// @recomp Patched to clear the current loaded extended file data.
+RECOMP_PATCH void clearScoreStates(void) {
+ bsStoredState_clear();
+ func_8031FFAC();
+ item_setItemsStartCounts();
+ jiggyscore_clearAll();
+ honeycombscore_clear();
+ mumboscore_clear();
+ volatileFlag_clear();
+ func_802D6344();
+
+ // @recomp Clear the current loaded extended file data.
+ bzero(&loaded_file_extension_data, sizeof(loaded_file_extension_data));
+}
diff --git a/patches/syms.ld b/patches/syms.ld
index 726f584..d142bfd 100644
--- a/patches/syms.ld
+++ b/patches/syms.ld
@@ -44,4 +44,6 @@ recomp_get_camera_inputs = 0x8F00009C;
recomp_get_analog_cam_enabled = 0x8F0000A0;
recomp_get_analog_inverted_axes = 0x8F0000A4;
recomp_set_right_analog_suppressed = 0x8F0000A8;
-osContGetReadData_recomp = 0x8F0000AC;
\ No newline at end of file
+osContGetReadData_recomp = 0x8F0000AC;
+bcopy_recomp = 0x8F0000B0;
+recomp_get_note_saving_enabled = 0x8F0000B4;
diff --git a/src/game/config.cpp b/src/game/config.cpp
index e361b74..6e90bf8 100644
--- a/src/game/config.cpp
+++ b/src/game/config.cpp
@@ -239,6 +239,7 @@ bool save_general_config(const std::filesystem::path& path) {
config_json["camera_invert_mode"] = banjo::get_camera_invert_mode();
config_json["analog_cam_mode"] = banjo::get_analog_cam_mode();
config_json["analog_camera_invert_mode"] = banjo::get_analog_camera_invert_mode();
+ config_json["note_saving_mode"] = banjo::get_note_saving_mode();
config_json["debug_mode"] = banjo::get_debug_mode_enabled();
return save_json_with_backups(path, config_json);
@@ -253,6 +254,7 @@ void set_general_settings_from_json(const nlohmann::json& config_json) {
banjo::set_camera_invert_mode(from_or_default(config_json, "camera_invert_mode", banjo::CameraInvertMode::InvertY));
banjo::set_analog_cam_mode(from_or_default(config_json, "analog_cam_mode", banjo::AnalogCamMode::Off));
banjo::set_analog_camera_invert_mode(from_or_default(config_json, "analog_camera_invert_mode", banjo::CameraInvertMode::InvertNone));
+ banjo::set_note_saving_mode(from_or_default(config_json, "note_saving_mode", banjo::NoteSavingMode::On));
banjo::set_debug_mode_enabled(from_or_default(config_json, "debug_mode", false));
}
diff --git a/src/game/recomp_api.cpp b/src/game/recomp_api.cpp
index edbf651..3f6e161 100644
--- a/src/game/recomp_api.cpp
+++ b/src/game/recomp_api.cpp
@@ -149,6 +149,10 @@ extern "C" void recomp_get_analog_cam_enabled(uint8_t* rdram, recomp_context* ct
_return(ctx, banjo::get_analog_cam_mode() == banjo::AnalogCamMode::On);
}
+extern "C" void recomp_get_note_saving_enabled(uint8_t* rdram, recomp_context* ctx) {
+ _return(ctx, banjo::get_note_saving_mode() == banjo::NoteSavingMode::On);
+}
+
extern "C" void recomp_get_camera_inputs(uint8_t* rdram, recomp_context* ctx) {
float* x_out = _arg<0, float*>(rdram, ctx);
float* y_out = _arg<1, float*>(rdram, ctx);
diff --git a/src/main/main.cpp b/src/main/main.cpp
index da552c7..20a7ca7 100644
--- a/src/main/main.cpp
+++ b/src/main/main.cpp
@@ -349,7 +349,8 @@ std::vector supported_games = {
.internal_name = "Banjo-Kazooie",
.game_id = u8"bk.n64.us.1.0",
.mod_game_id = "bk",
- .save_type = recomp::SaveType::Eep4k,
+ // Eep16k instead of Eep4k to have room for extra save file data.
+ .save_type = recomp::SaveType::Eep16k,
.is_enabled = false,
.decompression_routine = banjo::decompress_bk,
.has_compressed_code = true,
diff --git a/src/ui/ui_config.cpp b/src/ui/ui_config.cpp
index 8f9ec08..7e6b518 100644
--- a/src/ui/ui_config.cpp
+++ b/src/ui/ui_config.cpp
@@ -238,6 +238,7 @@ struct ControlOptionsContext {
banjo::CameraInvertMode camera_invert_mode;
banjo::AnalogCamMode analog_cam_mode;
banjo::CameraInvertMode analog_camera_invert_mode;
+ banjo::NoteSavingMode note_saving_mode;
};
ControlOptionsContext control_options_context;
@@ -325,6 +326,17 @@ void banjo::set_analog_cam_mode(banjo::AnalogCamMode mode) {
}
}
+banjo::NoteSavingMode banjo::get_note_saving_mode() {
+ return control_options_context.note_saving_mode;
+}
+
+void banjo::set_note_saving_mode(banjo::NoteSavingMode mode) {
+ control_options_context.note_saving_mode = mode;
+ if (general_model_handle) {
+ general_model_handle.DirtyVariable("note_saving_mode");
+ }
+}
+
banjo::CameraInvertMode banjo::get_analog_camera_invert_mode() {
return control_options_context.analog_camera_invert_mode;
}
@@ -830,6 +842,7 @@ public:
bind_option(constructor, "camera_invert_mode", &control_options_context.camera_invert_mode);
bind_option(constructor, "analog_cam_mode", &control_options_context.analog_cam_mode);
bind_option(constructor, "analog_camera_invert_mode", &control_options_context.analog_camera_invert_mode);
+ bind_option(constructor, "note_saving_mode", &control_options_context.note_saving_mode);
general_model_handle = constructor.GetModelHandle();
}