forked from Ports/goldeneye-pc-port
fix(D209): AI locomotion locked to walk anim — union byte alias shifts at 64-bit
Root cause of D193 ("NPC AI locomotion is too slow", the user's biggest
playtest complaint; broke Cradle completability).
get_sound_at_range() (chraction.c:3573) is the locomotion-animation
selector: it picks ANIM_DATA_sprinting/running/walking from an arg1 speed
tier. Its only caller passed that tier as self->act_ubytes.padding[45] — a
raw byte alias into the ChrRecord action union rather than the named field.
On N64 offset 45 is exactly act_gopos.unk59:
act_gopos = {coord3d targetpos@0, StandTile *target@12,
waypoint *target_path@16, waypoint *waypoints[6]@20,
u8 curindex@44, u8 unk59@45}
At 64-bit the three pointer members widen 4->8B (target@16, target_path@24,
waypoints@32), pushing unk59 to 81 while the literal 45 lands inside
waypoints[1] (40..47) — byte 5 of a heap pointer. The arena is low-4GB
(0x00000000_70xxxxxx), so that byte is always 0x00: arg1 reads 0 forever
and every AI chr in the game was bound to ANIM_DATA_walking /
ANIM_DATA_walking_unarmed regardless of what the ailist commanded. GE
character travel is animation-root-motion driven, so all AI moved at walk
pace on every level. Bond is unaffected (explicit physics, no root motion),
which is the reported "player fine / all AI slow" split.
Fix: PORT-only read of the named field; N64 line kept verbatim under #else.
ABI/pointer-width only, no game-logic change (rule-2 narrow exception, D3x).
Both callers reach the site with actiontype == ACT_GOPOS, so it is the same
byte N64 reads.
Evidence (Cradle -level_41, GE_D193A extended with tier/aidx): all six chrs
report ACT_GOPOS tier=1 SPEED_RUN on 420/440 samples, while the live
model->anim reverse-resolves to animation_table_ptrs1 index 40
(PTR_ANIM_walking) for Trevelyan and 107 (PTR_ANIM_walking_unarmed) for the
guards on every sample; 42 (running) and 41 (sprinting) never appear.
Travel sat in a flat 140-155 u/s band for all of them.
User playtest-verified on the fix build: Trevelyan and the guards run.
Also:
- GE_D193A extended with tier / gspd / typ / tick / move / frz / hid / aidx
(aidx reverse-resolves model->anim against animation_table_ptrs1).
- bondtypes.h: unk59 was commented "guess: room" — it is the SPEED tier.
- porting-notes.md A1: new bug class — raw-byte aliases into a union read a
plausible constant rather than garbage, so they misbehave silently.
- findings.md: D209 added; D193 closed against it.
This commit is contained in:
1 parent
47d6dfa9c3
commit
a63efe184b
6 files changed
+99
-7
No files matched your search
+6
-1
@@ -2188,7 +2188,12 @@ typedef union
|
||||
// recalculated, the array replaced with a new one and index set to 0.
|
||||
/* player offset 0x58 */
|
||||
u8 curindex;
|
||||
u8 unk59; // guess: room
|
||||
// SPEED tier for this GOPOS: 0 = walk, 1 = run, 2 = sprint. Set from
|
||||
// the SPEED enum in plot_course_for_actor(); read back by
|
||||
// chrlvApplySpeed() and (via a raw-byte union alias on N64 -- see
|
||||
// D209) by the locomotion-animation selector get_sound_at_range().
|
||||
// NB: the old "guess: room" comment here was wrong.
|
||||
u8 unk59;
|
||||
|
||||
// g_ClockTimer related
|
||||
u16 unk5a;
|
||||
|
||||
+42
-4
@@ -47,7 +47,8 @@
|
||||
* GE_D193A_CHR=<n> restricts output to that chrnum. Test-only, env-gated. */
|
||||
static int d193a_mode = -1;
|
||||
static int d193a_only = -1;
|
||||
struct D193aSlot { s16 chrnum; u8 used; f32 px, py, pz, accdist; OSTime t0; };
|
||||
struct D193aSlot { s16 chrnum; u8 used; f32 px, py, pz, accdist; OSTime t0;
|
||||
u32 ticks, moved, frozen; };
|
||||
static struct D193aSlot d193a_slots[48];
|
||||
static struct D193aSlot *d193aSlot(s16 n) {
|
||||
int free = -1;
|
||||
@@ -60,8 +61,18 @@ static struct D193aSlot *d193aSlot(s16 n) {
|
||||
d193a_slots[free].chrnum = n;
|
||||
d193a_slots[free].accdist = 0.0f;
|
||||
d193a_slots[free].t0 = 0;
|
||||
d193a_slots[free].ticks = d193a_slots[free].moved = d193a_slots[free].frozen = 0;
|
||||
return &d193a_slots[free];
|
||||
}
|
||||
static s32 d193aAnimIndex(void *anim) {
|
||||
extern s32 animation_table_ptrs1[];
|
||||
s32 i;
|
||||
if (anim == NULL) return -1;
|
||||
for (i = 0; animation_table_ptrs1[i] != 0; i++) {
|
||||
if ((void *)(uintptr_t)(u32)animation_table_ptrs1[i] == anim) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
static void d193aSample(ChrRecord *chr, Model *model) {
|
||||
if (d193a_mode < 0) {
|
||||
d193a_mode = getenv("GE_D193A") != NULL;
|
||||
@@ -72,6 +83,7 @@ static void d193aSample(ChrRecord *chr, Model *model) {
|
||||
if (d193a_only >= 0 && chr->chrnum != d193a_only) return;
|
||||
struct D193aSlot *s = d193aSlot(chr->chrnum);
|
||||
if (!s) return;
|
||||
s->ticks++;
|
||||
f32 x = chr->prop->pos.x, y = chr->prop->pos.y, z = chr->prop->pos.z;
|
||||
OSTime now = osGetTime();
|
||||
if (s->t0 == 0) { s->t0 = now; s->px = x; s->py = y; s->pz = z; s->accdist = 0.0f; return; }
|
||||
@@ -81,14 +93,36 @@ static void d193aSample(ChrRecord *chr, Model *model) {
|
||||
if (now - s->t0 >= 1000000) {
|
||||
f32 secs = (f32)(now - s->t0) / 1e6f;
|
||||
fprintf(stderr,
|
||||
"[D193A] chr=%d act=%d speed=%.1fu/s gtd=%.2f | scale=%.4f xlscale=%.4f "
|
||||
"animrate=%.4f playspeed=%.4f mspeed=%.4f endframe=%.1f anim=%p\n",
|
||||
"[D193A] chr=%d act=%d speed=%.1fu/s gtd=%.2f | tier=%d gspd=%.2f | typ=%d tick=%u move=%u frz=%u hid=%d | scale=%.4f xlscale=%.4f "
|
||||
"animrate=%.4f playspeed=%.4f mspeed=%.4f endframe=%.1f anim=%p aidx=%d\n",
|
||||
chr->chrnum, (int)chr->actiontype, s->accdist / secs, g_GlobalTimerDelta,
|
||||
(chr->actiontype == ACT_GOPOS) ? (int)chr->act_gopos.unk59 : -1,
|
||||
(chr->actiontype == ACT_GOPOS) ? (double)chr->act_gopos.speed : -1.0,
|
||||
(int)chr->prop->type, s->ticks, s->moved, s->frozen,
|
||||
(int)((chr->chrflags & CHRFLAG_HIDDEN) != 0),
|
||||
model->scale, model->anim_translation_scale, model->animrate,
|
||||
model->playspeed, model->speed, model->endframe, (void *)model->anim);
|
||||
model->playspeed, model->speed, model->endframe, (void *)model->anim,
|
||||
(int)d193aAnimIndex((void *)model->anim));
|
||||
s->t0 = now; s->accdist = 0.0f;
|
||||
s->ticks = s->moved = s->frozen = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* GE_D193A companion: called from chrUpdateAnim -- the ONLY path that
|
||||
* advances prop->pos from anim root motion. chrTick gates it behind
|
||||
* posIsOnScreen() for ACT_ANIM/ACT_STAND (chr.c:2540-2565): an off-screen
|
||||
* chr gets modelTickAnim(model, ticks, 0) instead, which skips
|
||||
* modelSetAnimFrame2WithChrStuff entirely and so does not move at all.
|
||||
* move << tick in the per-second line = the chr is being visibility-culled
|
||||
* out of its own locomotion; move == tick = the gate is clean and the
|
||||
* slowdown is a rate/magnitude problem inside the root-motion decode. */
|
||||
static void d193aMoved(ChrRecord *chr, int frozen) {
|
||||
if (d193a_mode <= 0 || chr->prop == NULL) return;
|
||||
if (d193a_only >= 0 && chr->chrnum != d193a_only) return;
|
||||
struct D193aSlot *s = d193aSlot(chr->chrnum);
|
||||
if (!s) return;
|
||||
if (frozen) s->frozen++; else s->moved++;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef VERSION_EU
|
||||
@@ -2369,6 +2403,10 @@ void chrUpdateAnim(ChrRecord *chr, s32 tickamount)
|
||||
model = chr->model;
|
||||
prop = chr->prop;
|
||||
|
||||
#ifdef PORT
|
||||
d193aMoved(chr, (chr->hidden & CHRHIDDEN_FREEZE) != 0);
|
||||
#endif
|
||||
|
||||
if (!(chr->hidden & CHRHIDDEN_FREEZE))
|
||||
{
|
||||
getsuboffset(model, &chr->prevpos);
|
||||
|
||||
@@ -3662,7 +3662,28 @@ void get_sound_at_range(ChrRecord *self, s32 arg1, s32 arg2)
|
||||
*/
|
||||
void play_hit_soundeffect_and_proper_volume( ChrRecord *self)
|
||||
{
|
||||
#ifdef PORT
|
||||
/* D209: `act_ubytes.padding[45]` is a raw-byte alias into the action
|
||||
* union for act_gopos.unk59 -- the SPEED tier (0 walk / 1 run / 2
|
||||
* sprint) that get_sound_at_range() uses to pick the locomotion
|
||||
* animation. On N64 the alias is exact: act_gopos is
|
||||
* {coord3d targetpos@0, StandTile *target@12, waypoint *target_path@16,
|
||||
* waypoint *waypoints[6]@20, u8 curindex@44, u8 unk59@45}. On x86-64 the
|
||||
* three pointer members widen 4->8 B, so unk59 moves 45 -> 81 while the
|
||||
* literal 45 now lands inside waypoints[1] (40..47) -- byte 5 of a heap
|
||||
* pointer, which for the low-4GB arena (0x00000000_70xxxxxx) is always
|
||||
* 0x00. arg1 therefore reads 0 forever and EVERY AI chr in the game is
|
||||
* bound to ANIM_DATA_walking / ANIM_DATA_walking_unarmed no matter what
|
||||
* speed the ailist commanded. Since chr travel is animation-root-motion
|
||||
* driven, they all move at walk pace -- D193. Both callers of this
|
||||
* function reach it with actiontype == ACT_GOPOS (chraction.c:3723 right
|
||||
* after `actiontype = ACT_GOPOS; act_gopos.unk59 = speed;`, and :9087 in
|
||||
* the explicit else of `actiontype == ACT_PATROL`), so the named field is
|
||||
* exactly the byte N64 reads. ABI/layout only, no logic change (D3x). */
|
||||
get_sound_at_range(self, self->act_gopos.unk59, c_item_entries[self->bodynum].isMale);
|
||||
#else
|
||||
get_sound_at_range(self, self->act_ubytes.padding[45], c_item_entries[self->bodynum].isMale);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user