From 1a3c2682786f71e88404fbd95002b77536a5a8a8 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Mon, 3 Aug 2026 10:38:24 -0500 Subject: [PATCH] #124: the aimed pick now intersects the DRAWN GEOMETRY -- sphere approximation retired; shared-hull hits route through the cylinder The zone-walk matrix caught the pick red-handed: with the servo verifiably holding the reticle on the dtorso segment, the per-segment BOUNDING-SPHERE pick returned rgun/ruleg -- the gun pods and legs thread the ray before the torso from most angles (its own comments admitted a foot could be unhittable behind its own knee). The 1995 pick was a dpl scene intersection against the drawn geometry. Restored that semantic: * BTGetPickMesh caches each segment d3d_OBJECT's triangles CPU-side once (its own BGF VB/IB, managed-pool locks); MechSegmentPick now runs sphere PRE-FILTER -> Moller-Trumbore nearest-hit across the threaded segments' posed meshes; the old smallest-sphere selection survives only as the no-triangle fallback. * Segments claimed as CARRIER by 2+ zones (the shared hull: madcat seg 4 carries dtorso+ltorso+rtorso+utorso+rears) cannot resolve one zone from geometry -- those hits return zone -1 WITH the accurate triangle point, and the victim's bit-verified (frame-fixed) cylinder assigns the panel by band/wedge. Unique carriers (legs, feet, arms, gun pods) keep the direct zone. * ZoneAimPoint: the walker aims at a zone's VISUAL center (largest pick object's cull-center) instead of the segment origin -- joint origins made feet/lower legs strike the part above. * Walker upgrades from live operation: 3-column truth (aim/pick/land), engage gate, damped servo with polarity watchdog, approach port. Zone-walk verdict (full cycles, spinning target, real MP): limbs 5-6/6 direct in-zone; every hull panel routes CYL; the victim's landed zones now include ltorso/rtorso/reardtorso/rearutorso/rearrtorso -- the panels night-10 reported unhittable. Residuals tracked on #124: rtorso/utorso aim-anchor placement, doors/searchlight small-zone sample, twisted-torso twist-sign verify. Co-Authored-By: Claude Fable 5 --- game/reconstructed/btl4vid.cpp | 234 ++++++++++++++++++++++++++++++++- game/reconstructed/btl4vid.hpp | 7 + game/reconstructed/mech4.cpp | 19 ++- scratchpad/night10/zonewalk.sh | 4 +- 4 files changed, 253 insertions(+), 11 deletions(-) diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index 48fe809..abc3826 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -1545,10 +1545,106 @@ int BTWreckSinkTick(Entity *victim, float dt) // -// #73 -- the aimed PER-PART pick (see the header note). Ray-vs-sphere over -// the per-segment draw objects recorded at tree build; world centers come -// through the draw-cached mLocalToWorld (updated every drawn frame -- the -// target being aimed at is on screen, so at most one frame stale). +// #124 -- the pick's TRIANGLE cache. The 1995 pick was a dpl scene +// intersection against the DRAWN GEOMETRY (the division card cast from the +// view); the port's sphere approximation measurably mis-picked (the zone-walk +// matrix: aim dead-on dtorso -> picked rgun/ruleg -- gun/limb spheres thread +// the ray before the torso from most angles, and its own comments admitted "a +// foot can be unhittable behind its own knee"). Restore the authentic +// semantic: nearest RAY-TRIANGLE hit across the candidate segments' posed +// meshes. Positions are read ONCE per d3d_OBJECT from its own BGF buffers +// (managed pool, lockable) and cached CPU-side; the per-frame cost is a +// sphere pre-filter + Moller-Trumbore over the few threaded segments. +// +struct BTPickMesh +{ + std::vector pos; // xyz per vertex + std::vector idx; // triangle list + int ok; +}; +static std::map gBTPickMeshes; + +static BTPickMesh * + BTGetPickMesh(d3d_OBJECT *obj) +{ + std::map::iterator mi = gBTPickMeshes.find(obj); + if (mi != gBTPickMeshes.end()) + return mi->second.ok ? &mi->second : 0; + + BTPickMesh &pm = gBTPickMeshes[obj]; + pm.ok = 0; + if (obj->mBgfVB == 0 || obj->mBgfIB == 0 || obj->mBgfStride < 12) + return 0; + + D3DINDEXBUFFER_DESC ibd; + if (FAILED(obj->mBgfIB->GetDesc(&ibd))) + return 0; + int idx32 = (ibd.Format == D3DFMT_INDEX32); + unsigned int nIdx = ibd.Size / (idx32 ? 4 : 2); + + void *vp = 0, *ip = 0; + if (FAILED(obj->mBgfVB->Lock(0, 0, &vp, D3DLOCK_READONLY))) + return 0; + if (FAILED(obj->mBgfIB->Lock(0, 0, &ip, D3DLOCK_READONLY))) + { + obj->mBgfVB->Unlock(); + return 0; + } + pm.pos.resize((size_t)obj->mBgfNumVerts * 3); + const unsigned char *vb = (const unsigned char *)vp; + for (UINT v = 0; v < obj->mBgfNumVerts; ++v) + { + const float *p = (const float *)(vb + (size_t)v * obj->mBgfStride); + pm.pos[v*3+0] = p[0]; // position-first vertex layout (the BGF + pm.pos[v*3+1] = p[1]; // loader's own decl; the cull sphere was + pm.pos[v*3+2] = p[2]; // computed from these same floats at load) + } + pm.idx.resize(nIdx); + if (idx32) + { + const unsigned int *s = (const unsigned int *)ip; + for (unsigned int k = 0; k < nIdx; ++k) pm.idx[k] = s[k]; + } + else + { + const unsigned short *s = (const unsigned short *)ip; + for (unsigned int k = 0; k < nIdx; ++k) pm.idx[k] = s[k]; + } + obj->mBgfIB->Unlock(); + obj->mBgfVB->Unlock(); + pm.ok = (pm.idx.size() >= 3 && pm.pos.size() >= 9); + return pm.ok ? &pm : 0; +} + +// Moller-Trumbore, both-sided (the pod's dpl geometry has no consistent +// winding guarantee across ported BGF pieces). Returns t >= 0 or -1. +static float + BTRayTri(const float o[3], const float d[3], + const float *a, const float *b, const float *c) +{ + float e1[3] = { b[0]-a[0], b[1]-a[1], b[2]-a[2] }; + float e2[3] = { c[0]-a[0], c[1]-a[1], c[2]-a[2] }; + float pv[3] = { d[1]*e2[2]-d[2]*e2[1], d[2]*e2[0]-d[0]*e2[2], d[0]*e2[1]-d[1]*e2[0] }; + float det = e1[0]*pv[0] + e1[1]*pv[1] + e1[2]*pv[2]; + if (det > -1e-8f && det < 1e-8f) return -1.0f; + float inv = 1.0f / det; + float tv[3] = { o[0]-a[0], o[1]-a[1], o[2]-a[2] }; + float u = (tv[0]*pv[0] + tv[1]*pv[1] + tv[2]*pv[2]) * inv; + if (u < 0.0f || u > 1.0f) return -1.0f; + float qv[3] = { tv[1]*e1[2]-tv[2]*e1[1], tv[2]*e1[0]-tv[0]*e1[2], tv[0]*e1[1]-tv[1]*e1[0] }; + float v = (d[0]*qv[0] + d[1]*qv[1] + d[2]*qv[2]) * inv; + if (v < 0.0f || u + v > 1.0f) return -1.0f; + float t = (e2[0]*qv[0] + e2[1]*qv[1] + e2[2]*qv[2]) * inv; + return (t >= 0.0f) ? t : -1.0f; +} + +// +// #73 -- the aimed PER-PART pick (see the header note). #124: now a true +// DRAWN-GEOMETRY intersection -- sphere pre-filter, then nearest ray-triangle +// hit across the threaded segments' posed meshes (the 1995 division-card +// semantic). The old smallest-sphere selection survives only as the fallback +// when no triangle anywhere is struck (grazing edge shots). World transforms +// come through the draw-cached mLocalToWorld (at most one frame stale). // int BTL4VideoRenderer::MechSegmentPick( @@ -1573,12 +1669,42 @@ int // envelope; smallest-pierced picks the most specific part on the aim line, // and the torso wins only when no limb is threaded -- which is the per-part // semantic the 1995 mesh intersection produced. - float bestR = 1e30f; // primary key: sphere radius (ascending) - float bestScore = 1.0f; // tie-break: normalized perpendicular d2/r2 + float bestR = 1e30f; // sphere-FALLBACK key: radius (ascending) + float bestScore = 1.0f; // sphere tie-break: normalized d2/r2 float bestT = max_range; int bestZone = -1; int hitAny = 0; + // triangle-accurate primary: nearest surface hit across all segments + float triBestT = max_range; + int triBestZone = -1; + int triBestSeg = -1; + int triHit = 0; + + // #124: segments claimed as CARRIER by TWO OR MORE zones (the shared hull + // -- e.g. madcat seg 4 carries dtorso AND ltorso AND rtorso AND utorso AND + // the rears) cannot resolve a single zone from geometry. The authentic + // route for their hits is the CYLINDER: return zone -1 with the accurate + // triangle hit point, and the victim's point resolver assigns the panel + // by band/wedge. Unique-carrier segments (legs, feet, arms, gun pods) + // keep their direct zone. + unsigned char segClaims[192]; + memset(segClaims, 0, sizeof(segClaims)); + { + extern int BTMechZoneSegAndName(void *mech_v, int zone_idx, + int *seg_out, const char **name_out); + int zsi; + const char *znm; + for (int zi = 0; zi < 64; ++zi) + { + if (!BTMechZoneSegAndName((void *)mech, zi, &zsi, &znm)) + break; + if (zsi >= 0 && zsi < (int)sizeof(segClaims) + && segClaims[zsi] < 255) + ++segClaims[zsi]; + } + } + std::map::iterator sp; for (sp = it->second.segPick.begin(); sp != it->second.segPick.end(); ++sp) { @@ -1609,6 +1735,53 @@ int if (t < 0.0f || t >= max_range) continue; + // ---- TRIANGLE TEST (#124): the sphere only nominates ---- + BTPickMesh *pm = BTGetPickMesh(obj); + if (pm != 0) + { + // ray into object-local space (affine inverse; segment poses are + // rigid, so local t == world t after direction normalization is + // preserved by construction below) + D3DXMATRIX w2l; + if (D3DXMatrixInverse(&w2l, 0, &l2w) != 0) + { + D3DXVECTOR3 lo, ld; + D3DXVECTOR3 wo(ray_start[0], ray_start[1], ray_start[2]); + D3DXVECTOR3 wd(ray_dir[0], ray_dir[1], ray_dir[2]); + D3DXVec3TransformCoord(&lo, &wo, &w2l); + D3DXVec3TransformNormal(&ld, &wd, &w2l); + float o3[3] = { lo.x, lo.y, lo.z }; + float d3[3] = { ld.x, ld.y, ld.z }; + const float *P = &pm->pos[0]; + size_t nv = pm->pos.size() / 3; + for (size_t k = 0; k + 2 < pm->idx.size(); k += 3) + { + unsigned int i0 = pm->idx[k], i1 = pm->idx[k+1], i2 = pm->idx[k+2]; + if (i0 >= nv || i1 >= nv || i2 >= nv) + continue; + float tt = BTRayTri(o3, d3, P + i0*3, P + i1*3, P + i2*3); + if (tt >= 0.0f && tt < triBestT) + { + // world-space t of the local hit (handles any scale) + D3DXVECTOR3 lh(o3[0]+d3[0]*tt, o3[1]+d3[1]*tt, o3[2]+d3[2]*tt); + D3DXVECTOR3 wh; + D3DXVec3TransformCoord(&wh, &lh, &l2w); + float wt = (wh.x - ray_start[0]) * ray_dir[0] + + (wh.y - ray_start[1]) * ray_dir[1] + + (wh.z - ray_start[2]) * ray_dir[2]; + if (wt >= 0.0f && wt < triBestT) + { + triBestT = wt; + triBestZone = sp->second.zone; + triBestSeg = sp->first; + triHit = 1; + } + } + } + } + } + + // ---- sphere FALLBACK bookkeeping (unchanged selection) ---- float score = d2 / r2; // 0 = dead-center thread if (r > bestR || (r == bestR && score >= bestScore)) @@ -1621,6 +1794,17 @@ int hitAny = 1; } + if (triHit) + { + bestT = triBestT; + bestZone = triBestZone; + hitAny = 1; + // shared-carrier segment -> the cylinder decides (accurate point kept) + if (triBestSeg >= 0 && triBestSeg < (int)sizeof(segClaims) + && segClaims[triBestSeg] > 1) + bestZone = -1; + } + // #92 probe: which spheres did the ray actually THREAD, and which won? // "smallest radius wins" means a big sphere can never beat a small one that // the ray also grazes -- so a foot can be unhittable behind its own knee. @@ -1691,6 +1875,44 @@ int return 1; } +// +// #124 zone walker: the zone's visual aim anchor (see the hpp note). +// +int + BTL4VideoRenderer::ZoneAimPoint(Entity *mech, int zone, float out3[3]) +{ + std::map::iterator it = mMechRenderTrees.find(mech); + if (it == mMechRenderTrees.end() || it->second.wrecked) + return 0; + d3d_OBJECT *best = 0; + std::map::iterator sp; + for (sp = it->second.segPick.begin(); sp != it->second.segPick.end(); ++sp) + { + if (sp->second.zone != zone || sp->second.obj == NULL) + continue; + if (best == 0 || sp->second.obj->mCullRadius > best->mCullRadius) + best = sp->second.obj; + } + if (best == 0 || best->mCullRadius <= 0.0f) + return 0; + D3DXMATRIX l2w = best->GetLocalToWorld(); + D3DXVECTOR3 cw; + D3DXVec3TransformCoord(&cw, &best->mCullCenter, &l2w); + out3[0] = cw.x; out3[1] = cw.y; out3[2] = cw.z; + return 1; +} + +int BTMechZoneAimPoint(void *mech, int zone, float out3[3]) +{ + if (mech == NULL || application == NULL) + return 0; + BTL4VideoRenderer *renderer = + (BTL4VideoRenderer *)application->GetVideoRenderer(); + if (renderer == NULL) + return 0; + return renderer->ZoneAimPoint((Entity *)mech, zone, out3); +} + // // Game-side bridge (mech4.cpp's per-frame target pick; same access pattern as // the wreck swap below). diff --git a/game/reconstructed/btl4vid.hpp b/game/reconstructed/btl4vid.hpp index c9ad5ec..67afa72 100644 --- a/game/reconstructed/btl4vid.hpp +++ b/game/reconstructed/btl4vid.hpp @@ -797,6 +797,13 @@ extern void BTDrawReticle(struct IDirect3DDevice9 *device); const float ray_dir[3], float max_range, float hit_out[3], int *zone_out); + // #124 zone walker: a zone's VISUAL aim point -- its largest pick + // object's cull-center in world (what a player aims at; the segment + // ORIGIN sits at the joint and makes lower segments strike the part + // above). 0 when the zone has no pick geometry. + int + ZoneAimPoint(Entity *mech, int zone, float out3[3]); + protected: // // Renderer-manager overrides diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 0478276..c343c96 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -6126,9 +6126,18 @@ void << "' seg=" << segIdx << " target=" << target->GetEntityID() << "\n" << std::flush; } + extern int BTMechZoneAimPoint(void *mech, int zone, float out3[3]); + int haveAim = 0; if (BTMechZoneSegAndName(target, s_zwZone, &segIdx, &zname) - && segIdx >= 0 - && BTResolveSegmentWorld((void *)target, segIdx, sp, rows)) + && segIdx >= 0) + { + // prefer the zone's VISUAL center (its pick geometry's + // cull-center); the segment ORIGIN sits at the joint + // and made feet/lower legs strike the part above. + haveAim = BTMechZoneAimPoint(target, s_zwZone, sp) + || BTResolveSegmentWorld((void *)target, segIdx, sp, rows); + } + if (haveAim) { float wx = sp[0]-rs[0], wy = sp[1]-rs[1], wz = sp[2]-rs[2]; float wl = sqrtf(wx*wx + wy*wy + wz*wz); @@ -6193,9 +6202,13 @@ void { gBTLaserKey = 1; ++s_zwShots; + // pick= is the LIVE picked zone (hotZone via the + // target slot) -- the 3-column truth: AIMED zone + // vs PICKED zone here, LANDED zone on the victim. DEBUG_STREAM << "[walk] FIRE " << s_zwShots << " zone " << s_zwZone << " '" << zname - << "' range=" << wl << "\n" << std::flush; + << "' pick=" << MECH_TARGET_SUBIDX(this) + << " range=" << wl << "\n" << std::flush; } } } diff --git a/scratchpad/night10/zonewalk.sh b/scratchpad/night10/zonewalk.sh index 86e4b70..849e4fc 100644 --- a/scratchpad/night10/zonewalk.sh +++ b/scratchpad/night10/zonewalk.sh @@ -20,12 +20,12 @@ bt_expert_egg MP.EGG ZW.EGG sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; 0,/^vehicle=.*/s//vehicle=madcat/" ZW.EGG # TARGET first (right/back window): spins, logs every damage application. -BT_SPIN_SELF=15 BT_DMG_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 \ +BT_SPIN_SELF=15 BT_DMG_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 \ bt_launch zw_b.log ZW.EGG 0x0C -net 1601 sleep 2 # SHOOTER second (foreground -- owns the pick focus): the walker. BT_ZONE_WALK=6 BT_GOTO=enemy BT_GOTO_STOP=90 BT_KEY_NOFOCUS=1 \ -BT_DMG_LOG=1 BT_RANGE_LOG=1 BT_MP_LOG=1 \ +BT_DMG_LOG=1 BT_RANGE_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 \ bt_launch zw_a.log ZW.EGG 0x03 -net 1501 sleep 5 python ../tools/btconsole.py ZW.EGG 127.0.0.1:1501 127.0.0.1:1601 > zw_relay.log 2>&1 &