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
This commit is contained in:
Wiseguy
2025-12-30 21:41:30 -05:00
committed by GitHub
parent 90dcdbde92
commit e7185d889e
18 changed files with 801 additions and 20 deletions
+37 -4
View File
@@ -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"
/>
<label class="config-option__tab-label" for="analog_camera_inversion_none">None</label>
@@ -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"
/>
<label class="config-option__tab-label" for="analog_camera_inversion_x">Invert X</label>
@@ -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"
/>
<label class="config-option__tab-label" for="analog_camera_inversion_y">Invert Y</label>
@@ -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"
/>
<label class="config-option__tab-label" for="analog_camera_inversion_both">Invert Both</label>
</div>
</div>
<!-- note saving -->
<div class="config-option" data-event-mouseover="set_cur_config_index(10)">
<label class="config-option__title">Note Saving</label>
<div class="config-option__list">
<input
type="radio"
data-event-blur="set_cur_config_index(-1)"
data-event-focus="set_cur_config_index(10)"
name="note_saving_mode"
data-checked="note_saving_mode"
value="On"
id="note_saving_enabled"
style="nav-up: #camera_inversion_none"
/>
<label class="config-option__tab-label" for="note_saving_enabled">On</label>
<input
type="radio"
data-event-blur="set_cur_config_index(-1)"
data-event-focus="set_cur_config_index(10)"
name="note_saving_mode"
data-checked="note_saving_mode"
value="Off"
id="note_saving_disabled"
style="nav-up: #camera_inversion_x"
/>
<label class="config-option__tab-label" for="note_saving_disabled">Off</label>
</div>
</div>
</div>
<!-- Descriptions -->
<div class="config__wrapper">
@@ -290,6 +320,9 @@
<p data-if="cur_config_index == 9">
Inverts the camera controls for the analog camera if it's enabled. <b>None</b> is the default.
</p>
<p data-if="cur_config_index == 10">
Note saving DESCRIPTION TODO.
</p>
</div>
</div>
</form>
+14
View File
@@ -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();
};
+4
View File
@@ -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;
+13 -12
View File
@@ -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();
}
+6 -1
View File
@@ -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);
}
// @recomp Run note saving map load code.
note_saving_on_map_load();
}
+8
View File
@@ -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;
}
+1
View File
@@ -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
+462
View File
@@ -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);
}
+17
View File
@@ -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
+5
View File
@@ -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, \
+17 -1
View File
@@ -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);
}
}
}
}
+24
View File
@@ -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
+169
View File
@@ -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));
}
+3 -1
View File
@@ -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;
osContGetReadData_recomp = 0x8F0000AC;
bcopy_recomp = 0x8F0000B0;
recomp_get_note_saving_enabled = 0x8F0000B4;
+2
View File
@@ -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));
}
+4
View File
@@ -149,6 +149,10 @@ extern "C" void recomp_get_analog_cam_enabled(uint8_t* rdram, recomp_context* ct
_return<s32>(ctx, banjo::get_analog_cam_mode() == banjo::AnalogCamMode::On);
}
extern "C" void recomp_get_note_saving_enabled(uint8_t* rdram, recomp_context* ctx) {
_return<s32>(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);
+2 -1
View File
@@ -349,7 +349,8 @@ std::vector<recomp::GameEntry> 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,
+13
View File
@@ -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();
}