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(); }