diff --git a/emulator/vpx-device/vpxlog.cpp b/emulator/vpx-device/vpxlog.cpp index d062d41..24788d7 100644 --- a/emulator/vpx-device/vpxlog.cpp +++ b/emulator/vpx-device/vpxlog.cpp @@ -72,6 +72,7 @@ static int in_len = 0, in_pos = 0; * NULL => every weapon misfires ("boo-beep"). A real i860 answered this each * frame; our HLE never did. We now carry a real scene hit in the frame ack. */ static bool sect_armed = false; +static unsigned sect_mask = 0xffffffffu; /* query mask from the 0x26 arm */ /* Real reticle raycast (Moller-Trumbore against the live scene, center ray). */ static bool raycast_pick(unsigned *inst_out, unsigned *dcs_out, unsigned *gg_out, unsigned *geom_out, float xyz_out[3]); @@ -789,6 +790,7 @@ static struct VScene { /* game (full DPL) hierarchy: instance placement + articulation */ std::map inst_object; /* instance -> object */ std::map inst_w3; /* instance -> display mode word3 */ + std::map inst_mask; /* instance -> INTERSECT mask */ std::map dcs_mat; /* dcs -> local matrix */ std::map dcs_parent; /* dcs_link child->parent */ /* wire lights: dpl3-revive decode -- lmodel (type 6) / light (type 0xe) @@ -2113,6 +2115,11 @@ static void scene_burst(const unsigned char *p, size_t n) { * 2=billboard, 0/1=HIDDEN until armed (the parked player mech * is w3=1 pre-translocation). Stored for the assembly skip. */ if (nb >= 20) S.inst_w3[name] = rd_u32(d + 16); + /* INTERSECT mask @d+40 (verified live: buildings/floor/sky = + * 0xffffffff, the player's own mech parts + beams + missiles + * = 0). The AUTHENTIC pick exclusion: the game's sect query + * ANDs its mask against this. */ + if (nb >= 44) S.inst_mask[name] = rd_u32(d + 40); for (size_t o = 8; o + 3 < nb; o += 4) { unsigned val = rd_u32(d + o); if (val && val != 0xFFFFFFFFu) { @@ -2196,8 +2203,11 @@ static void scene_burst(const unsigned char *p, size_t n) { break; case 38: /* set_sect_pixel: game armed the continuous reticle pick. * From here the board is expected to return the hit in each - * frame reply -- that's what unblocks weapons fire. */ + * frame reply -- that's what unblocks weapons fire. + * Payload: [xp][yp][radius][mask]; keep the query mask for + * the per-instance INTERSECT test. */ sect_armed = true; + if (nb >= 16) sect_mask = rd_u32(d + 12); break; default: break; @@ -2287,27 +2297,19 @@ static bool raycast_pick(unsigned *inst_out, unsigned *dcs_out, unsigned *gg_out M16 world; dcs_world(di->first, cache, world); for (size_t ii = 0; ii < di->second.size(); ii++) { unsigned inst = di->second[ii]; - /* Skip hidden-until-armed instances (w3 0/1: laser beams, - * missile models, effects). Picking them is poison: the beam - * stretches to the pick point, the next pick hits the BEAM at - * that point and locks the target there (feedback loop); a - * volley's own missiles get picked and homed on; and a picked - * missile whose DCS deletes on impact hands the game a stale - * handle (the arena2 freeze). The reticle resolves the WORLD: - * terrain, buildings, vehicles. */ - std::map::const_iterator wi = - S.inst_w3.find(inst); - if (wi != S.inst_w3.end() && wi->second != 2 && wi->second != 3) + /* AUTHENTIC pick exclusion: per-instance INTERSECT mask + * (instance word 9) ANDed with the query mask from the 0x26 + * arm -- exactly what the real board did. The game authors it + * for us: own mech parts, laser beams, missiles and effects + * carry mask 0 (never picked -- no beam feedback loops, no + * homing on own ordnance, no stale missile handles); terrain, + * sky, buildings and ENEMY mechs carry INTERSECT_ALL, so + * point-blank hulls are targetable again (this replaced the + * interim min-range and hidden-until-armed gates). */ + std::map::const_iterator mi = + S.inst_mask.find(inst); + if (mi != S.inst_mask.end() && (mi->second & sect_mask) == 0) continue; - /* Own-mech exclusion is by MINIMUM RANGE, not ancestry: walking - * S.dcs_parent (dcs_link edges) falsely flagged the FLOOR and - * SKY as the player's subtree (links also chain sibling scene - * DCSs in this dialect) -- ground/sky aims went no-hit and every - * such trigger boo-beeped. The player's arms and muzzle flash - * all live within ~12 of the eye; everything legitimately - * targetable is farther. Sky/ground are REAL pickable entities - * (sky.mod, afloor): firing into them fires -- and misses -- - * exactly like the period pods. */ std::map::const_iterator oi = S.inst_object.find(inst); if (oi == S.inst_object.end()) continue; @@ -2351,7 +2353,7 @@ static bool raycast_pick(unsigned *inst_out, unsigned *dcs_out, unsigned *gg_out m16_xform(world, &vv[o1], w1); m16_xform(world, &vv[o2], w2); float t = ray_tri(O, D, w0, w1, w2); - if (t > 15.0f && t < best_t) { /* min pick range */ + if (t > 0.0f && t < best_t) { best_t = t; best_inst = inst; best_dcs = di->first; best_gg = gg; best_geo = geo; } @@ -2386,7 +2388,8 @@ static bool raycast_pick(unsigned *inst_out, unsigned *dcs_out, unsigned *gg_out static void scene_reset(void) { S.type.clear(); S.verts.clear(); S.polys.clear(); S.mat.clear(); S.ggmat.clear(); S.children.clear(); - S.inst_object.clear(); S.inst_w3.clear(); S.dcs_mat.clear(); + S.inst_object.clear(); S.inst_w3.clear(); S.inst_mask.clear(); + S.dcs_mat.clear(); S.dcs_parent.clear(); S.lights.clear(); S.view = VFrame(); S.geom_active = false; S.conn_active = false;