From ee07c24902060250658c8b7a5a15a51199a7426a Mon Sep 17 00:00:00 2001 From: Wiseguy <68165316+Mr-Wiseguy@users.noreply.github.com> Date: Sun, 28 Dec 2025 23:17:42 -0500 Subject: [PATCH] Fix demos, SNS, and Bottles Bonus timings and desyncs (#29) * Fix speed of demos by using recorded deltatime to control frame divisor * Adjust frustum culling in demos to make them play back correctly * Adjust deltatime used for hourglass decrement in Bottles' Bonus to have slowdown like original hardware * Limit framerate in Mumbo SNS cutscene for some maps to let all dialog finish --- patches/culling_patches.c | 181 +++++++++++++++++++++++++++++++++++- patches/timing_patches.c | 144 ++++++++++++++++++++++++++++ patches/transform_tagging.c | 26 ++++-- 3 files changed, 343 insertions(+), 8 deletions(-) create mode 100644 patches/timing_patches.c diff --git a/patches/culling_patches.c b/patches/culling_patches.c index 82325a4..2b8b896 100644 --- a/patches/culling_patches.c +++ b/patches/culling_patches.c @@ -1,16 +1,105 @@ #include "patches.h" #include "core1/ml.h" #include "core1/mlmtx.h" +#include "core1/core1.h" +#include "core1/vimgr.h" +#include "functions.h" extern f32 sViewportPosition[3]; extern f32 sViewportFrustumPlanes[4][4]; +void animMtxList_setBoned(AnimMtxList **this_ptr, BKAnimationList *anim_list, BoneTransformList *arg2); + +extern s32 cur_model_would_have_been_culled_in_demo; +bool recomp_in_demo_playback_game_mode(); int frustum_checks_enabled = TRUE; void set_frustum_checks_enabled(int enabled) { frustum_checks_enabled = enabled; } -RECOMP_PATCH bool viewport_isBoundingBoxInFrustum(f32 arg0[3], f32 arg1[3]) { +// Unpatched copy of viewport_isBoundingBoxInFrustum, used to check if a cube would have been culled originally. +bool viewport_isBoundingBoxInFrustumCopy(f32 min[3], f32 max[3]) { + if (((sViewportFrustumPlanes[0][0] * min[0] + sViewportFrustumPlanes[0][1] * min[1] + sViewportFrustumPlanes[0][2] * min[2] + sViewportFrustumPlanes[0][3]) >= 0.0f) && + ((sViewportFrustumPlanes[0][0] * min[0] + sViewportFrustumPlanes[0][1] * min[1] + sViewportFrustumPlanes[0][2] * max[2] + sViewportFrustumPlanes[0][3]) >= 0.0f) && + ((sViewportFrustumPlanes[0][0] * min[0] + sViewportFrustumPlanes[0][1] * max[1] + sViewportFrustumPlanes[0][2] * min[2] + sViewportFrustumPlanes[0][3]) >= 0.0f) && + ((sViewportFrustumPlanes[0][0] * min[0] + sViewportFrustumPlanes[0][1] * max[1] + sViewportFrustumPlanes[0][2] * max[2] + sViewportFrustumPlanes[0][3]) >= 0.0f) && + ((sViewportFrustumPlanes[0][0] * max[0] + sViewportFrustumPlanes[0][1] * min[1] + sViewportFrustumPlanes[0][2] * min[2] + sViewportFrustumPlanes[0][3]) >= 0.0f) && + ((sViewportFrustumPlanes[0][0] * max[0] + sViewportFrustumPlanes[0][1] * min[1] + sViewportFrustumPlanes[0][2] * max[2] + sViewportFrustumPlanes[0][3]) >= 0.0f) && + ((sViewportFrustumPlanes[0][0] * max[0] + sViewportFrustumPlanes[0][1] * max[1] + sViewportFrustumPlanes[0][2] * min[2] + sViewportFrustumPlanes[0][3]) >= 0.0f) && + ((sViewportFrustumPlanes[0][0] * max[0] + sViewportFrustumPlanes[0][1] * max[1] + sViewportFrustumPlanes[0][2] * max[2] + sViewportFrustumPlanes[0][3]) >= 0.0f)) + return FALSE; + + if (((sViewportFrustumPlanes[1][0] * min[0] + sViewportFrustumPlanes[1][1] * min[1] + sViewportFrustumPlanes[1][2] * min[2] + sViewportFrustumPlanes[1][3]) >= 0.0f) && + ((sViewportFrustumPlanes[1][0] * min[0] + sViewportFrustumPlanes[1][1] * min[1] + sViewportFrustumPlanes[1][2] * max[2] + sViewportFrustumPlanes[1][3]) >= 0.0f) && + ((sViewportFrustumPlanes[1][0] * min[0] + sViewportFrustumPlanes[1][1] * max[1] + sViewportFrustumPlanes[1][2] * min[2] + sViewportFrustumPlanes[1][3]) >= 0.0f) && + ((sViewportFrustumPlanes[1][0] * min[0] + sViewportFrustumPlanes[1][1] * max[1] + sViewportFrustumPlanes[1][2] * max[2] + sViewportFrustumPlanes[1][3]) >= 0.0f) && + ((sViewportFrustumPlanes[1][0] * max[0] + sViewportFrustumPlanes[1][1] * min[1] + sViewportFrustumPlanes[1][2] * min[2] + sViewportFrustumPlanes[1][3]) >= 0.0f) && + ((sViewportFrustumPlanes[1][0] * max[0] + sViewportFrustumPlanes[1][1] * min[1] + sViewportFrustumPlanes[1][2] * max[2] + sViewportFrustumPlanes[1][3]) >= 0.0f) && + ((sViewportFrustumPlanes[1][0] * max[0] + sViewportFrustumPlanes[1][1] * max[1] + sViewportFrustumPlanes[1][2] * min[2] + sViewportFrustumPlanes[1][3]) >= 0.0f) && + ((sViewportFrustumPlanes[1][0] * max[0] + sViewportFrustumPlanes[1][1] * max[1] + sViewportFrustumPlanes[1][2] * max[2] + sViewportFrustumPlanes[1][3]) >= 0.0f)) + return FALSE; + + if (((sViewportFrustumPlanes[2][0] * min[0] + sViewportFrustumPlanes[2][1] * min[1] + sViewportFrustumPlanes[2][2] * min[2] + sViewportFrustumPlanes[2][3]) >= 0.0f) && + ((sViewportFrustumPlanes[2][0] * min[0] + sViewportFrustumPlanes[2][1] * min[1] + sViewportFrustumPlanes[2][2] * max[2] + sViewportFrustumPlanes[2][3]) >= 0.0f) && + ((sViewportFrustumPlanes[2][0] * min[0] + sViewportFrustumPlanes[2][1] * max[1] + sViewportFrustumPlanes[2][2] * min[2] + sViewportFrustumPlanes[2][3]) >= 0.0f) && + ((sViewportFrustumPlanes[2][0] * min[0] + sViewportFrustumPlanes[2][1] * max[1] + sViewportFrustumPlanes[2][2] * max[2] + sViewportFrustumPlanes[2][3]) >= 0.0f) && + ((sViewportFrustumPlanes[2][0] * max[0] + sViewportFrustumPlanes[2][1] * min[1] + sViewportFrustumPlanes[2][2] * min[2] + sViewportFrustumPlanes[2][3]) >= 0.0f) && + ((sViewportFrustumPlanes[2][0] * max[0] + sViewportFrustumPlanes[2][1] * min[1] + sViewportFrustumPlanes[2][2] * max[2] + sViewportFrustumPlanes[2][3]) >= 0.0f) && + ((sViewportFrustumPlanes[2][0] * max[0] + sViewportFrustumPlanes[2][1] * max[1] + sViewportFrustumPlanes[2][2] * min[2] + sViewportFrustumPlanes[2][3]) >= 0.0f) && + ((sViewportFrustumPlanes[2][0] * max[0] + sViewportFrustumPlanes[2][1] * max[1] + sViewportFrustumPlanes[2][2] * max[2] + sViewportFrustumPlanes[2][3]) >= 0.0f)) + return FALSE; + + if (((sViewportFrustumPlanes[3][0] * min[0] + sViewportFrustumPlanes[3][1] * min[1] + sViewportFrustumPlanes[3][2] * min[2] + sViewportFrustumPlanes[3][3]) >= 0.0f) && + ((sViewportFrustumPlanes[3][0] * min[0] + sViewportFrustumPlanes[3][1] * min[1] + sViewportFrustumPlanes[3][2] * max[2] + sViewportFrustumPlanes[3][3]) >= 0.0f) && + ((sViewportFrustumPlanes[3][0] * min[0] + sViewportFrustumPlanes[3][1] * max[1] + sViewportFrustumPlanes[3][2] * min[2] + sViewportFrustumPlanes[3][3]) >= 0.0f) && + ((sViewportFrustumPlanes[3][0] * min[0] + sViewportFrustumPlanes[3][1] * max[1] + sViewportFrustumPlanes[3][2] * max[2] + sViewportFrustumPlanes[3][3]) >= 0.0f) && + ((sViewportFrustumPlanes[3][0] * max[0] + sViewportFrustumPlanes[3][1] * min[1] + sViewportFrustumPlanes[3][2] * min[2] + sViewportFrustumPlanes[3][3]) >= 0.0f) && + ((sViewportFrustumPlanes[3][0] * max[0] + sViewportFrustumPlanes[3][1] * min[1] + sViewportFrustumPlanes[3][2] * max[2] + sViewportFrustumPlanes[3][3]) >= 0.0f) && + ((sViewportFrustumPlanes[3][0] * max[0] + sViewportFrustumPlanes[3][1] * max[1] + sViewportFrustumPlanes[3][2] * min[2] + sViewportFrustumPlanes[3][3]) >= 0.0f) && + ((sViewportFrustumPlanes[3][0] * max[0] + sViewportFrustumPlanes[3][1] * max[1] + sViewportFrustumPlanes[3][2] * max[2] + sViewportFrustumPlanes[3][3]) >= 0.0f)) + return FALSE; + + return TRUE; +} + +typedef struct { + s32 cmd_0; + s32 size_4; + s16 unk8[3]; + s16 unkE[3]; + s16 unk14; +}GeoCmdD; + +void func_80339124(Gfx **, Mtx **, BKGeoList *); + +// @recomp Patched to disable culling in model rendering. +RECOMP_PATCH void func_80338DCC(Gfx ** gfx, Mtx ** mtx, void *arg2){ + f32 sp2C[3]; + f32 sp20[3]; + GeoCmdD * cmd = (GeoCmdD *)arg2; + if(cmd->unk14){ + // @recomp Always run the child command. + // sp2C[0] = (f32)cmd->unk8[0] * modelRenderScale; + // sp2C[1] = (f32)cmd->unk8[1] * modelRenderScale; + // sp2C[2] = (f32)cmd->unk8[2] * modelRenderScale; + + // sp20[0] = (f32)cmd->unkE[0] * modelRenderScale; + // sp20[1] = (f32)cmd->unkE[1] * modelRenderScale; + // sp20[2] = (f32)cmd->unkE[2] * modelRenderScale; + if(TRUE){//viewport_isBoundingBoxInFrustum(sp2C, sp20)){ + func_80339124(gfx, mtx, (BKGeoList*)((s32)cmd + cmd->unk14)); + } + } +} + +// @recomp Patched to disable frustum culling. +RECOMP_PATCH bool viewport_isBoundingBoxInFrustum(f32 min[3], f32 max[3]) { + // @recomp Disable frustum culling for cubes outside of demos. Demos need to maintain the original + // culling for cubes to prevent them from behaving differently than the original game. + if (recomp_in_demo_playback_game_mode()) { + return viewport_isBoundingBoxInFrustumCopy(min, max); + } + return TRUE; } @@ -35,3 +124,93 @@ RECOMP_PATCH bool viewport_func_8024DB50(f32 pos[3], f32 distance) { return TRUE; } + + +// @recomp Patched to use the frustum culling result to set the marker's field. +// This is only enabled during demos, and is needed so that demos play back correctly. +RECOMP_PATCH void actor_postdrawMethod(ActorMarker *marker){ + if (!cur_model_would_have_been_culled_in_demo) { + marker->unk14_21 = TRUE; + } +} + +extern struct5Bs *D_8036E568; +bool func_80330534(Actor *actor); +bool func_8033056C(Actor *actor); +void anim_802897D4(AnimMtxList *this, BKAnimationList *arg0, Animation *dst); +void modelRender_setBoneTransformList(s32); +void modelRender_setAnimatedTexturesCacheId(s32 arg0); +s32 actor_getAnimatedTexturesCacheId(Actor *actor); + +// @recomp Patched to not call the actor's callback if the actor's model would have been culled. +// This is only enabled during demos, and is needed so that demos play back correctly. +RECOMP_PATCH void actor_predrawMethod(Actor *this){ + s32 pad4C; + BKModelBin *sp48; + bool sp44; + BKVertexList *sp40; + f32 sp34[3]; + + sp48 = marker_loadModelBin(this->marker); + func_80330534(this); + if(this->anctrl != NULL){ + anctrl_drawSetup(this->anctrl, this->position, 1); + } + + if(this->marker->unk20 != NULL){ + sp44 = FALSE; + if(this->unk148 != NULL){ + animMtxList_setBoned(&this->marker->unk20, model_getAnimationList(sp48), skeletalAnim_getBoneTransformList(this->unk148)); + sp44 = TRUE; + }//L8032542C + else if(this->anctrl != NULL && model_getAnimationList(sp48)){ + anim_802897D4(&this->marker->unk20, model_getAnimationList(sp48), anctrl_getAnimPtr(this->anctrl)); + sp44 = TRUE; + }//L80325474 + + if(sp44){ + func_8033A444((AnimMtxList*)this->marker->unk20); + } + }//L8032548C + + if(this->alpha_124_19 < 0xFF){ + modelRender_setAlpha(this->alpha_124_19); + } + + modelRender_setDepthMode(this->depth_mode); + if(this->marker->unk44 != 0){ + if((s32)this->marker->unk44 == 1){ + func_8033A450(D_8036E568); + } + else{ + func_8033A450(this->marker->unk44); + } + } + + if(this->unkF4_30){ + sp40 = func_80330C74(this); + if(this->unk138_29){ + sp34[0] = this->pitch; + sp34[1] = this->yaw; + sp34[2] = this->roll; + codeAC520_func_80333D48(sp40, this->position, sp34, this->scale, 0, model_getVtxList(sp48)); + }//L80325560 + modelRender_setVertexList(sp40); + this->unkF4_29 = NOT(this->unkF4_29); + }//L80325594 + + // @recomp Only call the actor's callback if the model wouldn't have been frustum culled. + // This only has an effect during demos, as the in frustum flag always set to true outside of demos. + if (!cur_model_would_have_been_culled_in_demo) { + if(this->unk130){ + this->unk130(this); + } + } + + if(this->unk148 && !this->marker->unk20){ + modelRender_setBoneTransformList((s32)skeletalAnim_getBoneTransformList(this->unk148)); + } + + func_8033056C(this); + modelRender_setAnimatedTexturesCacheId(actor_getAnimatedTexturesCacheId(this)); +} diff --git a/patches/timing_patches.c b/patches/timing_patches.c new file mode 100644 index 0000000..a9fa8c2 --- /dev/null +++ b/patches/timing_patches.c @@ -0,0 +1,144 @@ +#include "patches.h" +#include "prop.h" + +typedef struct demo_input{ + u8 unk0; + u8 unk1; + u16 unk2; + u8 unk4; + u8 unk5; +}DemoInput; + +typedef struct demo_file_header{ + u8 pad0[0x4]; + DemoInput inputs[]; +} DemoFileHeader; + +extern DemoInput *D_803860D0; //demo_input_ptr +extern DemoFileHeader * D_803860D4; //demo_file_ptr +extern s32 D_803860D8;//current_input +extern s32 D_803860DC;//total_inputs + +extern DemoInput D_80371EF0; + +enum extra_actors_e { + ACTOR_19_FRAMERATE_60 = 0x19, + ACTOR_1A_FRAMERATE_30, + ACTOR_1B_FRAMERATE_20, + ACTOR_1C_FRAMERATE_15, + ACTOR_1D_FRAMERATE_12, +}; + +typedef struct struct_core2_9B180_s{ + s16 unk0; + // u8 pad2[0x2]; + NodeProp *unk4; + void (*unk8)(struct struct_core2_9B180_s *); + void (*unkC)(struct struct_core2_9B180_s *); + void (*unk10)(struct struct_core2_9B180_s *); +}Struct_core2_9B180_0; + +extern Struct_core2_9B180_0 D_8036DE00[6]; + +extern volatile s32 D_802808D8; +extern s32 D_802808DC; +extern s32 D_80280E90; + +void func_80244A98(s32 arg0); +NodeProp *cubeList_findNodePropByActorIdAndPosition_s32(enum actor_e actor_id, s32 arg1[3]); +void func_8032236C(s32 arg0, s32 arg1, s32 *arg2); +void viMgr_func_8024BF94(s32 arg0); +void viMgr_func_8024BFD8(s32 arg0); +void dummy_func_8025AFB8(void); +s32 getGameMode(void); +f32 time_getDelta(void); +s32 item_adjustByDiffWithHud(enum item_e item, s32 diff); +void func_802FACA4(enum item_e item_id); +bool func_802FAFE8(enum item_e item_id); +enum map_e map_get(void); + +s32 demo_frame_divisor = -1; + +// @recomp Patched to set a variable to use as the frame divisor when processing demo inputs. +RECOMP_PATCH int demo_readInput(OSContPad* arg0, s32* arg1){ + DemoInput *input_ptr = &D_803860D0[D_803860D8++]; + int not_eof = D_803860D8 < D_803860DC; + + if(!not_eof) + input_ptr = &D_80371EF0; + + arg0->stick_x = input_ptr->unk0; + arg0->stick_y = input_ptr->unk1; + arg0->button = input_ptr->unk2; + *arg1 = input_ptr->unk4; + + // @recomp Track the frame divisor for later. + demo_frame_divisor = input_ptr->unk4; + + // @recomp Lock the frame divisor to 3 (20 FPS) in Bottles' Bonus. + // The game generally runs at 20 FPS during Bottles' Bonus on original hardware, which means the minigame timer should last a similar amount of time. + // Return 2 (30 FPS) into arg1 so that the game doesn't compensate by making the timer run faster. + s32 game_mode = getGameMode(); + if (game_mode == GAME_MODE_8_BOTTLES_BONUS) { + // *arg1 = 2; + demo_frame_divisor = 3; + } + // @recomp Lock the frame divisor in the Mumbo SNS pictures based on the loaded map. + // This makes sure that the dialog doesn't get cut off because the scene runs too quickly. + if (game_mode == GAME_MODE_A_SNS_PICTURE) { + s32 cur_map = map_get(); + + switch (cur_map) { + // 20 FPS maps. + case MAP_7F_FP_WOZZAS_CAVE: + case MAP_92_GV_SNS_CHAMBER: + demo_frame_divisor = 3; + break; + // Remaining maps run at 30 FPS. + default: + demo_frame_divisor = 2; + break; + } + } + + return not_eof; +} + +// @recomp Patched to override the VI frame divisor when the demo frame divisor has been set. +RECOMP_PATCH s32 viMgr_func_8024BFA0() { + if (demo_frame_divisor != -1) { + return demo_frame_divisor; + } + return D_802808DC; +} + +// @recomp Patched to clear the demo frame divisor after viMgr_func_8024BFD8. +RECOMP_PATCH void viMgr_func_8024C1B4(void){ + viMgr_func_8024BFD8(0); + // @recomp Clear the demo frame divisor. + demo_frame_divisor = -1; + dummy_func_8025AFB8(); +} + +// @recomp Patched to clear the demo frame divisor after viMgr_func_8024BFD8. +RECOMP_PATCH void viMgr_func_8024C1DC(void){ + viMgr_func_8024BFD8(1); + // @recomp Clear the demo frame divisor. + demo_frame_divisor = -1; +} + +// @recomp Patched to use a fixed time delta of 30 FPS when decrementing the hourglass timer during Bottles' Bonus. +// Because the game is running at 20 FPS, this will result in the timer running slightly slower, which is accurate +// to how fast it runs on original hardware. +RECOMP_PATCH void func_80345EB0(enum item_e item){ + if(func_802FAFE8(item)){ + // @recomp Get the time delta and override it if this is the hourglass timer and the gamemode is Bottles' Bonus. + f32 time_delta = time_getDelta(); + if (item == ITEM_0_HOURGLASS_TIMER && getGameMode() == GAME_MODE_8_BOTTLES_BONUS) { + time_delta = 1.0f / 30.0f; + } + item_adjustByDiffWithHud(item, (s32)(-time_delta*60.0f * 1.1)); + }else{ + func_802FACA4(item); + } +} diff --git a/patches/transform_tagging.c b/patches/transform_tagging.c index 7da502a..bde5b0f 100644 --- a/patches/transform_tagging.c +++ b/patches/transform_tagging.c @@ -6,6 +6,8 @@ #include "core1/mlmtx.h" #include "functions.h" +extern bool recomp_in_demo_playback_game_mode(); + bool skip_all_interpolation = FALSE; bool has_additional_model_scale = FALSE; f32 additional_model_scale_x; @@ -32,6 +34,7 @@ s32 cur_drawn_model_transform_id = 0; s32 cur_drawn_model_transform_id_skip_interpolation = FALSE; s32 cur_model_transform_id_offset = 0; s32 cur_model_uses_bones = FALSE; +s32 cur_model_would_have_been_culled_in_demo = FALSE; Mtx identity_fixed_mtx = {{ { @@ -598,16 +601,20 @@ RECOMP_PATCH BKModelBin *modelRender_draw(Gfx **gfx, Mtx **mtx, f32 position[3], return 0; } - // @recomp Disable frustum checks. - set_frustum_checks_enabled(FALSE); - - D_80370990 = (D_80383704) ? viewport_func_8024DB50(object_position, spD0*scale) : 1; - - // @recomp Re-enable frustum checks. - set_frustum_checks_enabled(TRUE); + // @recomp Record the frustum check result, but ignore it during this function to disable frustum culling. + // It will get set before this function returns. + cur_model_would_have_been_culled_in_demo = !((D_80383704) ? viewport_func_8024DB50(object_position, spD0*scale) : 1); + D_80370990 = TRUE; + + // @recomp Force the frustum check to be true if the game isn't in demo playback mode. + if (!recomp_in_demo_playback_game_mode()) { + cur_model_would_have_been_culled_in_demo = FALSE; + } if(D_80370990 == 0){ modelRender_reset(); + // @recomp Clear the flag indicating that the model would have been culled before returning. + cur_model_would_have_been_culled_in_demo = FALSE; return 0; } @@ -841,5 +848,10 @@ RECOMP_PATCH BKModelBin *modelRender_draw(Gfx **gfx, Mtx **mtx, f32 position[3], } modelRender_reset(); + + // @recomp Save the actual frustum culling result before returning. + D_80370990 = !cur_model_would_have_been_culled_in_demo; + // @recomp Clear the flag indicating that the model would have been culled before returning. + cur_model_would_have_been_culled_in_demo = FALSE; return model_bin; }