diff --git a/soh/src/code/z_actor.c b/soh/src/code/z_actor.c index ef249ae9e83..490f2a6a95c 100644 --- a/soh/src/code/z_actor.c +++ b/soh/src/code/z_actor.c @@ -103,7 +103,19 @@ void ActorShadow_Draw(Actor* actor, Lights* lights, PlayState* play, Gfx* dlist, f32 temp2; MtxF sp60; - if (actor->floorPoly != NULL) { + // https://github.com/HarbourMasters/Shipwright/issues/1953 + // + // Actors that cache floorPoly pointers into the DynaPoly polyList buffer can end up holding stale references when + // DynaPoly_ExpandSRT rebuilds the buffers. The pointer address stays valid but the data at that + // address shifts to a different poly -- often a wall poly with a horizontal normal. + // + // When this happens, func_80038A28 builds the shadow matrix from the wall normal, rotating the shadow 90-degrees + // onto a vertical plane. Combined with G_CULL_BACK in sSetupDL[0x2C], the shadow flickers in and out depending on + // camera angle. + // + // Add a normal.y > 0 guard so polys that no longer face upward are rejected. This prevents rendering from stale + // wall or ceiling polys. + if (actor->floorPoly != NULL && actor->floorPoly->normal.y > 0) { temp1 = actor->world.pos.y - actor->floorHeight; if (temp1 >= -50.0f && temp1 < 500.0f) { diff --git a/soh/src/code/z_bgcheck.c b/soh/src/code/z_bgcheck.c index f0ad28568bb..09676b3fe0c 100644 --- a/soh/src/code/z_bgcheck.c +++ b/soh/src/code/z_bgcheck.c @@ -2960,6 +2960,34 @@ void func_8003F8EC(PlayState* play, DynaCollisionContext* dyna, Actor* actor) { /** * DynaPolyInfo_setup */ + +// https://github.com/HarbourMasters/Shipwright/issues/1953 +// +// When DYNAPOLY_INVALIDATE_LOOKUP triggers a full rebuild in DynaPoly_ExpandSRT, the polyList buffer is rewritten and +// poly indices can shift (i.e., when a BgActor is added or removed). Actors that cached a floorPoly pointer into the +// old buffer now hold stale references. +// +// Rederive floorPoly for every actor on a DynaPoly surface so their shadows render from correct data. +static void DynaPoly_RefreshActorFloorPolys(PlayState* play) { + Actor* actor = NULL; + s32 floorBgId = 0; + + for (s32 category = 0; category < ACTORCAT_MAX; ++category) { + for (actor = play->actorCtx.actorLists[category].head; actor != NULL; actor = actor->next) { + if (actor->floorBgId != BGCHECK_SCENE) { + Vec3f checkPos; + checkPos.x = actor->world.pos.x; + checkPos.y = actor->world.pos.y + 50; + checkPos.z = actor->world.pos.z; + actor->floorHeight = BgCheck_EntityRaycastFloor5(play, &play->colCtx, + &actor->floorPoly, &floorBgId, + actor, &checkPos); + actor->floorBgId = floorBgId; + } + } + } +} + void DynaPoly_Setup(PlayState* play, DynaCollisionContext* dyna) { DynaPolyActor* actor; s32 vtxStartIndex; @@ -2999,6 +3027,9 @@ void DynaPoly_Setup(PlayState* play, DynaCollisionContext* dyna) { dyna->bitFlag |= DYNAPOLY_INVALIDATE_LOOKUP; } } + + bool isPolyListInvalid = (dyna->bitFlag & DYNAPOLY_INVALIDATE_LOOKUP) != 0; + vtxStartIndex = 0; polyStartIndex = 0; for (i = 0; i < BG_ACTOR_MAX; i++) { @@ -3007,6 +3038,10 @@ void DynaPoly_Setup(PlayState* play, DynaCollisionContext* dyna) { } } dyna->bitFlag &= ~DYNAPOLY_INVALIDATE_LOOKUP; + + if (isPolyListInvalid) { + DynaPoly_RefreshActorFloorPolys(play); + } } /**