From 065c11459079c74fc642fece9c721745d37c04d4 Mon Sep 17 00:00:00 2001 From: arcattack Date: Mon, 13 Jul 2026 08:14:12 -0500 Subject: [PATCH] Impact-FX FORENSICS wave: the i860 specialfx engine, per-round detonations, the ram economy closed The "we're off the rails" reinvestigation -- a 5-thread evidence workflow (logs / PFX data / RES model lists / decomp / port audit) + the i860 firmware decode, then surgical fixes. Every claim tiered; the wrong turns are on the record in the KB alongside the corrections. THE OLD-STYLE SPECIALFX ENGINE [T1, firmware-decoded from VREND.MNG]: rebuilt scratchpad/i860dis.py (binutils opcode table); mapped the dispatch (data+0xdd0c; sfx trigger 0xf040cda0, install 0xf040cdc0, step ~0xf0413698, instance init 0xf04128d8); decoded the heat model EXACTLY: per 30Hz board frame h *= cool_a; RGB_ch = h_old*(h_new*cook_ch - 0.25) + 0.25 (K=0.25 = the ember floor @VA 0xF080; kill at h <= 1e-4 @0xF0A0); alpha fades cool_b x RAW dt (PER-SECOND -- the x30 scaling made laser hits invisible); 7s cap; y_off = the kill plane; "variance" is DEAD DATA (binary reads "variance", INI authors "varience"). The 13 descriptors (PPCHit/LaserHit/MissileHit/ Chunks/Sparks/Fireball...) parse from BTDPL.INI and render via the BTPfx layer -- heat bursts draw the FIERY sheet (brightness over fire), .PFX keeps GRAYSCALE (authored colours: DNBOOM orange, DDAM gray). HIT-PACKAGE CENSUS [T1, RES byte-verified]: ppchit=[8] lzrhit=[9] mghit=[7,11] canhit=[11] mslhit=[10,12,1023] explode=[6]; all 8 mech death lists identical. Effect routing corrected in BOTH consumers: <100 = specialfx, >=1000 = psfx. "SILVER MIST x5" ROOT CAUSE: SHKWAVE.PFX (mslhit's 1023) authors maxIssue=5 relPeriod=0.2 rate=1 -- the ONE file where rate contradicts the window; the emitter trusted rate -> 5 shells at exactly 1Hz. Emission rate now always maxIssue/releasePeriod. PER-ROUND DETONATIONS [T1 @004bef78]: every missile round spawns its own ExplosionModelFile at ITS impact (hull + terrain) -- a volley ripples 12 fireballs like the demos; rack-tube launch spread [T3] (GUIDED rounds only -- the slot-0 deflection sent every AFC100 shell 3.4deg left/2.5deg down: the phantom "4th gold beam"); replicant salvos detonate too (launcher index rides the visual push). Missile damage bundles through the shooter's messmgr with the launcher's subsystemID (mslhit fires at the consolidated point; the binary's dedup CONFIRMED [T1 @0049b784] -- a workflow agent's per-record claim REFUTED by direct decomp read). DAMAGE-BAND SEMANTICS [T1]: MechDeathHandler fires the CURRENT band effect on any damage rise (the binary's changed-flag coalescing) -- a mauled mech under fire smokes/burns per hit; bands 3/4 are authored fire plumes. THE RAM ECONOMY -- CLOSED (3 layers, measured live): 1. armor = POINTS (every zone: damageScale = 1/armorPoints, armor 50-140; the [zone-armor] BT_DMG_LOG dump); 2. StaticBounce prices rams with authored moverMass ~1.3e6 -> ~59,000 pts @10m/s; the binary dispatches it RAW but pod MP dropped it on the local replicant (MECH.CPP:986 warns) -- ram damage was NETWORK-INERT; our task-#47 replicant forwarding surfaced it as a one-shot. Port normalizes x1e-3 to the point economy [T3]; 3. contact EDGE (ramLastVictim/ramContactLinger): the binary's bounce made separation implicit; our gait-derived velocity re-priced full rams at 60Hz (the respawn explode-loop). One bump = one hit; pressed = BLOCK. PLUS: sfx size x0.5 + 1.25m visibility floor + hot-phase occlusion [T3]; particle pool 2048->8192 (missile traffic starved laser bursts -- the "intermittent effects"); replicant beam aging (a lost beam-END record pinned a stale beam on forever); UV-variant noise stamps (mask-safe mirror/swap); no scroll on particle stamps (material-path only); BT_FX_TEST/fxshot.py self-verification harness ([beam-draw]/[zone-armor]/[collide-tx] telemetry). KB: rendering.md (specialfx engine + census + closeout), combat-damage.md (ram economy + band semantics). Co-Authored-By: Claude Fable 5 --- context/combat-damage.md | 21 + context/rendering.md | 75 + engine/MUNGA_L4/L4VIDEO.cpp | 499 +++++- game/reconstructed/emitter.hpp | 20 + game/reconstructed/mech.cpp | 2 + game/reconstructed/mech.hpp | 10 + game/reconstructed/mech4.cpp | 212 ++- game/reconstructed/mechdmg.cpp | 26 + game/reconstructed/mislanch.cpp | 9 +- game/reconstructed/projweap.cpp | 6 +- scratchpad/fxshot.py | 56 + scratchpad/i860_opcodes.h | 507 ++++++ scratchpad/i860dis.py | 99 ++ scratchpad/sfx_engine.txt | 2944 +++++++++++++++++++++++++++++++ 14 files changed, 4404 insertions(+), 82 deletions(-) create mode 100644 scratchpad/fxshot.py create mode 100644 scratchpad/i860_opcodes.h create mode 100644 scratchpad/i860dis.py create mode 100644 scratchpad/sfx_engine.txt diff --git a/context/combat-damage.md b/context/combat-damage.md index a1b5e8c..b872df0 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -104,6 +104,27 @@ per ER-M laser; [[decomp-reference]] §5). Flying projectiles (LRM/autocannon) a reconstruction (`BTPushProjectile` — the 2007 Entity is too small for the binary's raw integrator offsets). [T2] +## The RAM economy — CLOSED (2026-07-12) [T1 evidence, T3 normalization] +Three-layer story, measured live + decomp-verified: +1. **The armor economy is POINTS**: every zone streams `damageScale[5]` EVEN = `1/armorPoints` + (armor 50–140; `damageLevel += amount/armor`, 1.0 = destroyed — the `[zone-armor]` dump, + BT_DMG_LOG, mechdmg.cpp). Weapons author point-scale amounts (laser 11.77 ≈ 10 torso hits). ✓ +2. **StaticBounce prices rams with the AUTHORED moverMass ≈ 1.3e6 units** → ~600×v² ≈ 59,000 + points for a 10 m/s ram (measured: 59221@9.94, 398@0.81; `[collide-tx]` now logs mass/e). + The binary DISPATCHES it raw (@part_012:15324-15358) — but in pod MP it landed on the local + REPLICANT of the victim (the master never heard it; MECH.CPP:986 warns and proceeds) — + **ram damage was network-inert in the pod**. Our task-#47 replicant forwarding (required + for MP weapon damage) surfaced it as a one-shot. Port: `BTDispatchCollisionDamage` + normalizes ×1e-3 to the armor-point economy (bump = points, charge = tens) [T3]. +3. **The contact EDGE**: the binary's bounce reversed the mover's velocity so the frame after + a bump read as separating; our gait re-derives velocity per frame (still closing while + pressed / respawn-overlapped) → sustained contact re-priced full rams at 60Hz (the respawn + explode-loop). Port: `ramLastVictim`/`ramContactLinger` (mech.hpp) — one bump = one damage + event; pressed = BLOCK; re-arms 0.35s after separation; cleared on Reset. +Band effects: `MechDeathHandler::Tick` fires the CURRENT band descriptor on any damage rise +(the binary's changed-flag semantics, workflow-verified) — a damaged mech under fire smokes/ +burns per hit; the earlier crossing-only gate was over-tight (the metronome was SHKWAVE). + ## Damage delivery + the real damage model `Entity::TakeDamageMessage(id, size, inflictingEntityID, zone, Damage&)` → `target->Dispatch`. **Base handler IGNORES zone==−1** (`Entity::TakeDamageMessageHandler`, ENTITY.cpp:878 — returns on diff --git a/context/rendering.md b/context/rendering.md index 0da8ea9..1f44c7f 100644 --- a/context/rendering.md +++ b/context/rendering.md @@ -75,6 +75,81 @@ the slice. Reading it as RGB overlaid 2-3 gray sheets = the "rainbow/graffiti" b resolves the mech-skin `softer` ramp (defined in OTHER libs). Every mech renders authentic gray-metal paint now. See [[asset-formats]]. [T2] +## The OLD-STYLE SPECIALFX layer (the weapon-impact action) — RECONSTRUCTED 2026-07-12 (firmware-decoded) +The per-weapon impact visuals are fully AUTHORED in `content/BTDPL.INI` `[effects_to_load]`: +`specialfx3..15` = Chunks1/Chunks2/Sparks/Explosion3/MachineGun/PPCHit/LaserHit/MissileHit/ +CannonHit/GroundHit/MinorHit/MajordHit/Fireball — "old-style" (version-1) descriptor sections +(`texture/type/size/velocity/v_bias/y_off/cook(3f)/varience/gravity/cool(2f)/count/repeats`), +most on `btfx:firesmoke1_scr_tex` (MissileHit size 12 ×7; Fireball size 14, gravity −1.5 rises; +Chunks1/2 = ballistic debris vel 40-55 grav 34-35; Sparks NOTEX). [T1: the 1995 binary parses +them — part_008.c:1546-1590, field strings @004f25e4-004f2663 → installer FUN_00490210 packs a +0x44-byte board command via FUN_0048e2c0.] **Simulation semantics (type/cook/cool) live in the +i860 firmware** `content/VREND.MNG` (same decode workflow as the task-#55 'damageize' handler). +- **EFFECT-NUMBER ROUTING (corrects the old claim):** `effect_number < 100` = the dpl board + EFFECT TABLE = this specialfx list (1996 case-4 comments match: 3=Chunks, 12=GroundHit, + 15=Fireball), **NOT** a psfx slot; `>= 1000` = psfx slot n−1000 (idam/ddam PFX); 100-999 = + ExplosionScripts. The port's `<100 → psfx slot` mapping renders PPCHit(8) as ddam5 smoke and + Chunks(3)/Sparks(5) as ddamN smoke — the "hits are just a little smoke" report. The 2007 + WinTesla loader SKIPS all version-1 descriptors (`if (version < 2) continue;` — it only reads + the 1998 INDIE format), which is why the layer was never seen. [T1] +- **Live hit census (mp logs):** weapon hits dispatch idam bands 1009-1013 (30×1009) + raw + 8/9 (PPCHit/LaserHit, mis-rendered); **MissileHit(10)/CannonHit(11) never appeared** — the + projectile/salvo damage path likely doesn't thread the firing launcher's subsystemID into the + damage records, so messmgr never queues its explosion. [T2 census; T4 cause] +- **RECONSTRUCTION (2026-07-12, firmware-first):** the i860 sim decoded from VREND.MNG + (dispatch table at data+0xdd0c, VA base: code 0xf0400000, data 0x1000; handlers: sfx trigger + 0xf040cda0→ca48, install 0xf040cdc0→cba0, step near 0xf0413698, spawn near 0xf0414afc — + string-anchored via "attempt to step a NULL sfx object"; disassembler REBUILT at + `scratchpad/i860dis.py` from binutils i860 opcodes, `scratchpad/i860_opcodes.h`). + **The heat model [T1]:** per 30Hz board frame `h *= cool_a`; `RGB_ch = h_old*(h_new*cook_ch + − 0.25) + 0.25` (K=0.25 @VA 0xF080 = the ember floor); kill at h ≤ 1e-4 (@0xF0A0); alpha −= + cool_b per frame; `pos += vel`, gravity as accel. y_off = the particle KILL PLANE + (Chunks −1 = die at the ground) [T3 data-indicated]. Burst frame randomly spun (quadratic + small-angle rot, θ from the host-fed random table cmd 0x21). Implemented in L4VIDEO.cpp: + `BTInstallSfx`/`BTStartSfx` + sfxMode particles in the BTPfx layer; parser beside the psfx + page walk (`[sfx] page 'effects_to_load': 13 old-style specialfx installed`). Effect-number + routing fixed in BOTH consumers (ExplosionClassID + DPLIndependantEffect): <100 → specialfx, + ≥1000 → psfx slot n−1000. **Missile/cannon hits now bundle** [T2]: BTProjectile carries + shooter + weaponSubsys; contact routes through the SHOOTER's SubsystemMessageManager + (AddDamageMessage w/ subsystemID → mslhit/acanhit explosion at the impact), mirroring the + laser path's SendDamageMessage. Cone shape (v_bias as up-bias) + per-type draw weights are + [T3]; the full type-0/1/2 draw-geometry decode in the firmware draw fn (0xf0416160) remains + open. + +## Weapon-hit forensics closeout (2026-07-12, workflow-verified) [T1] +- **The HIT PACKAGES (BTL4.RES model lists, byte-verified):** ppchit=[8], lzrhit=[9], + mghit=[7,11], canhit=[11] (no "acanhit"), mslhit=**[10, 12, 1023]** (MissileHit + GroundHit + + psfx23 SHKWAVE), gndhit=[12], minhit=[1002], majhit=[1003], explode=[6]; dam1-4hit=[1002-1005], + desthit/stephit=[1008]; cockpit "i" variants (lzrhiti etc.) = same sfx + idam 1009-1013; ALL + eight mech death lists = ["104","3","4","5","1007","15","1001"]. +- **"Silver mist exactly 5×, perfectly timed" ROOT CAUSE:** SHKWAVE.PFX authors maxIssue=5, + releasePeriod=0.2, **rate=1** — the ONE shipped .PFX where rate ≠ maxIssue/releasePeriod; the + emitter trusted 'rate' → 5 huge white shells (exp 350/s) at exactly 1Hz. **Emission-window + rule hardened:** effective rate = maxIssue/releasePeriod always (BTDrawPfx emitter). +- **SpecialFX palette [T1 firmware]:** instance init @0xf04128d8: inst[0x19c]=cool_a (per + board-frame heat decay), inst[0x1a4]=cool_b (× raw dt in the step = PER-SECOND fade), + inst[0x188]=7.0s cap, heat=1.0. The heat channels are BRIGHTNESS over an inherently + FIRE-coloured texture (firesmoke through its material ramp): sfx batch draws the FIERY-ramped + sheet, psfx keeps GRAYSCALE (authored colours decide — DNBOOM orange, DDAM gray). Drawing sfx + grayscale = silver mist; scaling fade ×30 = 0.1s pops ("nothing happens on laser hits"). +- **Damage-band .PFX shapes are authored GENTLE (puffy clouds):** bands 1-4 of ndam/idam/ddam + rise at 1-4 u/s with buoyant accel; ONLY band 5 + the BOOM/SHKWAVE files are explosive-omni. + idam = scaled-down WORLD venting smoke (half radius/counts of ddam), not cockpit-only. + N-set = night = same physics as D-set with fewer particles. NSRM/DSRM trails = 0.1s overbright + ORANGE directional puffs (never silver). +- **messmgr explosion DEDUP CONFIRMED [T1, decomp-read @0049b784]:** `Find` (weaponExplosions + vtable slot 0xC) gates `AddValue` (slot 8) — ONE explosion per unique weapon resource per + frame, exactly as reconstructed. (A workflow agent claimed "one per record, staggered + 0.1s" — REFUTED; the stagger applies to the deduped queue.) Missile multiplicity comes from + the ORDNANCE side instead: Missile::MoveAndCollide @004bef78 detonates per round — ported as + BTSpawnRoundDetonation (mech4) + rack-tube launch spread [T3]. Alpha-strike drama = distinct + per-weapon-TYPE packages (lzrhit+ppchit) + the victim's band fire, not explosion count. +- Remaining [T3]: sfx cone shape (v_bias as up-bias), size ×0.5 + 1.25m textured floor + + hot-phase occlusion 0.45..0.9, burst scatter ×0.2; trail + density is frame-rate-dependent (2/frame @60fps = 2× pod density); .PFX 'repeats' unconsumed + (all shipped files author 1); replicant visual salvo chases the stale fire-time aim point + (no re-lead without a target handle). + ## Weapon beams (the unported dpl_* layer) The `dpl_*` beam renderable was never ported → beams are drawn by `BTPushBeam`/`BTDrawBeams` (L4VIDEO). The real look is the `ermlaser.bgf` TUBE (native Z 0..−2000, UVs tiled) with the scrolling diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index ac47f58..a3aa3f1 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -677,6 +677,24 @@ struct BTPfxParticle float rad, exp, dexp; float colorWarp, alphaWarp; // def-level warps, carried per particle float sI[4], sO[4], eI[4], eO[4]; + // OLD-STYLE SPECIALFX mode (the VREND.MNG heat model, firmware-decoded + // 2026-07-12): when sfxMode != 0 the colour comes from the board's heat + // equation instead of the .PFX colour quads: + // h *= coolMul per board frame (30 Hz); + // channel = h_old*(h_new*cook_ch - 0.25) + 0.25 (0.25 = the ember floor, + // firmware constant @VA 0xF080); kill at h <= 1e-4 (@0xF0A0), at + // fade <= 0 (fade -= coolSub per board frame), or below the kill plane + // (the descriptor's y_off -- Chunks die 1m under the burst: the floor). + int sfxMode; + float heat, coolMul, coolSub, fade; + float cookR, cookG, cookB; + float killY; + int sfxType; // descriptor 'type' (draw weight) + int uvVariant; // 0..7 mirror/swap of the noise UVs + // (de-correlates the stamps like the + // board's random burst spin; mirror/ + // swap keeps UVs in 0..1 so the + // CLAMPed radial mask stays valid) }; static std::vector gBTPfxEmitters; static std::vector gBTPfxParticles; @@ -769,7 +787,7 @@ void BTStartPfxFrame(int effect_number, float x, float y, float z, const BTPfxDef &d = gBTPfxDefs[effect_number]; if (!d.valid) return; - if (gBTPfxEmitters.size() > 64) // runaway guard + if (gBTPfxEmitters.size() > 256) // runaway guard return; BTPfxEmitter e; e.def = &d; @@ -821,7 +839,8 @@ void BTPfxTrailPuff(int effect_number, float x, float y, float z, static void BTPfxSpawn(const BTPfxEmitter &e) { const BTPfxDef &d = *e.def; - if (gBTPfxParticles.size() > 2048) // global particle cap + if (gBTPfxParticles.size() > 8192) // global particle cap (2048 starved + // laser bursts during missile volleys) return; BTPfxParticle p; // Sample in the effect's LOCAL frame, then orient into the world through @@ -859,9 +878,126 @@ static void BTPfxSpawn(const BTPfxEmitter &e) p.eI[i] = d.eI[i] + d.eIv[i] * BTPfxRand01(); p.eO[i] = d.eO[i] + d.eOv[i] * BTPfxRand01(); } + p.sfxMode = 0; // .PFX path: authored colour quads + p.uvVariant = (int)(BTPfxRand01() * 7.99f); gBTPfxParticles.push_back(p); } +//===========================================================================// +// OLD-STYLE SPECIALFX (the "more fire and 'splosions" layer) -- the 1995 +// per-weapon impact effects: BTDPL.INI [effects_to_load] "specialfxN=Name" + +// [Name] descriptor sections (Chunks1/Sparks/PPCHit/LaserHit/MissileHit/ +// Fireball/...). These are the dpl EFFECT-TABLE entries that effect numbers +// < 100 index (the 1996 ExplosionScripts' own comments match: 3=Chunks, +// 12=GroundHit, 15=Fireball) -- NOT psfx slots. +// +// SEMANTICS [T1, decoded from the i860 firmware VREND.MNG 2026-07-12]: +// - host installer FUN_00490210 ships the descriptor VERBATIM (cmd 0x1C, +// 0x44 bytes; trigger = cmd 0x1B {id, dcs, 4x4 matrix}); +// - per board frame (30 Hz): h *= cool_a; RGB_ch = h_old*(h_new*cook_ch +// - 0.25) + 0.25 (superbright start -> 0.25 ember floor; constants pool +// @VA 0xF070: K=0.25 @0xF080, kill floor 1e-4 @0xF0A0); +// - alpha fades linearly (the step: out[0x40] -= dt * rate); +// - pos += vel; the burst frame is randomly SPUN per trigger (quadratic +// small-angle rotation, theta from the host-fed random table); +// - "variance" is DEAD DATA in the shipping pod: the binary looks up +// "variance" (@004f2604) but every INI section spells "varience" -- the +// lookup missed and the field stayed 0. Honoured (not parsed). +// - y_off reads as the particle KILL PLANE relative to the burst origin +// (Chunks -1 = die at the ground; hits -3) [T3 -- strongly data-indicated]. +//===========================================================================// +struct BTSfxDef +{ + int valid; + int type; // 0 = chunks (solid debris), 1/2 = fiery ball + int noTex; // texture=NOTEX(T): untextured sparks + float size, velocity, vBias, yOff; + float cook[3]; // RGB heat gains + float gravity; + float coolMul, coolSub; + int count, repeats; +}; +#define BT_SFX_SLOTS 32 +static BTSfxDef gBTSfxDefs[BT_SFX_SLOTS]; + +int BTInstallSfx(int slot, int type, int no_tex, float size, float velocity, + float v_bias, float y_off, const float cook[3], float gravity, + float cool_mul, float cool_sub, int count, int repeats) +{ + if (slot < 0 || slot >= BT_SFX_SLOTS) + return 0; + BTSfxDef &d = gBTSfxDefs[slot]; + d.type = type; d.noTex = no_tex; + d.size = size; d.velocity = velocity; d.vBias = v_bias; d.yOff = y_off; + d.cook[0] = cook[0]; d.cook[1] = cook[1]; d.cook[2] = cook[2]; + d.gravity = gravity; d.coolMul = cool_mul; d.coolSub = cool_sub; + d.count = count > 0 ? count : 1; + d.repeats = repeats > 0 ? repeats : 1; + d.valid = 1; + return 1; +} + +void BTStartSfx(int effect_number, float x, float y, float z) +{ + if (effect_number < 0 || effect_number >= BT_SFX_SLOTS) + return; + const BTSfxDef &d = gBTSfxDefs[effect_number]; + if (!d.valid) + return; + for (int i = 0; i < d.count; ++i) + { + if (gBTPfxParticles.size() > 8192) + return; + BTPfxParticle p; + memset(&p, 0, sizeof(p)); + // Random burst direction; v_bias biases it upward (Sparks 1.4 = + // fountain, GroundHit 0.01 = flat ring, Fireball 0.99 = rising + // column -- the authored values read cleanly under this model). + // The firmware spins the whole burst frame by a random angle; + // per-particle isotropic randoms give the same coverage. + float ax = BTPfxRand01() * 2.0f - 1.0f; + float ay = BTPfxRand01() * 2.0f - 1.0f; + float az = BTPfxRand01() * 2.0f - 1.0f; + float dl = sqrtf(ax * ax + ay * ay + az * az); + if (dl < 1e-4f) { ax = 0; ay = 1; az = 0; dl = 1; } + ax /= dl; ay /= dl; az /= dl; + ay = fabsf(ay) * 0.3f + d.vBias; // upward bias + dl = sqrtf(ax * ax + ay * ay + az * az); + ax /= dl; ay /= dl; az /= dl; + float spd = d.velocity * (0.6f + 0.4f * BTPfxRand01()); + // burst scatter: break the perfect overlap of count co-spawned + // sprites (the flash read as a single smooth ball) [T3] + float sc = d.size * 0.2f; + p.pos = D3DXVECTOR3( + x + sc * (BTPfxRand01() * 2.0f - 1.0f), + y + sc * (BTPfxRand01() * 2.0f - 1.0f), + z + sc * (BTPfxRand01() * 2.0f - 1.0f)); + p.vel = D3DXVECTOR3(ax * spd, ay * spd, az * spd); + p.accel = D3DXVECTOR3(0.0f, -d.gravity, 0.0f); + p.age = 0.0f; + p.life = 10.0f; // real kills: heat/fade/plane + // size scale [T3]: x0.5 (size ~ diameter) with a 1.25m visibility + // floor for textured bursts -- combat ranges run 100m+; LaserHit + // (authored size 1.9) at <1m rendered ~8px = "practically nothing" + // (user-rejected). MissileHit 12 -> 6m radius core. Sparks/NOTEX + // keep their authored tininess (they read by count, not size). + p.rad = (d.size > 0.01f) ? d.size * 0.5f : 0.15f; + if (!d.noTex && d.size > 0.01f && p.rad < 1.25f) p.rad = 1.25f; + p.exp = 0.0f; p.dexp = 0.0f; + p.colorWarp = 1.0f; p.alphaWarp = 1.0f; + p.sfxMode = 1; + p.heat = 1.0f; + p.coolMul = (d.coolMul > 0.0f && d.coolMul < 1.0f) ? d.coolMul : 0.9f; + p.coolSub = d.coolSub > 0.0f ? d.coolSub : 0.02f; + p.fade = 1.0f; + p.cookR = d.cook[0]; p.cookG = d.cook[1]; p.cookB = d.cook[2]; + p.killY = y + d.yOff; + p.sfxType = d.type; + p.uvVariant = (int)(BTPfxRand01() * 7.99f); + gBTPfxParticles.push_back(p); + } +} + // The beams' decoded grit texture (built in BTDrawBeams) -- shared with the // particles so the fire quads carry the authentic firesmoke-family noise. extern LPDIRECT3DTEXTURE9 BTGetBeamGritTexture(); @@ -870,6 +1006,60 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt) { // ---- sim ---- if (dt > 0.1f) dt = 0.1f; // stall guard + + // DEV HARNESS: BT_FX_TEST="10" (or "10,1003,15") fires the listed effect + // numbers every 3s, 30m in front of the camera -- iterate the effect look + // via screenshots without a live fire loop. <100 = specialfx descriptor, + // >=1000 = psfx slot n-1000 (the real dispatch encodings). + { + static int s_fxTestN = -1; + static int s_fxTest[8]; + static float s_fxTestTimer = 0.0f; + if (s_fxTestN < 0) + { + s_fxTestN = 0; + const char *tv = getenv("BT_FX_TEST"); + if (tv != 0) + { + const char *p = tv; + while (*p && s_fxTestN < 8) + { + s_fxTest[s_fxTestN++] = atoi(p); + while (*p && *p != ',') ++p; + if (*p == ',') ++p; + } + } + } + if (s_fxTestN > 0) + { + s_fxTestTimer -= dt; + if (s_fxTestTimer <= 0.0f) + { + s_fxTestTimer = 3.0f; + float cx, cy, cz; + d3d_OBJECT::GetCameraPosition(&cx, &cy, &cz); + // fire on BOTH sides of the camera (view-matrix handedness + // varies; one of the two is dead-centre in frame) + for (int side = -1; side <= 1; side += 2) + { + float fx = cx + view->_13 * 30.0f * side; + float fy = cy + view->_23 * 30.0f * side + 4.0f; + float fz = cz + view->_33 * 30.0f * side; + for (int i = 0; i < s_fxTestN; ++i) + { + extern void BTStartSfx(int, float, float, float); + if (s_fxTest[i] >= 1000) + BTStartPfx(s_fxTest[i] - 1000, fx, fy, fz); + else + BTStartSfx(s_fxTest[i], fx, fy, fz); + } + } + DEBUG_STREAM << "[fxtest] fired +-30m of cam (" << cx << "," << cy << "," << cz + << ") particles=" << (int)gBTPfxParticles.size() + << " emitters=" << (int)gBTPfxEmitters.size() << "\n" << std::flush; + } + } + } for (size_t ei = 0; ei < gBTPfxEmitters.size(); ++ei) { BTPfxEmitter &e = gBTPfxEmitters[ei]; @@ -881,7 +1071,17 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt) // DDAM2 35/16~=2, DDTHSMK 30/3=10) -- release_period IS the emission // WINDOW, not a batch interval. (The old one-burst batching dumped a // 10-second smoke plume in a single frame's puff.) - e.emitAccum += d.rate * dt; + // EMISSION-WINDOW RULE (hardened 2026-07-12): release_period IS the + // emission window -- derive the rate from maxIssue/releasePeriod + // instead of trusting the authored 'rate' field. SHKWAVE.PFX (the + // missile-hit shockwave, mslhit's 1023) authors maxIssue=5, + // releasePeriod=0.2, rate=1 -- the one shipped file where they + // disagree; trusting 'rate' stretched the single 0.2s layered + // 5-shell blast into 5 metronomic 1Hz pulses ("silver mist exactly + // 5 times at perfectly timed intervals" -- user-reported). + float erate = (d.releasePeriod > 1e-3f) + ? (float)d.maxIssue / d.releasePeriod : d.rate; + e.emitAccum += erate * dt; int n = (int)e.emitAccum; if (n > 0) { @@ -901,12 +1101,28 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt) gBTPfxEmitters.resize(w); } { // advance + expire particles + const float boardSteps = dt * 30.0f; // the pod stepped the sfx sim per 30Hz frame size_t w = 0; for (size_t i = 0; i < gBTPfxParticles.size(); ++i) { BTPfxParticle &p = gBTPfxParticles[i]; p.age += dt; if (p.age >= p.life) continue; + if (p.sfxMode) + { + // the firmware kills (heat floor 1e-4 @0xF0A0; the y_off kill + // plane). TIMEBASE [T1, re-read from the step disasm]: heat + // decays per BOARD FRAME (f18 = f17*f16, no dt -- hence the + // 30Hz boardSteps exponent) but the alpha fade is multiplied + // by RAW dt (@f0413754-64: out[0x40] -= dt * inst[0x1a4]) -- + // cool_b is PER-SECOND. The earlier x30 scaling killed every + // burst in ~0.1s: fireballs read as a subliminal pop and + // laser hits as "nothing happens" (user-reported). + p.heat *= powf(p.coolMul, boardSteps); + p.fade -= p.coolSub * dt; + if (p.heat <= 1e-4f || p.fade <= 0.0f) continue; + if (p.pos.y < p.killY) continue; + } p.vel += p.accel * dt; p.pos += p.vel * dt; p.rad += p.exp * dt; @@ -977,6 +1193,10 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt) // Fallback when BINTA.VTX is absent: the old grit x radial single-tex bake. static LPDIRECT3DTEXTURE9 s_pfxTex = 0; // fallback single-texture static LPDIRECT3DTEXTURE9 s_pfxNoise = 0; // bintA through the "fiery" ramp + static LPDIRECT3DTEXTURE9 s_pfxNoiseGray = 0; // RAW bintA (grayscale) -- the + // old-style specialfx sample the + // TEXTURE, not the ramped material: + // the heat model supplies ALL colour static LPDIRECT3DTEXTURE9 s_pfxMask = 0; // radial falloff (stage 1, CLAMP) static int s_pfxTexTried = 0; if (!s_pfxTexTried) @@ -1013,6 +1233,28 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt) } else { s_pfxNoise->Release(); s_pfxNoise = 0; } } + if (s_pfxNoise && fireImg.ok && + SUCCEEDED(dev->CreateTexture(fireImg.w, fireImg.h, 1, 0, D3DFMT_A8R8G8B8, + D3DPOOL_MANAGED, &s_pfxNoiseGray, NULL))) + { + D3DLOCKED_RECT lr; + if (SUCCEEDED(s_pfxNoiseGray->LockRect(0, &lr, NULL, 0))) + { + for (int y = 0; y < fireImg.h; ++y) + { + DWORD *dst = (DWORD *)((char *)lr.pBits + y * lr.Pitch); + for (int x = 0; x < fireImg.w; ++x) + { + uint32_t s = fireImg.argb[(size_t)y * fireImg.w + x]; + int l = (((s >> 16) & 0xFF) + ((s >> 8) & 0xFF) + (s & 0xFF)) / 3; + int a = (int)((0.35f + 0.65f * (l / 255.0f)) * 255.0f); + dst[x] = ((DWORD)a << 24) | (l << 16) | (l << 8) | l; + } + } + s_pfxNoiseGray->UnlockRect(0); + } + else { s_pfxNoiseGray->Release(); s_pfxNoiseGray = 0; } + } if (s_pfxNoise && SUCCEEDED(dev->CreateTexture(TW, TH, 1, 0, D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, &s_pfxMask, NULL))) @@ -1099,29 +1341,32 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt) } } - // The authored SCROLL clock (firesmoke1's rate; the shipped variants share - // the sheet and differ only in rate, so the port rolls every sprite at the - // dominant one). Accumulate the OFFSETS and wrap each into [0,1) -- an - // unbounded time * rate loses float precision over a long session (the - // tsphere scroll-precision lesson). - static float s_pfxScrollU = 0.0f, s_pfxScrollV = 0.0f; - s_pfxScrollU += 0.05f * dt; s_pfxScrollU -= floorf(s_pfxScrollU); - s_pfxScrollV += -0.331f * dt; s_pfxScrollV -= floorf(s_pfxScrollV); if (s_pfxNoise) { - // stage 0: diffuse x the scrolled fire noise (WRAPped) - dev->SetTexture(0, s_pfxNoise); + // stage 0: diffuse x the scrolled noise (WRAPped). GRAYSCALE: every + // .PFX and specialfx descriptor references the TEXTURE + // btfx:firesmokeN_scr_tex (the raw bintA noise), NOT the fiery-ramped + // material -- the AUTHORED per-particle colours are the sole colour + // source (DNBOOM's 0.8,0.3,0 = the orange fire; DDAM's 0.5-gray = the + // smoke). The earlier ramp-baked sheet gilded the gray smoke bands + // into "gold blasts of light" (user-rejected). The fiery ramp + // belongs to the MATERIAL path only (flamebig etc. via bgfload). + dev->SetTexture(0, s_pfxNoiseGray ? s_pfxNoiseGray : s_pfxNoise); dev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); dev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE); dev->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TEXTURE); dev->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE); dev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE); dev->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_TEXTURE); - D3DXMATRIX tm; D3DXMatrixIdentity(&tm); - tm._31 = s_pfxScrollU; tm._32 = s_pfxScrollV; // (u,v) translation - dev->SetTransform(D3DTS_TEXTURE0, &tm); - dev->SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2); + // NO SCROLL on particle stamps: the authored SCROLL belongs to the + // TEXTURE record for the MATERIAL path (flame meshes); sliding the + // noise through short-lived particle sprites reads as a shimmering + // crawl ("not sure what I'm looking at" -- user-rejected). Each + // particle instead gets a FIXED random window into the tileable + // sheet (u0/v0, set at spawn; the WRAP addressing makes any window + // valid) -- the board's per-burst random spin de-correlation. + dev->SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_DISABLE); dev->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP); dev->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_WRAP); // stage 1: x the CLAMPed radial mask (same UV set, UNscrolled) -- both @@ -1158,6 +1403,15 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt) struct V { float x, y, z; DWORD c; float u, v; }; static std::vector verts; verts.clear(); + // SPECIALFX batch: drawn with the FIERY-RAMPED sheet. On the IG board the + // heat channels are BRIGHTNESS multipliers over an inherently fire- + // coloured texture (firesmoke through its material ramp): white-hot flash + // -> ember-orange dissipating cloud (the demo fireballs). The .PFX batch + // keeps the GRAYSCALE sheet (its authored colours decide: DNBOOM orange + // fire, DDAM gray smoke). Drawing sfx ember clouds grayscale made them + // "explosive silver mist" (user-rejected). + static std::vector sfxVerts; + sfxVerts.clear(); // BACK-TO-FRONT order: premultiplied occlusion (smoke) composites in depth // order; unsorted draws pop when a near puff renders before a far one. @@ -1185,16 +1439,54 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt) float tc = powf(t, 1.0f / p.colorWarp); float ta = powf(t, 1.0f / p.alphaWarp); - // two quads: outer glow (2.2x radius) then inner core - for (int layer = 0; layer < 2; ++layer) + // The .PFX sI/sO are the INTERIOR/EXTERIOR colours of ONE sprite (the + // dpl_PARTICLE model) -- approximated as an exterior quad at the + // AUTHORED radius + an interior core at 0.45x, with the exterior's + // occlusion halved (it is the sprite's soft skirt, not a second + // particle). The old 2.2x "outer glow" inflated every particle and + // doubled its occlusion -- 20 overlapping damage-smoke particles + // merged into one giant black blob (seen in the BT_FX_TEST captures). + // Old-style specialfx draw ONE sprite per particle (the firmware + // draw emits a single projected sprite). + for (int layer = (p.sfxMode ? 1 : 0); layer < 2; ++layer) { const float *cs = layer ? p.sI : p.sO; const float *ce = layer ? p.eI : p.eO; - float scale = layer ? 1.0f : 2.2f; - float r = cs[0] + (ce[0] - cs[0]) * tc; - float g = cs[1] + (ce[1] - cs[1]) * tc; - float b = cs[2] + (ce[2] - cs[2]) * tc; - float a = cs[3] + (ce[3] - cs[3]) * ta; + float scale = layer ? 0.45f : 1.0f; + if (p.sfxMode) scale = 1.0f; + float r, g, b, a; + if (p.sfxMode) + { + // THE FIRMWARE HEAT EQUATION [T1]: channel = h_old*(h_new* + // cook_ch - 0.25) + 0.25. h was already stepped in the sim + // above; approximate h_old over this frame's step from the + // per-frame factor (h_old = h / coolMul^steps was h before + // the step -- one step back is close enough at 30Hz). + float hNew = p.heat; + float hOld = p.heat / p.coolMul; // one board step back + if (hOld > 1.0f) hOld = 1.0f; + r = hOld * (hNew * p.cookR - 0.25f) + 0.25f; + g = hOld * (hNew * p.cookG - 0.25f) + 0.25f; + b = hOld * (hNew * p.cookB - 0.25f) + 0.25f; + if (r < 0) r = 0; if (r > 1) r = 1; + if (g < 0) g = 0; if (g > 1) g = 1; + if (b < 0) b = 0; if (b > 1) b = 1; + // occlusion weight: solid debris chunks occlude; fiery balls + // mostly-occlude while HOT (bright fire body), thinning as + // the heat dies (0.5 halved the flash into a faint wisp at + // combat range -- "practically nothing", user-rejected) + float occl = (p.sfxType == 0) ? 0.85f + : (0.45f + 0.45f * (p.heat > 1.0f ? 1.0f : p.heat)); + a = p.fade * occl; + } + else + { + r = cs[0] + (ce[0] - cs[0]) * tc; + g = cs[1] + (ce[1] - cs[1]) * tc; + b = cs[2] + (ce[2] - cs[2]) * tc; + a = cs[3] + (ce[3] - cs[3]) * ta; + if (layer == 0) a *= 0.5f; // exterior skirt: half occlusion + } if (a <= 0.0f) continue; if (a > 1.0f) a = 1.0f; // PREMULTIPLIED: vertex rgb = colour x alpha (the framebuffer @@ -1210,20 +1502,62 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt) float rad = p.rad * scale; D3DXVECTOR3 rv = right * rad, uv = up * rad; + // noise-stamp orientation: mirror/swap the 0..1 UVs by the + // particle's variant (stays in 0..1 -> the CLAMPed mask holds) + float cu[4] = { 0, 0, 1, 1 }; // corners: BL, TL, TR, BR + float cv[4] = { 1, 0, 0, 1 }; + for (int k = 0; k < 4; ++k) + { + float uu2 = cu[k], vv2 = cv[k]; + if (p.uvVariant & 1) uu2 = 1.0f - uu2; + if (p.uvVariant & 2) vv2 = 1.0f - vv2; + if (p.uvVariant & 4) { float t = uu2; uu2 = vv2; vv2 = t; } + cu[k] = uu2; cv[k] = vv2; + } V q[6]; - q[0].x = p.pos.x - rv.x - uv.x; q[0].y = p.pos.y - rv.y - uv.y; q[0].z = p.pos.z - rv.z - uv.z; q[0].u = 0; q[0].v = 1; - q[1].x = p.pos.x - rv.x + uv.x; q[1].y = p.pos.y - rv.y + uv.y; q[1].z = p.pos.z - rv.z + uv.z; q[1].u = 0; q[1].v = 0; - q[2].x = p.pos.x + rv.x + uv.x; q[2].y = p.pos.y + rv.y + uv.y; q[2].z = p.pos.z + rv.z + uv.z; q[2].u = 1; q[2].v = 0; + q[0].x = p.pos.x - rv.x - uv.x; q[0].y = p.pos.y - rv.y - uv.y; q[0].z = p.pos.z - rv.z - uv.z; q[0].u = cu[0]; q[0].v = cv[0]; + q[1].x = p.pos.x - rv.x + uv.x; q[1].y = p.pos.y - rv.y + uv.y; q[1].z = p.pos.z - rv.z + uv.z; q[1].u = cu[1]; q[1].v = cv[1]; + q[2].x = p.pos.x + rv.x + uv.x; q[2].y = p.pos.y + rv.y + uv.y; q[2].z = p.pos.z + rv.z + uv.z; q[2].u = cu[2]; q[2].v = cv[2]; q[3] = q[0]; q[4] = q[2]; - q[5].x = p.pos.x + rv.x - uv.x; q[5].y = p.pos.y + rv.y - uv.y; q[5].z = p.pos.z + rv.z - uv.z; q[5].u = 1; q[5].v = 1; - for (int k = 0; k < 6; ++k) { q[k].c = c; verts.push_back(q[k]); } + q[5].x = p.pos.x + rv.x - uv.x; q[5].y = p.pos.y + rv.y - uv.y; q[5].z = p.pos.z + rv.z - uv.z; q[5].u = cu[3]; q[5].v = cv[3]; + std::vector &sink = p.sfxMode ? sfxVerts : verts; + for (int k = 0; k < 6; ++k) { q[k].c = c; sink.push_back(q[k]); } } } + // DIAG (BT_FX_TEST): prove the draw reaches pixels -- particle count, the + // vertex count, the first particle's colour/alpha/pos, texture presence. + if (getenv("BT_FX_TEST") != 0 && !gBTPfxParticles.empty()) + { + static float s_drawLog = 0.0f; + s_drawLog += dt; + if (s_drawLog >= 2.0f) + { + s_drawLog = 0.0f; + const BTPfxParticle &p0 = gBTPfxParticles[0]; + DEBUG_STREAM << "[pfx-draw] n=" << (int)gBTPfxParticles.size() + << " verts=" << (int)verts.size() + << " tex=" << (s_pfxNoiseGray ? "gray" : (s_pfxNoise ? "fiery" : (s_pfxTex ? "grit" : "NONE"))) + << " p0(sfx=" << p0.sfxMode << " pos=" << p0.pos.x << "," << p0.pos.y << "," << p0.pos.z + << " rad=" << p0.rad << " heat=" << p0.heat << " fade=" << p0.fade + << ")\n" << std::flush; + if (!verts.empty()) + DEBUG_STREAM << "[pfx-draw] v0=(" << verts[0].x << "," << verts[0].y << "," << verts[0].z + << ") c=" << std::hex << (unsigned)verts[0].c << std::dec << "\n" << std::flush; + } + } if (!verts.empty()) dev->DrawPrimitiveUP(D3DPT_TRIANGLELIST, (UINT)verts.size() / 3, &verts[0], sizeof(V)); + if (!sfxVerts.empty()) + { + // the fiery-ramped sheet for the heat-model bursts (see above) + if (s_pfxNoise) + dev->SetTexture(0, s_pfxNoise); + dev->DrawPrimitiveUP(D3DPT_TRIANGLELIST, (UINT)sfxVerts.size() / 3, + &sfxVerts[0], sizeof(V)); + } dev->SetTexture(0, NULL); if (s_pfxNoise) @@ -4503,6 +4837,63 @@ void << pfx_loaded << " effect definitions loaded\n" << std::flush; delete psfx_namelist; } + + // + // OLD-STYLE SPECIALFX ("specialfxN=Name" + [Name] descriptor pages) -- the + // per-weapon impact effects (PPCHit/LaserHit/MissileHit/Chunks/Sparks/ + // Fireball...). Field names per the 1995 binary's parser (part_008.c:1546: + // type/size/velocity/v_bias/y_off/variance/gravity/count/repeats + + // "cook" "%f %f %f" + "cool" "%f %f" + texture). NOTE the binary reads + // "variance" but the INI authors "varience" -- the lookup MISSED in the + // shipping pod, so variance is authentically dead (not parsed here). + // The 2007 loader's version-2-only walk below skips all of these + // (version < 2 -> continue), which is why the layer never rendered. + // + { + NameList *sfx_namelist = + master_notation_file->MakeEntryList(starting_page_name, "specialfx"); + if (sfx_namelist != NULL) + { + extern int BTInstallSfx(int slot, int type, int no_tex, float size, + float velocity, float v_bias, float y_off, const float cook[3], + float gravity, float cool_mul, float cool_sub, int count, int repeats); + int sfx_loaded = 0; + for (entry = sfx_namelist->GetFirstEntry(); + entry; + entry = entry->GetNextEntry()) + { + int sfx_number = atoi(entry->GetName() + 9); // "specialfxN" + const char *page = entry->GetChar(); + if (page == NULL || page[0] == '\0') + continue; + int type = 0, count = 1, repeats = 1; + float size = 0, velocity = 0, v_bias = 0, y_off = 0, gravity = 0; + float cook[3] = { 1, 1, 1 }; + float cool_mul = 0.9f, cool_sub = 0.02f; + const char *sp = NULL; + master_notation_file->GetEntry(page, "type", &type); + master_notation_file->GetEntry(page, "size", &size); + master_notation_file->GetEntry(page, "velocity", &velocity); + master_notation_file->GetEntry(page, "v_bias", &v_bias); + master_notation_file->GetEntry(page, "y_off", &y_off); + master_notation_file->GetEntry(page, "gravity", &gravity); + master_notation_file->GetEntry(page, "count", &count); + master_notation_file->GetEntry(page, "repeats", &repeats); + if (master_notation_file->GetEntry(page, "cook", &sp) && sp != NULL) + sscanf(sp, "%f %f %f", &cook[0], &cook[1], &cook[2]); + if (master_notation_file->GetEntry(page, "cool", &sp) && sp != NULL) + sscanf(sp, "%f %f", &cool_mul, &cool_sub); + int no_tex = 0; + if (master_notation_file->GetEntry(page, "texture", &sp) && sp != NULL) + no_tex = (strncmp(sp, "NOTEX", 5) == 0); // NOTEX / NOTEXT + sfx_loaded += BTInstallSfx(sfx_number, type, no_tex, size, velocity, + v_bias, y_off, cook, gravity, cool_mul, cool_sub, count, repeats); + } + DEBUG_STREAM << "[sfx] page '" << starting_page_name << "': " + << sfx_loaded << " old-style specialfx installed\n" << std::flush; + delete sfx_namelist; + } + } //// //// (original stubbed loader retained for provenance) //// @@ -6840,29 +7231,40 @@ void << entity->localOrigin.linearPosition.x << ", " << entity->localOrigin.linearPosition.y << ", " << entity->localOrigin.linearPosition.z << "], " << effect_number << ");" << std::endl << std::flush; - // Both effect-number ENCODINGS resolve to the same psfx slot: - // <100 = the raw dpl board number; >=1000 = the WinTesla-era - // "1000+slot" INDIE id carried by the damage-band resources - // (1002-1005 = ddam1-4 light..critical damage smoke, 1008 = - // ddam5 zone-destroyed -- the SAME [pfx_day] mapping). BT - // ships no INDIE descriptors (the version-2 specialfx pages - // don't exist), so all slots route to the BT .PFX layer, with - // the Explosion entity's ORIENTATION (the victim's frame) -- - // the .PFX offsets/velocities are authored mech-local. + // TWO DISTINCT ENCODINGS (routing corrected 2026-07-12): + // < 100 = the dpl EFFECT-TABLE number = an OLD-STYLE + // specialfx descriptor (BTDPL.INI [effects_to_load] + // "specialfxN": 8=PPCHit, 9=LaserHit, 10=MissileHit, + // 3=Chunks1, 5=Sparks, 15=Fireball -- the 1996 + // script's own comments match) -> the firmware- + // decoded heat-model burst (BTStartSfx); + // >= 1000 = the "1000+slot" psfx id (1002-1005 = ddam1-4, + // 1008 = ddam5, 1009-1013 = idam1-5) -> the .PFX + // layer, oriented in the victim's frame. + // (The old "<100 -> psfx slot" mapping rendered PPCHit as + // ddam5 SMOKE and Chunks as damage smoke -- the "hits are + // just a little smoke" report.) + if (effect_number >= 1000) { - int pfx_slot = (effect_number >= 1000) - ? effect_number - 1000 : effect_number; extern void BTStartPfxFrame(int, float, float, float, const float *, const float *, const float *); float xr[3] = { (float)entity->localToWorld(0,0), (float)entity->localToWorld(0,1), (float)entity->localToWorld(0,2) }; float yr[3] = { (float)entity->localToWorld(1,0), (float)entity->localToWorld(1,1), (float)entity->localToWorld(1,2) }; float zr[3] = { (float)entity->localToWorld(2,0), (float)entity->localToWorld(2,1), (float)entity->localToWorld(2,2) }; - BTStartPfxFrame(pfx_slot, + BTStartPfxFrame(effect_number - 1000, (float)entity->localOrigin.linearPosition.x, (float)entity->localOrigin.linearPosition.y, (float)entity->localOrigin.linearPosition.z, xr, yr, zr); } + else + { + extern void BTStartSfx(int, float, float, float); + BTStartSfx(effect_number, + (float)entity->localOrigin.linearPosition.x, + (float)entity->localOrigin.linearPosition.y, + (float)entity->localOrigin.linearPosition.z); + } } else { @@ -8630,17 +9032,14 @@ void } // - // Board effect numbers (<100) -- the 1995 dpl explosion/damage effects. - // RECONSTRUCTED: routed to the BT .PFX particle layer (the [pfx_day]/ - // [pfx_night] psfxN mapping loads each number's authentic .PFX definition; - // see the layer banner at the top of this file). This is what makes every - // weapon-hit / damage-band / mech-death explosion actually VISIBLE -- the - // original dpl_Effect(dpl_effect_type_explosion, ...) below was IG-board - // hardware and was never ported. + // Board effect numbers (<100) -- the 1995 dpl EFFECT TABLE = the old-style + // specialfx descriptors (BTDPL.INI [effects_to_load]), NOT psfx slots + // (routing corrected 2026-07-12; the psfx encoding is "1000+slot"). The + // firmware-decoded heat-model burst renders them (BTStartSfx). // { - extern void BTStartPfx(int effect_number, float x, float y, float z); - BTStartPfx(effect_number, location.x, location.y, location.z); + extern void BTStartSfx(int effect_number, float x, float y, float z); + BTStartSfx(effect_number, location.x, location.y, location.z); } //dpl_EXPLOSION_EFFECT_INFO my_explosion; diff --git a/game/reconstructed/emitter.hpp b/game/reconstructed/emitter.hpp index 15754e2..be61119 100644 --- a/game/reconstructed/emitter.hpp +++ b/game/reconstructed/emitter.hpp @@ -189,6 +189,26 @@ class NotationFile; // beam from this live sim state -- the authentic per-weapon fire look). // int BeamOn() const { return beamFlag; } // 0x46c + Scalar DischargeTimer() const { return dischargeTimer; } // forensics + // REPLICANT-side beam aging (port, 2026-07-13): replicated discharge + // state arrives via update records, but replicant subsystems never + // Execute -- a lost/undelivered beam-END record left the peer's beam + // ON forever, drawn at its stale fire-time endpoint (the phantom + // "4th gold beam", user-reported). Run the master's deterministic + // countdown locally: no alarms, no upstream records; a live master + // refreshes the state with every real record anyway. + void ReplicantServiceBeam(Scalar dt) + { + if (beamFlag == 0) + return; + dischargeTimer -= dt; + if (dischargeTimer <= 0.0f) + { + beamFlag = 0; + firingActive = 0; + dischargeTimer = dischargeTime; + } + } const Point3D &BeamEndpoint() const { return beamEndpoint; } // 0x460 public: diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index 9b0f1a5..28143e3 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -1390,6 +1390,8 @@ Mech::Mech( duckedTemplateMaxY = 0.0f; templateBottomLift = 0.0f; frameEntryWorldVelocity = Vector3D(0.0f, 0.0f, 0.0f); // collision-damage guard snapshot + ramLastVictim = 0; // ram contact-edge state + ramContactLinger = 0.0f; if (GroundReal() && GetCollisionVolumeCount() > 0 && collisionTemplate != 0 && collisionVolume != 0) { diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index 17f92fc..e25b895 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -639,6 +639,16 @@ public: // approach speed -- which is all the binary's StaticBounce ever saw. // By-name access only; declared after the layout-locked fields. Vector3D frameEntryWorldVelocity; + // RAM CONTACT EDGE (port analog of the binary's bounce-separation): in the + // 1995 mover, StaticBounce REVERSED worldLinearVelocity, so the frame after + // a bump read as separating and the damage gate stopped repeat dispatch. + // Our gait re-derives velocity every frame (still closing while the stick + // is held / while respawn-overlapped), so sustained contact re-priced a + // fresh full-speed ram EVERY frame (~60 dmg x 60fps = melt; the respawn + // explode-loop). One bump = one damage event; re-arms after the contact + // breaks for ramContactLinger seconds. + Entity *ramLastVictim; + Scalar ramContactLinger; // AUTHENTIC TARGETING (task #36): the engine Reticle struct (MUNGA/RETICLE.h // [T0]) -- the mech's "TargetReticle" attribute (id 0x1d) binds to it, per the diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 0969ab5..5002843 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -153,6 +153,7 @@ #include // CulturalIcon::IsStoppingCollisionVolume / GetClassDerivations #include // HostManager::GetEntityPointer (band-effect attacker resolve) #include // SubsystemMessageManager (task #7 consolidated delivery) +#include // MechWeapon::GetExplosionResourceID (per-round detonation) #if !defined(PLAYER_HPP) # include // Player::VehicleDeadMessage -- the death->respawn notification (task #52) #endif @@ -774,18 +775,60 @@ struct BTProjectile { Scalar aimOffsetY; // vertical aim offset vs the target's origin (live re-lead) Scalar damage; // <= 0 -> VISUAL-ONLY round (replicant-side salvo mirror) int guided; // 1 = missile (seeker loft + steering); 0 = ballistic (straight) + Entity *shooter; // the firing mech (messmgr explosion bundling at impact) + int weaponSubsys; // firing weapon's roster index (-1 = unthreaded) -- + // resolves the weapon's ExplosionModelFile (mslhit/ + // acanhit) in the messmgr, task #7 bundling int active; }; static BTProjectile gProjectiles[64]; extern void BTPushBeam(float,float,float, float,float,float, unsigned, float, float); +//########################################################################### +// PER-ROUND DETONATION (the binary's Missile::MoveAndCollide @004bef78: every +// round spawns ITS OWN ExplosionModelFile at its impact point, resource +// missile+0x33c -- a volley RIPPLES fireballs across its arrival frames, the +// demo look). Resolved from the firing weapon's roster entry (weapon+0x3E4); +// runs on BOTH nodes (the replicant visual salvo passes its launcher's index +// too). The messmgr's one bundled explosion remains (direct-fire semantics + +// the cross-pod damage stream); among a rippled volley it is invisible. +//########################################################################### +static void + BTSpawnRoundDetonation(Entity *shooter, int weapon_subsys, const Point3D &at) +{ + extern int BTIsRegisteredMech(Entity *e); + if (shooter == 0 || weapon_subsys < 0 || !BTIsRegisteredMech(shooter)) + return; + Mech *sm = (Mech *)shooter; + if (weapon_subsys >= sm->GetSubsystemCount()) + return; + Subsystem *w = sm->GetSubsystem(weapon_subsys); + if (w == 0 || !w->IsDerivedFrom(*MechWeapon::GetClassDerivations())) + return; + ResourceDescription::ResourceID res = ((MechWeapon *)w)->GetExplosionResourceID(); + if (res <= 0) + return; + Origin o; + o.linearPosition = at; + o.angularPosition = EulerAngles(Radian(0.0f), Radian(0.0f), Radian(0.0f)); + Explosion::MakeMessage m( + Explosion::MakeMessageID, sizeof(Explosion::MakeMessage), + (Entity::ClassID)RegisteredClass::ExplosionClassID, EntityID::Null, + res, Explosion::DefaultFlags, o, + sm->GetEntityID(), sm->GetEntityID()); + Explosion *e = Explosion::Make(&m); + if (e) + Register_Object(e); +} + // Called from ProjectileWeapon / MissileLauncher::FireWeapon (via the extern below) // with the live muzzle, the SHOOTER mech (to resolve the real launch port), the owner's // locked target entity + point, the launch speed (|muzzleVelocity|), and per-shot damage. void BTPushProjectile(const Point3D &muzzle, void *shooter, void *target, const Point3D &targetPos, - Scalar speed, Scalar damage, const Vector3D *launch_velocity, int guided) + Scalar speed, Scalar damage, const Vector3D *launch_velocity, int guided, + int weapon_subsys) { // MUZZLE (muzzle wave, 2026-07-12): the passed muzzle is now the weapon's // AUTHENTIC mount segment (GetMuzzlePoint reads the real segmentIndex -- @@ -880,6 +923,34 @@ void ? (tpos.y - ((Mech *)target)->localOrigin.linearPosition.y) : 0.0f; p.damage = damage; p.guided = guided; // autocannon shells fly straight (no seeker in the binary's plain Projectile) + p.shooter = (Entity *)shooter; + p.weaponSubsys = weapon_subsys; + + // RACK-TUBE SPREAD [T3, physically grounded]: the binary's Missile + // entities each launch from their own rack tube (per-tube authored + // MuzzlePosition); our single muzzle superimposed a whole salvo onto + // ONE trajectory -- 12 rounds read as one round, and their 12 + // detonations stacked into one frame. Approximate the tube offsets + // with a small deterministic per-slot cone (+-~2.5 deg) + lateral + // muzzle offset; the seeker re-converges them onto the target so the + // volley arrives as the demo's RIPPLE of impacts. + // GUIDED ROUNDS ONLY: a ballistic shell has no seeker to re-converge + // it -- the slot-0 pattern deflected every AFC100 shell a fixed + // 3.4deg left / 2.5deg down (the phantom "4th gold beam" burying its + // tracer beside the target, user-reported). The binary's plain + // Projectile flies straight at the pick. + if (guided) + { + float ja = ((i % 5) - 2) * 0.030f; // yaw +-0.06 rad + float jb = (((i / 5) % 5) - 2) * 0.022f; // pitch +-0.044 rad + float vx = p.vel.x, vy = p.vel.y, vz = p.vel.z; + float ca = cosf(ja), sa = sinf(ja); + p.vel.x = vx * ca - vz * sa; + p.vel.z = vx * sa + vz * ca; + p.vel.y = vy + jb * p.speed; + p.pos.x += ((i % 3) - 1) * 0.6f; + p.pos.z += (((i / 3) % 3) - 1) * 0.6f; + } p.active = 1; if (getenv("BT_PROJ_LOG")) DEBUG_STREAM << "[projectile] vel=(" << p.vel.x << "," << p.vel.y @@ -888,7 +959,7 @@ void ? " authored=(" : " none=(") << (launch_velocity ? launch_velocity->x : 0.0f) << "," << (launch_velocity ? launch_velocity->y : 0.0f) << "," - << (launch_velocity ? launch_velocity->z : 0.0f) << ")\n" << std::flush; + << (launch_velocity ? launch_velocity->z : 0.0f) << ")" << std::endl; return; } } @@ -973,13 +1044,19 @@ static void if (BTGroundRayHit(prev.x, prev.y, prev.z, rd.x, rd.y, rd.z, step + 1.0f, &hx, &hy, &hz)) { - // burst on the rock: a tight puff cluster at the hit, no damage + // burst on the rock: the round's own DETONATION (the binary + // missile detonates on ANY geometry, @004bef78) + a tight + // puff cluster, no damage + { + Point3D hp; hp.x = hx; hp.y = hy; hp.z = hz; + BTSpawnRoundDetonation(p.shooter, p.weaponSubsys, hp); + } extern void BTPfxTrailPuff(int, float, float, float, float, float, float, int); for (int pf = 0; pf < 4; ++pf) BTPfxTrailPuff(0, hx, hy, hz, -rd.x, -rd.y, -rd.z, 2); if (getenv("BT_PROJ_LOG")) DEBUG_STREAM << "[projectile] WORLD burst at(" << hx << "," - << hy << "," << hz << ")\n" << std::flush; + << hy << "," << hz << ")" << std::endl; p.active = 0; continue; } @@ -1014,6 +1091,7 @@ static void } if (contact) { + BTSpawnRoundDetonation(p.shooter, p.weaponSubsys, p.pos); Entity *tgt = p.target; // Deliver to the projectile's target mech -- the launcher set p.target // from the shooter's 0x388 slot (the picked victim; any peer mech in @@ -1033,15 +1111,38 @@ static void dmg.damageAmount = p.damage; dmg.burstCount = 1; dmg.impactPoint = p.pos; - Entity::TakeDamageMessage take_damage( - Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage), - 0 /*inflictor id: bring-up*/, -1 /*unaimed -> cylinder resolves*/, dmg); - tgt->Dispatch(&take_damage); + // Route through the SHOOTER's SubsystemMessageManager with the + // firing launcher's roster index (task #7 bundling): the + // consolidation resolves roster[id]+0x3E4 = the weapon's + // ExplosionModelFile (mslhit/acanhit) and fires it AT the + // impact point -- the missing missile-hit explosion (the + // laser path always did this via SendDamageMessage; the + // projectile path bypassed the manager entirely). + SubsystemMessageManager *mgr = 0; + if (p.shooter != 0 && p.weaponSubsys >= 0 && BTIsRegisteredMech(p.shooter)) + mgr = (SubsystemMessageManager *)((Mech *)p.shooter)->GetMessageManager(); + if (mgr != 0) + { + Entity::TakeDamageMessage take_damage( + Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage), + p.shooter->GetEntityID(), -1 /*unaimed -> cylinder resolves*/, + dmg, p.weaponSubsys); + mgr->AddDamageMessage(tgt, &take_damage); + } + else + { + Entity::TakeDamageMessage take_damage( + Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage), + 0 /*inflictor id: bring-up*/, -1 /*unaimed -> cylinder resolves*/, dmg); + tgt->Dispatch(&take_damage); + } // gauge scoring wave (Step 6): a projectile hit credits SCORE too // (tgt == gEnemyMech here; local player is the viewpoint shooter). BTPostDamageScore((Entity *)tgt, p.damage); DEBUG_STREAM << "[projectile] IMPACT damage=" << p.damage - << " zone=" << take_damage.damageZone << " (cyl-resolved)\n" << std::flush; + << " subsys=" << p.weaponSubsys + << (mgr ? " (msgmgr bundled)" : " (direct)") + << " (zone cyl-resolved)\n" << std::flush; } } p.active = 0; @@ -1196,6 +1297,8 @@ void worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f); localVelocity = Motion::Identity; frameEntryWorldVelocity = Vector3D(0.0f, 0.0f, 0.0f); + ramLastVictim = 0; + ramContactLinger = 0.0f; // --- our RELOCATED gait/motion accumulators -> identity (the 1995 offsets // the decomp zeroes map to these named members in our layout) --- @@ -1363,7 +1466,7 @@ void if (getenv("BT_DEATH_LOG")) DEBUG_STREAM << "[death] mech " << GetEntityID() << " destroyed -> subsystem shutdown + frozen wreck (IsDisabled=" - << (int)IsDisabled() << ")\n" << std::flush; + << (int)IsDisabled() << ")" << std::endl; // --- DEATH EFFECTS (task #42): dispatched HERE, at the VICTIM's own // once-per-death transition, so they fire regardless of WHAT killed it @@ -1516,7 +1619,7 @@ void DEBUG_STREAM << "[mech-exec] entity " << GetEntityID() << " instance=" << (int)GetInstance() << " pos=(" << localOrigin.linearPosition.x << ", " - << localOrigin.linearPosition.z << ")\n" << std::flush; + << localOrigin.linearPosition.z << ")" << std::endl; } } @@ -2023,7 +2126,7 @@ void << " err=" << err << " turn=" << gBTGotoTurn << " thr=" << gBTGotoThrottle << " arr=" << arrived << " fwd=(" << fwdX << "," << fwdZ << ")" - << " tgt=(" << s_gx << "," << s_gz << ")\n" << std::flush; } + << " tgt=(" << s_gx << "," << s_gz << ")" << std::endl; } } // SELF-DRIVE (task #48): a "beeline" harness must supply its OWN // forward throttle, not only steering -- otherwise it just turns @@ -2357,7 +2460,7 @@ void gCurrentGait = wantGait; DEBUG_STREAM << "[gait] SequenceController -> blh" << suffix << " id=" << (long)*clip << " (" << (wantGait ? "run" : "walk") - << ")\n" << std::flush; + << ")" << std::endl; } } adv = bodyAnimation.Advance(dt, 1); // inline clip-select path @@ -2427,7 +2530,7 @@ void DEBUG_STREAM << "[gait] adv=" << travelAdv << " (proj=" << adv << ") pos=(" << localOrigin.linearPosition.x << ", " - << localOrigin.linearPosition.z << ")\n" << std::flush; + << localOrigin.linearPosition.z << ")" << std::endl; } } else @@ -2446,7 +2549,7 @@ void gCurrentGait = wantGait; gBodyAnimReady = 1; DEBUG_STREAM << "[anim] gait -> blh" << suffix << " id=" << (long)*clip - << " (" << (wantGait ? "run" : "walk") << ")\n" << std::flush; + << " (" << (wantGait ? "run" : "walk") << ")" << std::endl; } else { @@ -2481,7 +2584,7 @@ void << (dir < 0 ? " (reverse)" : "") << " adv=" << adv << " loops=" << gBodyAnimLoops << " pos=(" << localOrigin.linearPosition.x << ", " - << localOrigin.linearPosition.z << ")\n" << std::flush; + << localOrigin.linearPosition.z << ")" << std::endl; } } } // close the BT_GAIT_CUTOVER else (STEP 1-2 path) @@ -2591,7 +2694,7 @@ void DEBUG_STREAM << "[collide] pushed out by (" << dx << ", " << (localOrigin.linearPosition.y - before.y) << ", " << dz << ") pos=(" << localOrigin.linearPosition.x << ", " - << localOrigin.linearPosition.z << ")\n" << std::flush; + << localOrigin.linearPosition.z << ")" << std::endl; } // Floor clamp: never sink below the base ground (safety on solid-less maps). if (localOrigin.linearPosition.y < s_groundY) @@ -2716,7 +2819,7 @@ void DEBUG_STREAM << "[visgnd] lift=" << vLift << " at (" << localOrigin.linearPosition.x << ", " << localOrigin.linearPosition.y << ", " - << localOrigin.linearPosition.z << ")\n" << std::flush; + << localOrigin.linearPosition.z << ")" << std::endl; } } } @@ -3804,6 +3907,14 @@ void // and a multi-solid frame compounds those reflections into a garbage // velocity that priced mech-vs-mech damage 4x-40x too high (mp_a.log:32651). frameEntryWorldVelocity = savedWorldVel; + // ram contact-edge decay: once contact with the last victim has been + // broken for the linger window, re-arm the one-bump-one-hit gate + if (ramContactLinger > 0.0f) + { + ramContactLinger -= dt; + if (ramContactLinger <= 0.0f) + ramLastVictim = 0; + } Damage dmg; // ctor zeroes damageAmount if (cols != 0) ProcessCollisionList(cols, dt, old_position, &dmg); @@ -3831,7 +3942,7 @@ void } if (GroundLog()) DEBUG_STREAM << "[ground] CRUNCH (crushable icon) at (" - << savedPos.x << ", " << savedPos.z << ")\n" << std::flush; + << savedPos.x << ", " << savedPos.z << ")" << std::endl; } else if (dmg.damageAmount > 0.0f) // blocking hit: FULL FRAME REJECTION { @@ -3893,12 +4004,12 @@ void } if (GroundLog()) DEBUG_STREAM << "[ground] BLOCK + CRASH (iv2=" << iv2 - << " clip=" << animationClips[0x20] << ")\n" << std::flush; + << " clip=" << animationClips[0x20] << ")" << std::endl; } else if (GroundLog()) DEBUG_STREAM << "[ground] BLOCK dmg=" << dmg.damageAmount << " at (" << old_position.x << ", " << old_position.z - << ")\n" << std::flush; + << ")" << std::endl; } // 1-Hz telemetry (verification: h / y / hit-miss); INSIDE-SOLID detector: @@ -3915,7 +4026,7 @@ void DEBUG_STREAM << "[ground] INSIDE-SOLID streak at (" << localOrigin.linearPosition.x << ", " << localOrigin.linearPosition.y << ", " - << localOrigin.linearPosition.z << ")\n" << std::flush; + << localOrigin.linearPosition.z << ")" << std::endl; } else s_zeroStreak = 0; @@ -3928,7 +4039,7 @@ void << " lift=" << collisionTemplate->minY << " pos=(" << localOrigin.linearPosition.x << ", " << localOrigin.linearPosition.y << ", " - << localOrigin.linearPosition.z << ")\n" << std::flush; + << localOrigin.linearPosition.z << ")" << std::endl; } } } @@ -3989,7 +4100,18 @@ static void { Damage dmg; dmg.damageType = Damage::CollisionDamageType; - dmg.damageAmount = resolved->damageAmount; + // RAM ECONOMY NORMALIZATION [T3, evidence-forced 2026-07-12]: StaticBounce + // prices the kinetic loss with the AUTHORED moverMass (~1.3e6 units) -- + // ~59,000 "points" for a 10 m/s ram, against zones whose armor absorbs + // 1 point per point (scale = 1/armorPoints, armor 50-140: the [zone-armor] + // dump). The BINARY dispatches this raw number [T1 @part_012:15324] -- + // but on the pod network it only ever hit the local REPLICANT of the + // victim, where it evaporated (the master never heard it; MECH.CPP:986 + // merely warns). Our MP forwards replicant damage (task #47, required + // for weapons), which surfaced the dormant 59K as a one-shot ram-kill. + // Normalize by 1e-3 (the mass-unit/armor-point scale): a walking bump = + // a few points, a full charge = tens -- the armor-table economy. + dmg.damageAmount = resolved->damageAmount * 0.001f; dmg.surfaceNormal = resolved->surfaceNormal; dmg.impactPoint = Point3D( (collision.collisionSlice.minX + collision.collisionSlice.maxX) * 0.5f, @@ -4016,9 +4138,11 @@ static void DEBUG_STREAM << "[collide-tx] " << inflictor->GetEntityID() << " rams " << victim->GetEntityID() << " dmg=" << resolved->damageAmount + << " mass=" << inflictor->moverMass + << " e=" << inflictor->elasticityCoefficient << " |v|=" << (Scalar)sqrtf(v.x*v.x + v.y*v.y + v.z*v.z) << " at(" << dmg.impactPoint.x << "," << dmg.impactPoint.y - << "," << dmg.impactPoint.z << ")\n" << std::flush; + << "," << dmg.impactPoint.z << ")" << std::endl; } } @@ -4100,7 +4224,21 @@ void // is UNBLOCKED (was deferred: zone==-1 used to be dropped). if (owner->IsDerivedFrom(*Mech::GetClassDerivations())) { - BTDispatchCollisionDamage(this, owner, damage, collision); + // RAM CONTACT EDGE (see mech.hpp): dispatch damage only on a FRESH + // contact with this victim; while pressed, refresh the linger so + // sustained contact (held stick / respawn overlap) stays a BLOCK, + // not a 60Hz damage grinder -- the binary's bounce-separation made + // this edge implicit; our gait-derived velocity needs it explicit. + if (owner == ramLastVictim && ramContactLinger > 0.0f) + { + ramContactLinger = 0.35f; // still pressed: refresh, no damage + } + else + { + ramLastVictim = owner; + ramContactLinger = 0.35f; + BTDispatchCollisionDamage(this, owner, damage, collision); + } } } @@ -4234,6 +4372,11 @@ void continue; ++energyOrdinal; Emitter *em = (Emitter *)ws; + // replicant beams: age the replicated discharge locally (a lost + // beam-END record otherwise pins the beam on forever -- see + // Emitter::ReplicantServiceBeam) + if (GetInstance() == Entity::ReplicantInstance) + em->ReplicantServiceBeam(ttl1); if (!em->BeamOn()) continue; Point3D mz; @@ -4293,6 +4436,21 @@ void (((unsigned)(40.0f + r * 215.0f) & 0xFF) << 16) | (((unsigned)(40.0f + g * 215.0f) & 0xFF) << 8) | ((unsigned)(40.0f + b * 215.0f) & 0xFF); + // PHANTOM-BEAM FORENSICS (BT_FIRE_LOG): identify every drawn beam -- + // owner/instance, roster slot, class, discharge state, colour, ends. + if (getenv("BT_FIRE_LOG")) + { + static int s_bd = 0; + if ((++s_bd % 30) == 1) // ~2Hz per sustained beam + DEBUG_STREAM << "[beam-draw] mech=" << GetEntityID() + << " inst=" << (int)GetInstance() + << " slot=" << wi << " cid=" << wcid + << " timer=" << em->DischargeTimer() + << " pip=(" << r << "," << g << "," << b << ")" + << " mz=(" << mz.x << "," << mz.y << "," << mz.z << ")" + << " end=(" << bend.x << "," << bend.y << "," << bend.z + << ")" << std::endl; + } BTPushBeamKind(mz.x, mz.y, mz.z, bend.x, bend.y, bend.z, tint, ttl1, 1.0f /* natural model width */, isPPC ? 1 : 0 /* ppc.bgf : ermlaser.bgf */); @@ -4300,6 +4458,6 @@ void DEBUG_STREAM << "[beam] " << (isPPC ? "PPC" : "laser") << " #" << wi << " mz=(" << mz.x << "," << mz.y << "," << mz.z << ") end=(" << bend.x << "," << bend.y << "," << bend.z - << ") rgb=(" << r << "," << g << "," << b << ")\n" << std::flush; + << ") rgb=(" << r << "," << g << "," << b << ")" << std::endl; } } diff --git a/game/reconstructed/mechdmg.cpp b/game/reconstructed/mechdmg.cpp index ddeb120..1a365a1 100644 --- a/game/reconstructed/mechdmg.cpp +++ b/game/reconstructed/mechdmg.cpp @@ -240,6 +240,9 @@ Mech__DamageZone::Mech__DamageZone( // members (read raw from the stream by the base ctor, DAMAGE.cpp:301-307); we // normalise them in place here -- not a raw 0x144/0x140 offset (which would be the // binary layout, wrong when compiled against the engine base). + Scalar rawScale[Damage::DamageTypeCount]; + for (int type = 0; type < Damage::DamageTypeCount; ++type) + rawScale[type] = damageScale[type]; for (int type = 0; type < Damage::DamageTypeCount; ++type) // 5 damage types { Scalar even = 1.0f / defaultArmorPoints; // _DAT_0049d1c4 / count @@ -248,6 +251,19 @@ Mech__DamageZone::Mech__DamageZone( if (rightLeg || leftLeg) damageScale[type] *= 0.5f; // _DAT_0049d1cc } + // ARMOR ECONOMY DUMP (the ram-kill forensics): the authored per-type + // absorption -- damageLevel += amount * scale[type]; level 1.0 = zone + // destroyed. scale[0] (Collision) x the StaticBounce kinetic number is + // the authored ram economy. + if (getenv("BT_DMG_LOG")) + DEBUG_STREAM << "[zone-armor] zone=" << damageZoneIndex + << " armorPts=" << defaultArmorPoints + << " raw={" << rawScale[0] << "," << rawScale[1] << "," << rawScale[2] + << "," << rawScale[3] << "," << rawScale[4] << "}" + << " scale={" << damageScale[0] << "," << damageScale[1] << "," + << damageScale[2] << "," << damageScale[3] << "," << damageScale[4] << "}" + << " leg=" << (int)(rightLeg || leftLeg) + << " vital=" << (int)vitalDamageZone << std::endl; // // Critical-subsystem table. @@ -1055,6 +1071,16 @@ void continue; lastLevel[i] = level; + // AUTHENTIC GATE (decomp re-verified 2026-07-12, workflow): the binary + // fires the CURRENT band descriptor's effect whenever the zone's + // damage-CHANGED flag is set (+0xb8 & 4, set by DamageZone::TakeDamage, + // coalesced per tick) -- i.e. EVERY hit on a damaged mech re-puffs its + // band effect. Bands 3/4 are authored ember/bright-FIRE plumes, so a + // nearly-destroyed mech under fire burns continuously (the demo look). + // A level RISE this tick IS the flag analog -- no crossing test. (The + // earlier metronome was SHKWAVE mispacing, not this path; an observer + // node sees a few replication-step puffs per volley -- smoke, fine.) + const Mech__DamageZone::DamageDescriptor *d = zone->DescriptorForLevel(level); if (level >= 1.0f && prev < 1.0f) // just destroyed -> the Destroyed descriptor { diff --git a/game/reconstructed/mislanch.cpp b/game/reconstructed/mislanch.cpp index 1f0731a..2c4ce08 100644 --- a/game/reconstructed/mislanch.cpp +++ b/game/reconstructed/mislanch.cpp @@ -72,7 +72,8 @@ // shooter's frame (up-tilted rack launch); damage <= 0 = a VISUAL-ONLY round. extern void BTPushProjectile(const Point3D &muzzle, void *shooter, void *target, const Point3D &targetPos, Scalar speed, Scalar damage, - const Vector3D *launch_velocity = 0, int guided = 1); + const Vector3D *launch_velocity = 0, int guided = 1, + int weapon_subsys = -1); //########################################################################### // Port-side salvo replication state (missile-visibility wave). @@ -305,7 +306,8 @@ void MissileLauncher::FireWeapon() int nmiss = (missileCount > 0 && missileCount < 40) ? missileCount : 1; for (int i = 0; i < nmiss; ++i) BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount, - &launchVelocity); + &launchVelocity, 1, + subsystemID /*messmgr explosion bundling at impact (task #7)*/); // Salvo replication (missile-visibility wave): bump the fire counter + stamp // the aim point; the extended update record carries both to peer nodes. @@ -376,7 +378,8 @@ void n = (missileCount > 0 && missileCount < 40) ? missileCount : 1; for (int i = 0; i < n; ++i) BTPushProjectile(mz, owner, 0 /*no entity: aim point only*/, - rec->salvoTarget, spd, 0.0f /*VISUAL*/, &launchVelocity); + rec->salvoTarget, spd, 0.0f /*VISUAL*/, &launchVelocity, 1, + subsystemID /*per-round detonation resolve on this node*/); if (getenv("BT_PROJ_LOG")) DEBUG_STREAM << "[projectile] REPLICANT salvo x" << n << " at(" << rec->salvoTarget.x << "," << rec->salvoTarget.y diff --git a/game/reconstructed/projweap.cpp b/game/reconstructed/projweap.cpp index dcfdb3e..f50fdae 100644 --- a/game/reconstructed/projweap.cpp +++ b/game/reconstructed/projweap.cpp @@ -94,7 +94,8 @@ // crash, 2026-07-12 -- gotcha #3). extern void BTPushProjectile(const Point3D &muzzle, void *shooter, void *target, const Point3D &targetPos, Scalar speed, Scalar damage, - const Vector3D *launch_velocity = 0, int guided = 1); + const Vector3D *launch_velocity = 0, int guided = 1, + int weapon_subsys = -1); //############################################################################# @@ -697,7 +698,8 @@ void + launchVelocity.y*launchVelocity.y + launchVelocity.z*launchVelocity.z); BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount, - 0 /*aim straight at the pick*/, 0 /*BALLISTIC: no seeker on a shell*/); + 0 /*aim straight at the pick*/, 0 /*BALLISTIC: no seeker on a shell*/, + subsystemID /*messmgr explosion bundling at impact (task #7)*/); simulationFlags |= 0x1; // replication-dirty Check_Fpu(); diff --git a/scratchpad/fxshot.py b/scratchpad/fxshot.py new file mode 100644 index 0000000..5141335 --- /dev/null +++ b/scratchpad/fxshot.py @@ -0,0 +1,56 @@ +"""Capture timed screenshots of a btl4 window via PrintWindow (works occluded). + +Usage: python fxshot.py [count] [interval_s] [outprefix] [nodesubstr] +""" +import ctypes, ctypes.wintypes as wt, sys, time + +user32 = ctypes.windll.user32 +gdi32 = ctypes.windll.gdi32 + +count = int(sys.argv[1]) if len(sys.argv) > 1 else 8 +interval = float(sys.argv[2]) if len(sys.argv) > 2 else 0.4 +prefix = sys.argv[3] if len(sys.argv) > 3 else 'fx' +want = sys.argv[4] if len(sys.argv) > 4 else '1501' + +titles = [] +def enum_cb(hwnd, lparam): + n = user32.GetWindowTextLengthW(hwnd) + if n > 0: + buf = ctypes.create_unicode_buffer(n + 1) + user32.GetWindowTextW(hwnd, buf, n + 1) + if buf.value.startswith('BattleTech') and want in buf.value: + titles.append((hwnd, buf.value)) + return True +WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, wt.HWND, wt.LPARAM) +user32.EnumWindows(WNDENUMPROC(enum_cb), 0) +if not titles: + print('no matching BattleTech window'); sys.exit(1) +hwnd, title = titles[0] +print('capturing:', title) + +rect = wt.RECT() +user32.GetClientRect(hwnd, ctypes.byref(rect)) +w, h = rect.right, rect.bottom + +hdc = user32.GetDC(0) +mem = gdi32.CreateCompatibleDC(hdc) +bmp = gdi32.CreateCompatibleBitmap(hdc, w, h) +gdi32.SelectObject(mem, bmp) + +class BMIH(ctypes.Structure): + _fields_ = [('biSize', wt.DWORD), ('biWidth', wt.LONG), ('biHeight', wt.LONG), + ('biPlanes', wt.WORD), ('biBitCount', wt.WORD), ('biCompression', wt.DWORD), + ('biSizeImage', wt.DWORD), ('biXPelsPerMeter', wt.LONG), ('biYPelsPerMeter', wt.LONG), + ('biClrUsed', wt.DWORD), ('biClrImportant', wt.DWORD)] + +from PIL import Image +for i in range(count): + # PW_RENDERFULLCONTENT (2) captures D3D content + user32.PrintWindow(hwnd, mem, 2 | 1) # +PW_CLIENTONLY + bmi = BMIH(ctypes.sizeof(BMIH), w, -h, 1, 32, 0, 0, 0, 0, 0, 0) + buf = ctypes.create_string_buffer(w * h * 4) + gdi32.GetDIBits(mem, bmp, 0, h, buf, ctypes.byref(bmi), 0) + img = Image.frombuffer('RGB', (w, h), buf.raw, 'raw', 'BGRX', 0, 1) + img.save(r'C:\git\bt411\scratchpad\%s_%d.png' % (prefix, i)) + time.sleep(interval) +print('saved %d shots %dx%d' % (count, w, h)) diff --git a/scratchpad/i860_opcodes.h b/scratchpad/i860_opcodes.h new file mode 100644 index 0000000..7836bff --- /dev/null +++ b/scratchpad/i860_opcodes.h @@ -0,0 +1,507 @@ +/* Table of opcodes for the i860. + Copyright 1989, 1991, 2000, 2002, 2003 Free Software Foundation, Inc. + +This file is part of GAS, the GNU Assembler, and GDB, the GNU disassembler. + +GAS/GDB is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 1, or (at your option) +any later version. + +GAS/GDB is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GAS or GDB; see the file COPYING. If not, write to +the Free Software Foundation, 51 Franklin Street - Fifth Floor, +Boston, MA 02110-1301, USA. */ + + +/* Structure of an opcode table entry. */ +struct i860_opcode +{ + /* The opcode name. */ + const char *name; + + /* Bits that must be set. */ + unsigned long match; + + /* Bits that must not be set. */ + unsigned long lose; + + const char *args; + + /* Nonzero if this is a possible expand-instruction. */ + char expand; +}; + + +enum expand_type +{ + E_MOV = 1, E_ADDR, E_U32, E_AND, E_S32, E_DELAY, XP_ONLY +}; + + +/* All i860 opcodes are 32 bits, except for the pseudo-instructions + and the operations utilizing a 32-bit address expression, an + unsigned 32-bit constant, or a signed 32-bit constant. + These opcodes are expanded into a two-instruction sequence for + any situation where the immediate operand does not fit in 32 bits. + In the case of the add and subtract operations the expansion is + to a three-instruction sequence (ex: orh, or, adds). In cases + where the address is to be relocated, the instruction is + expanded to handle the worse case, this could be optimized at + the final link if the actual address were known. + + The pseudoinstructions are: mov, fmov, pmov, nop, and fnop. + These instructions are implemented as a one or two instruction + sequence of other operations. + + The match component is a mask saying which bits must match a + particular opcode in order for an instruction to be an instance + of that opcode. + + The args component is a string containing one character + for each operand of the instruction. + +Kinds of operands: + # Number used by optimizer. It is ignored. + 1 src1 integer register. + 2 src2 integer register. + d dest register. + c ctrlreg control register. + i 16 bit immediate. + I 16 bit immediate, aligned 2^0. (ld.b) + J 16 bit immediate, aligned 2^1. (ld.s) + K 16 bit immediate, aligned 2^2. (ld.l, {p}fld.l, fst.l) + L 16 bit immediate, aligned 2^3. ({p}fld.d, fst.d) + M 16 bit immediate, aligned 2^4. ({p}fld.q, fst.q) + 5 5 bit immediate. + l lbroff 26 bit PC relative immediate. + r sbroff 16 bit PC relative immediate. + s split 16 bit immediate. + S split 16 bit immediate, aligned 2^0. (st.b) + T split 16 bit immediate, aligned 2^1. (st.s) + U split 16 bit immediate, aligned 2^2. (st.l) + e src1 floating point register. + f src2 floating point register. + g dest floating point register. */ + + +/* The order of the opcodes in this table is significant. The assembler + requires that all instances of the same mnemonic must be consecutive. + If they aren't, the assembler will not function properly. + + The order of opcodes does not affect the disassembler. */ + +static const struct i860_opcode i860_opcodes[] = +{ +/* REG-Format Instructions. */ +{ "ld.c", 0x30000000, 0xcc000000, "c,d", 0 }, /* ld.c csrc2,idest */ +{ "ld.b", 0x00000000, 0xfc000000, "1(2),d", 0 }, /* ld.b isrc1(isrc2),idest */ +{ "ld.b", 0x04000000, 0xf8000000, "I(2),d", E_ADDR }, /* ld.b #const(isrc2),idest */ +{ "ld.s", 0x10000000, 0xec000001, "1(2),d", 0 }, /* ld.s isrc1(isrc2),idest */ +{ "ld.s", 0x14000000, 0xe8000001, "J(2),d", E_ADDR }, /* ld.s #const(isrc2),idest */ +{ "ld.l", 0x10000001, 0xec000000, "1(2),d", 0 }, /* ld.l isrc1(isrc2),idest */ +{ "ld.l", 0x14000001, 0xe8000000, "K(2),d", E_ADDR }, /* ld.l #const(isrc2),idest */ + +{ "st.c", 0x38000000, 0xc4000000, "1,c", 0 }, /* st.c isrc1ni,csrc2 */ +{ "st.b", 0x0c000000, 0xf0000000, "1,S(2)", E_ADDR }, /* st.b isrc1ni,#const(isrc2) */ +{ "st.s", 0x1c000000, 0xe0000001, "1,T(2)", E_ADDR }, /* st.s isrc1ni,#const(isrc2) */ +{ "st.l", 0x1c000001, 0xe0000000, "1,U(2)", E_ADDR }, /* st.l isrc1ni,#const(isrc2) */ + +{ "ixfr", 0x08000000, 0xf4000000, "1,g", 0 }, /* ixfr isrc1ni,fdest */ + +{ "fld.l", 0x20000002, 0xdc000001, "1(2),g", 0 }, /* fld.l isrc1(isrc2),fdest */ +{ "fld.l", 0x24000002, 0xd8000001, "K(2),g", E_ADDR }, /* fld.l #const(isrc2),fdest */ +{ "fld.l", 0x20000003, 0xdc000000, "1(2)++,g", 0 }, /* fld.l isrc1(isrc2)++,fdest */ +{ "fld.l", 0x24000003, 0xd8000000, "K(2)++,g", E_ADDR }, /* fld.l #const(isrc2)++,fdest */ +{ "fld.d", 0x20000000, 0xdc000007, "1(2),g", 0 }, /* fld.d isrc1(isrc2),fdest */ +{ "fld.d", 0x24000000, 0xd8000007, "L(2),g", E_ADDR }, /* fld.d #const(isrc2),fdest */ +{ "fld.d", 0x20000001, 0xdc000006, "1(2)++,g", 0 }, /* fld.d isrc1(isrc2)++,fdest */ +{ "fld.d", 0x24000001, 0xd8000006, "L(2)++,g", E_ADDR }, /* fld.d #const(isrc2)++,fdest */ +{ "fld.q", 0x20000004, 0xdc000003, "1(2),g", 0 }, /* fld.q isrc1(isrc2),fdest */ +{ "fld.q", 0x24000004, 0xd8000003, "M(2),g", E_ADDR }, /* fld.q #const(isrc2),fdest */ +{ "fld.q", 0x20000005, 0xdc000002, "1(2)++,g", 0 }, /* fld.q isrc1(isrc2)++,fdest */ +{ "fld.q", 0x24000005, 0xd8000002, "M(2)++,g", E_ADDR }, /* fld.q #const(isrc2)++,fdest */ + +{ "pfld.l", 0x60000002, 0x9c000001, "1(2),g", 0 }, /* pfld.l isrc1(isrc2),fdest */ +{ "pfld.l", 0x64000002, 0x98000001, "K(2),g", E_ADDR }, /* pfld.l #const(isrc2),fdest */ +{ "pfld.l", 0x60000003, 0x9c000000, "1(2)++,g", 0 }, /* pfld.l isrc1(isrc2)++,fdest */ +{ "pfld.l", 0x64000003, 0x98000000, "K(2)++,g", E_ADDR }, /* pfld.l #const(isrc2)++,fdest */ +{ "pfld.d", 0x60000000, 0x9c000007, "1(2),g", 0 }, /* pfld.d isrc1(isrc2),fdest */ +{ "pfld.d", 0x64000000, 0x98000007, "L(2),g", E_ADDR }, /* pfld.d #const(isrc2),fdest */ +{ "pfld.d", 0x60000001, 0x9c000006, "1(2)++,g", 0 }, /* pfld.d isrc1(isrc2)++,fdest */ +{ "pfld.d", 0x64000001, 0x98000006, "L(2)++,g", E_ADDR }, /* pfld.d #const(isrc2)++,fdest */ +{ "pfld.q", 0x60000004, 0x9c000003, "1(2),g", XP_ONLY }, /* pfld.q isrc1(isrc2),fdest */ +{ "pfld.q", 0x64000004, 0x98000003, "L(2),g", XP_ONLY }, /* pfld.q #const(isrc2),fdest */ +{ "pfld.q", 0x60000005, 0x9c000002, "1(2)++,g", XP_ONLY }, /* pfld.q isrc1(isrc2)++,fdest */ +{ "pfld.q", 0x64000005, 0x98000002, "L(2)++,g", XP_ONLY }, /* pfld.q #const(isrc2)++,fdest */ + +{ "fst.l", 0x28000002, 0xd4000001, "g,1(2)", 0 }, /* fst.l fdest,isrc1(isrc2) */ +{ "fst.l", 0x2c000002, 0xd0000001, "g,K(2)", E_ADDR }, /* fst.l fdest,#const(isrc2) */ +{ "fst.l", 0x28000003, 0xd4000000, "g,1(2)++", 0 }, /* fst.l fdest,isrc1(isrc2)++ */ +{ "fst.l", 0x2c000003, 0xd0000000, "g,K(2)++", E_ADDR }, /* fst.l fdest,#const(isrc2)++ */ +{ "fst.d", 0x28000000, 0xd4000007, "g,1(2)", 0 }, /* fst.d fdest,isrc1(isrc2) */ +{ "fst.d", 0x2c000000, 0xd0000007, "g,L(2)", E_ADDR }, /* fst.d fdest,#const(isrc2) */ +{ "fst.d", 0x28000001, 0xd4000006, "g,1(2)++", 0 }, /* fst.d fdest,isrc1(isrc2)++ */ +{ "fst.d", 0x2c000001, 0xd0000006, "g,L(2)++", E_ADDR }, /* fst.d fdest,#const(isrc2)++ */ +{ "fst.q", 0x28000004, 0xd4000003, "g,1(2)", 0 }, /* fst.d fdest,isrc1(isrc2) */ +{ "fst.q", 0x2c000004, 0xd0000003, "g,M(2)", E_ADDR }, /* fst.d fdest,#const(isrc2) */ +{ "fst.q", 0x28000005, 0xd4000002, "g,1(2)++", 0 }, /* fst.d fdest,isrc1(isrc2)++ */ +{ "fst.q", 0x2c000005, 0xd0000002, "g,M(2)++", E_ADDR }, /* fst.d fdest,#const(isrc2)++ */ + +{ "pst.d", 0x3c000000, 0xc0000007, "g,L(2)", E_ADDR }, /* pst.d fdest,#const(isrc2) */ +{ "pst.d", 0x3c000001, 0xc0000006, "g,L(2)++", E_ADDR }, /* pst.d fdest,#const(isrc2)++ */ + +{ "addu", 0x80000000, 0x7c000000, "1,2,d", 0 }, /* addu isrc1,isrc2,idest */ +{ "addu", 0x84000000, 0x78000000, "i,2,d", E_S32 }, /* addu #const,isrc2,idest */ +{ "adds", 0x90000000, 0x6c000000, "1,2,d", 0 }, /* adds isrc1,isrc2,idest */ +{ "adds", 0x94000000, 0x68000000, "i,2,d", E_S32 }, /* adds #const,isrc2,idest */ +{ "subu", 0x88000000, 0x74000000, "1,2,d", 0 }, /* subu isrc1,isrc2,idest */ +{ "subu", 0x8c000000, 0x70000000, "i,2,d", E_S32 }, /* subu #const,isrc2,idest */ +{ "subs", 0x98000000, 0x64000000, "1,2,d", 0 }, /* subs isrc1,isrc2,idest */ +{ "subs", 0x9c000000, 0x60000000, "i,2,d", E_S32 }, /* subs #const,isrc2,idest */ + +{ "shl", 0xa0000000, 0x5c000000, "1,2,d", 0 }, /* shl isrc1,isrc2,idest */ +{ "shl", 0xa4000000, 0x58000000, "i,2,d", 0 }, /* shl #const,isrc2,idest */ +{ "shr", 0xa8000000, 0x54000000, "1,2,d", 0 }, /* shr isrc1,isrc2,idest */ +{ "shr", 0xac000000, 0x50000000, "i,2,d", 0 }, /* shr #const,isrc2,idest */ +{ "shrd", 0xb0000000, 0x4c000000, "1,2,d", 0 }, /* shrd isrc1,isrc2,idest */ +{ "shra", 0xb8000000, 0x44000000, "1,2,d", 0 }, /* shra isrc1,isrc2,idest */ +{ "shra", 0xbc000000, 0x40000000, "i,2,d", 0 }, /* shra #const,isrc2,idest */ + +{ "mov", 0xa0000000, 0x5c00f800, "2,d", 0 }, /* shl r0,isrc2,idest */ +{ "mov", 0x94000000, 0x69e00000, "i,d", E_MOV }, /* adds #const,r0,idest */ +{ "nop", 0xa0000000, 0x5ffff800, "", 0 }, /* shl r0,r0,r0 */ +{ "fnop", 0xb0000000, 0x4ffff800, "", 0 }, /* shrd r0,r0,r0 */ + +{ "trap", 0x44000000, 0xb8000000, "1,2,d", 0 }, /* trap isrc1ni,isrc2,idest */ + +{ "flush", 0x34000004, 0xc81f0003, "L(2)", E_ADDR }, /* flush #const(isrc2) */ +{ "flush", 0x34000005, 0xc81f0002, "L(2)++", E_ADDR }, /* flush #const(isrc2)++ */ + +{ "and", 0xc0000000, 0x3c000000, "1,2,d", 0 }, /* and isrc1,isrc2,idest */ +{ "and", 0xc4000000, 0x38000000, "i,2,d", E_AND }, /* and #const,isrc2,idest */ +{ "andh", 0xcc000000, 0x30000000, "i,2,d", 0 }, /* andh #const,isrc2,idest */ +{ "andnot", 0xd0000000, 0x2c000000, "1,2,d", 0 }, /* andnot isrc1,isrc2,idest */ +{ "andnot", 0xd4000000, 0x28000000, "i,2,d", E_U32 }, /* andnot #const,isrc2,idest */ +{ "andnoth", 0xdc000000, 0x20000000, "i,2,d", 0 }, /* andnoth #const,isrc2,idest */ +{ "or", 0xe0000000, 0x1c000000, "1,2,d", 0 }, /* or isrc1,isrc2,idest */ +{ "or", 0xe4000000, 0x18000000, "i,2,d", E_U32 }, /* or #const,isrc2,idest */ +{ "orh", 0xec000000, 0x10000000, "i,2,d", 0 }, /* orh #const,isrc2,idest */ +{ "xor", 0xf0000000, 0x0c000000, "1,2,d", 0 }, /* xor isrc1,isrc2,idest */ +{ "xor", 0xf4000000, 0x08000000, "i,2,d", E_U32 }, /* xor #const,isrc2,idest */ +{ "xorh", 0xfc000000, 0x00000000, "i,2,d", 0 }, /* xorh #const,isrc2,idest */ + +{ "bte", 0x58000000, 0xa4000000, "1,2,r", 0 }, /* bte isrc1s,isrc2,sbroff */ +{ "bte", 0x5c000000, 0xa0000000, "5,2,r", 0 }, /* bte #const5,isrc2,sbroff */ +{ "btne", 0x50000000, 0xac000000, "1,2,r", 0 }, /* btne isrc1s,isrc2,sbroff */ +{ "btne", 0x54000000, 0xa8000000, "5,2,r", 0 }, /* btne #const5,isrc2,sbroff */ +{ "bla", 0xb4000000, 0x48000000, "1,2,r", E_DELAY }, /* bla isrc1s,isrc2,sbroff */ +{ "bri", 0x40000000, 0xbc000000, "1", E_DELAY }, /* bri isrc1ni */ + +/* Core Escape Instruction Format */ +{ "lock", 0x4c000001, 0xb000001e, "", 0 }, /* lock set BL in dirbase */ +{ "calli", 0x4c000002, 0xb000001d, "1", E_DELAY }, /* calli isrc1ni */ +{ "intovr", 0x4c000004, 0xb000001b, "", 0 }, /* intovr trap on integer overflow */ +{ "unlock", 0x4c000007, 0xb0000018, "", 0 }, /* unlock clear BL in dirbase */ +{ "ldio.l", 0x4c000408, 0xb00003f7, "2,d", XP_ONLY }, /* ldio.l isrc2,idest */ +{ "ldio.s", 0x4c000208, 0xb00005f7, "2,d", XP_ONLY }, /* ldio.s isrc2,idest */ +{ "ldio.b", 0x4c000008, 0xb00007f7, "2,d", XP_ONLY }, /* ldio.b isrc2,idest */ +{ "stio.l", 0x4c000409, 0xb00003f6, "1,2", XP_ONLY }, /* stio.l isrc1ni,isrc2 */ +{ "stio.s", 0x4c000209, 0xb00005f6, "1,2", XP_ONLY }, /* stio.s isrc1ni,isrc2 */ +{ "stio.b", 0x4c000009, 0xb00007f6, "1,2", XP_ONLY }, /* stio.b isrc1ni,isrc2 */ +{ "ldint.l", 0x4c00040a, 0xb00003f5, "2,d", XP_ONLY }, /* ldint.l isrc2,idest */ +{ "ldint.s", 0x4c00020a, 0xb00005f5, "2,d", XP_ONLY }, /* ldint.s isrc2,idest */ +{ "ldint.b", 0x4c00000a, 0xb00007f5, "2,d", XP_ONLY }, /* ldint.b isrc2,idest */ +{ "scyc.b", 0x4c00000b, 0xb00007f4, "2", XP_ONLY }, /* scyc.b isrc2 */ + +/* CTRL-Format Instructions */ +{ "br", 0x68000000, 0x94000000, "l", E_DELAY }, /* br lbroff */ +{ "call", 0x6c000000, 0x90000000, "l", E_DELAY }, /* call lbroff */ +{ "bc", 0x70000000, 0x8c000000, "l", 0 }, /* bc lbroff */ +{ "bc.t", 0x74000000, 0x88000000, "l", E_DELAY }, /* bc.t lbroff */ +{ "bnc", 0x78000000, 0x84000000, "l", 0 }, /* bnc lbroff */ +{ "bnc.t", 0x7c000000, 0x80000000, "l", E_DELAY }, /* bnc.t lbroff */ + +/* Floating Point Escape Instruction Format - pfam.p fsrc1,fsrc2,fdest. */ +{ "r2p1.ss", 0x48000400, 0xb40001ff, "e,f,g", 0 }, +{ "r2p1.sd", 0x48000480, 0xb400017f, "e,f,g", 0 }, +{ "r2p1.dd", 0x48000580, 0xb400007f, "e,f,g", 0 }, +{ "r2pt.ss", 0x48000401, 0xb40001fe, "e,f,g", 0 }, +{ "r2pt.sd", 0x48000481, 0xb400017e, "e,f,g", 0 }, +{ "r2pt.dd", 0x48000581, 0xb400007e, "e,f,g", 0 }, +{ "r2ap1.ss", 0x48000402, 0xb40001fd, "e,f,g", 0 }, +{ "r2ap1.sd", 0x48000482, 0xb400017d, "e,f,g", 0 }, +{ "r2ap1.dd", 0x48000582, 0xb400007d, "e,f,g", 0 }, +{ "r2apt.ss", 0x48000403, 0xb40001fc, "e,f,g", 0 }, +{ "r2apt.sd", 0x48000483, 0xb400017c, "e,f,g", 0 }, +{ "r2apt.dd", 0x48000583, 0xb400007c, "e,f,g", 0 }, +{ "i2p1.ss", 0x48000404, 0xb40001fb, "e,f,g", 0 }, +{ "i2p1.sd", 0x48000484, 0xb400017b, "e,f,g", 0 }, +{ "i2p1.dd", 0x48000584, 0xb400007b, "e,f,g", 0 }, +{ "i2pt.ss", 0x48000405, 0xb40001fa, "e,f,g", 0 }, +{ "i2pt.sd", 0x48000485, 0xb400017a, "e,f,g", 0 }, +{ "i2pt.dd", 0x48000585, 0xb400007a, "e,f,g", 0 }, +{ "i2ap1.ss", 0x48000406, 0xb40001f9, "e,f,g", 0 }, +{ "i2ap1.sd", 0x48000486, 0xb4000179, "e,f,g", 0 }, +{ "i2ap1.dd", 0x48000586, 0xb4000079, "e,f,g", 0 }, +{ "i2apt.ss", 0x48000407, 0xb40001f8, "e,f,g", 0 }, +{ "i2apt.sd", 0x48000487, 0xb4000178, "e,f,g", 0 }, +{ "i2apt.dd", 0x48000587, 0xb4000078, "e,f,g", 0 }, +{ "rat1p2.ss", 0x48000408, 0xb40001f7, "e,f,g", 0 }, +{ "rat1p2.sd", 0x48000488, 0xb4000177, "e,f,g", 0 }, +{ "rat1p2.dd", 0x48000588, 0xb4000077, "e,f,g", 0 }, +{ "m12apm.ss", 0x48000409, 0xb40001f6, "e,f,g", 0 }, +{ "m12apm.sd", 0x48000489, 0xb4000176, "e,f,g", 0 }, +{ "m12apm.dd", 0x48000589, 0xb4000076, "e,f,g", 0 }, +{ "ra1p2.ss", 0x4800040a, 0xb40001f5, "e,f,g", 0 }, +{ "ra1p2.sd", 0x4800048a, 0xb4000175, "e,f,g", 0 }, +{ "ra1p2.dd", 0x4800058a, 0xb4000075, "e,f,g", 0 }, +{ "m12ttpa.ss", 0x4800040b, 0xb40001f4, "e,f,g", 0 }, +{ "m12ttpa.sd", 0x4800048b, 0xb4000174, "e,f,g", 0 }, +{ "m12ttpa.dd", 0x4800058b, 0xb4000074, "e,f,g", 0 }, +{ "iat1p2.ss", 0x4800040c, 0xb40001f3, "e,f,g", 0 }, +{ "iat1p2.sd", 0x4800048c, 0xb4000173, "e,f,g", 0 }, +{ "iat1p2.dd", 0x4800058c, 0xb4000073, "e,f,g", 0 }, +{ "m12tpm.ss", 0x4800040d, 0xb40001f2, "e,f,g", 0 }, +{ "m12tpm.sd", 0x4800048d, 0xb4000172, "e,f,g", 0 }, +{ "m12tpm.dd", 0x4800058d, 0xb4000072, "e,f,g", 0 }, +{ "ia1p2.ss", 0x4800040e, 0xb40001f1, "e,f,g", 0 }, +{ "ia1p2.sd", 0x4800048e, 0xb4000171, "e,f,g", 0 }, +{ "ia1p2.dd", 0x4800058e, 0xb4000071, "e,f,g", 0 }, +{ "m12tpa.ss", 0x4800040f, 0xb40001f0, "e,f,g", 0 }, +{ "m12tpa.sd", 0x4800048f, 0xb4000170, "e,f,g", 0 }, +{ "m12tpa.dd", 0x4800058f, 0xb4000070, "e,f,g", 0 }, + +/* Floating Point Escape Instruction Format - pfsm.p fsrc1,fsrc2,fdest. */ +{ "r2s1.ss", 0x48000410, 0xb40001ef, "e,f,g", 0 }, +{ "r2s1.sd", 0x48000490, 0xb400016f, "e,f,g", 0 }, +{ "r2s1.dd", 0x48000590, 0xb400006f, "e,f,g", 0 }, +{ "r2st.ss", 0x48000411, 0xb40001ee, "e,f,g", 0 }, +{ "r2st.sd", 0x48000491, 0xb400016e, "e,f,g", 0 }, +{ "r2st.dd", 0x48000591, 0xb400006e, "e,f,g", 0 }, +{ "r2as1.ss", 0x48000412, 0xb40001ed, "e,f,g", 0 }, +{ "r2as1.sd", 0x48000492, 0xb400016d, "e,f,g", 0 }, +{ "r2as1.dd", 0x48000592, 0xb400006d, "e,f,g", 0 }, +{ "r2ast.ss", 0x48000413, 0xb40001ec, "e,f,g", 0 }, +{ "r2ast.sd", 0x48000493, 0xb400016c, "e,f,g", 0 }, +{ "r2ast.dd", 0x48000593, 0xb400006c, "e,f,g", 0 }, +{ "i2s1.ss", 0x48000414, 0xb40001eb, "e,f,g", 0 }, +{ "i2s1.sd", 0x48000494, 0xb400016b, "e,f,g", 0 }, +{ "i2s1.dd", 0x48000594, 0xb400006b, "e,f,g", 0 }, +{ "i2st.ss", 0x48000415, 0xb40001ea, "e,f,g", 0 }, +{ "i2st.sd", 0x48000495, 0xb400016a, "e,f,g", 0 }, +{ "i2st.dd", 0x48000595, 0xb400006a, "e,f,g", 0 }, +{ "i2as1.ss", 0x48000416, 0xb40001e9, "e,f,g", 0 }, +{ "i2as1.sd", 0x48000496, 0xb4000169, "e,f,g", 0 }, +{ "i2as1.dd", 0x48000596, 0xb4000069, "e,f,g", 0 }, +{ "i2ast.ss", 0x48000417, 0xb40001e8, "e,f,g", 0 }, +{ "i2ast.sd", 0x48000497, 0xb4000168, "e,f,g", 0 }, +{ "i2ast.dd", 0x48000597, 0xb4000068, "e,f,g", 0 }, +{ "rat1s2.ss", 0x48000418, 0xb40001e7, "e,f,g", 0 }, +{ "rat1s2.sd", 0x48000498, 0xb4000167, "e,f,g", 0 }, +{ "rat1s2.dd", 0x48000598, 0xb4000067, "e,f,g", 0 }, +{ "m12asm.ss", 0x48000419, 0xb40001e6, "e,f,g", 0 }, +{ "m12asm.sd", 0x48000499, 0xb4000166, "e,f,g", 0 }, +{ "m12asm.dd", 0x48000599, 0xb4000066, "e,f,g", 0 }, +{ "ra1s2.ss", 0x4800041a, 0xb40001e5, "e,f,g", 0 }, +{ "ra1s2.sd", 0x4800049a, 0xb4000165, "e,f,g", 0 }, +{ "ra1s2.dd", 0x4800059a, 0xb4000065, "e,f,g", 0 }, +{ "m12ttsa.ss", 0x4800041b, 0xb40001e4, "e,f,g", 0 }, +{ "m12ttsa.sd", 0x4800049b, 0xb4000164, "e,f,g", 0 }, +{ "m12ttsa.dd", 0x4800059b, 0xb4000064, "e,f,g", 0 }, +{ "iat1s2.ss", 0x4800041c, 0xb40001e3, "e,f,g", 0 }, +{ "iat1s2.sd", 0x4800049c, 0xb4000163, "e,f,g", 0 }, +{ "iat1s2.dd", 0x4800059c, 0xb4000063, "e,f,g", 0 }, +{ "m12tsm.ss", 0x4800041d, 0xb40001e2, "e,f,g", 0 }, +{ "m12tsm.sd", 0x4800049d, 0xb4000162, "e,f,g", 0 }, +{ "m12tsm.dd", 0x4800059d, 0xb4000062, "e,f,g", 0 }, +{ "ia1s2.ss", 0x4800041e, 0xb40001e1, "e,f,g", 0 }, +{ "ia1s2.sd", 0x4800049e, 0xb4000161, "e,f,g", 0 }, +{ "ia1s2.dd", 0x4800059e, 0xb4000061, "e,f,g", 0 }, +{ "m12tsa.ss", 0x4800041f, 0xb40001e0, "e,f,g", 0 }, +{ "m12tsa.sd", 0x4800049f, 0xb4000160, "e,f,g", 0 }, +{ "m12tsa.dd", 0x4800059f, 0xb4000060, "e,f,g", 0 }, + +/* Floating Point Escape Instruction Format - pfmam.p fsrc1,fsrc2,fdest. */ +{ "mr2p1.ss", 0x48000000, 0xb40005ff, "e,f,g", 0 }, +{ "mr2p1.sd", 0x48000080, 0xb400057f, "e,f,g", 0 }, +{ "mr2p1.dd", 0x48000180, 0xb400047f, "e,f,g", 0 }, +{ "mr2pt.ss", 0x48000001, 0xb40005fe, "e,f,g", 0 }, +{ "mr2pt.sd", 0x48000081, 0xb400057e, "e,f,g", 0 }, +{ "mr2pt.dd", 0x48000181, 0xb400047e, "e,f,g", 0 }, +{ "mr2mp1.ss", 0x48000002, 0xb40005fd, "e,f,g", 0 }, +{ "mr2mp1.sd", 0x48000082, 0xb400057d, "e,f,g", 0 }, +{ "mr2mp1.dd", 0x48000182, 0xb400047d, "e,f,g", 0 }, +{ "mr2mpt.ss", 0x48000003, 0xb40005fc, "e,f,g", 0 }, +{ "mr2mpt.sd", 0x48000083, 0xb400057c, "e,f,g", 0 }, +{ "mr2mpt.dd", 0x48000183, 0xb400047c, "e,f,g", 0 }, +{ "mi2p1.ss", 0x48000004, 0xb40005fb, "e,f,g", 0 }, +{ "mi2p1.sd", 0x48000084, 0xb400057b, "e,f,g", 0 }, +{ "mi2p1.dd", 0x48000184, 0xb400047b, "e,f,g", 0 }, +{ "mi2pt.ss", 0x48000005, 0xb40005fa, "e,f,g", 0 }, +{ "mi2pt.sd", 0x48000085, 0xb400057a, "e,f,g", 0 }, +{ "mi2pt.dd", 0x48000185, 0xb400047a, "e,f,g", 0 }, +{ "mi2mp1.ss", 0x48000006, 0xb40005f9, "e,f,g", 0 }, +{ "mi2mp1.sd", 0x48000086, 0xb4000579, "e,f,g", 0 }, +{ "mi2mp1.dd", 0x48000186, 0xb4000479, "e,f,g", 0 }, +{ "mi2mpt.ss", 0x48000007, 0xb40005f8, "e,f,g", 0 }, +{ "mi2mpt.sd", 0x48000087, 0xb4000578, "e,f,g", 0 }, +{ "mi2mpt.dd", 0x48000187, 0xb4000478, "e,f,g", 0 }, +{ "mrmt1p2.ss", 0x48000008, 0xb40005f7, "e,f,g", 0 }, +{ "mrmt1p2.sd", 0x48000088, 0xb4000577, "e,f,g", 0 }, +{ "mrmt1p2.dd", 0x48000188, 0xb4000477, "e,f,g", 0 }, +{ "mm12mpm.ss", 0x48000009, 0xb40005f6, "e,f,g", 0 }, +{ "mm12mpm.sd", 0x48000089, 0xb4000576, "e,f,g", 0 }, +{ "mm12mpm.dd", 0x48000189, 0xb4000476, "e,f,g", 0 }, +{ "mrm1p2.ss", 0x4800000a, 0xb40005f5, "e,f,g", 0 }, +{ "mrm1p2.sd", 0x4800008a, 0xb4000575, "e,f,g", 0 }, +{ "mrm1p2.dd", 0x4800018a, 0xb4000475, "e,f,g", 0 }, +{ "mm12ttpm.ss",0x4800000b, 0xb40005f4, "e,f,g", 0 }, +{ "mm12ttpm.sd",0x4800008b, 0xb4000574, "e,f,g", 0 }, +{ "mm12ttpm.dd",0x4800018b, 0xb4000474, "e,f,g", 0 }, +{ "mimt1p2.ss", 0x4800000c, 0xb40005f3, "e,f,g", 0 }, +{ "mimt1p2.sd", 0x4800008c, 0xb4000573, "e,f,g", 0 }, +{ "mimt1p2.dd", 0x4800018c, 0xb4000473, "e,f,g", 0 }, +{ "mm12tpm.ss", 0x4800000d, 0xb40005f2, "e,f,g", 0 }, +{ "mm12tpm.sd", 0x4800008d, 0xb4000572, "e,f,g", 0 }, +{ "mm12tpm.dd", 0x4800018d, 0xb4000472, "e,f,g", 0 }, +{ "mim1p2.ss", 0x4800000e, 0xb40005f1, "e,f,g", 0 }, +{ "mim1p2.sd", 0x4800008e, 0xb4000571, "e,f,g", 0 }, +{ "mim1p2.dd", 0x4800018e, 0xb4000471, "e,f,g", 0 }, + +/* Floating Point Escape Instruction Format - pfmsm.p fsrc1,fsrc2,fdest. */ +{ "mr2s1.ss", 0x48000010, 0xb40005ef, "e,f,g", 0 }, +{ "mr2s1.sd", 0x48000090, 0xb400056f, "e,f,g", 0 }, +{ "mr2s1.dd", 0x48000190, 0xb400046f, "e,f,g", 0 }, +{ "mr2st.ss", 0x48000011, 0xb40005ee, "e,f,g", 0 }, +{ "mr2st.sd", 0x48000091, 0xb400056e, "e,f,g", 0 }, +{ "mr2st.dd", 0x48000191, 0xb400046e, "e,f,g", 0 }, +{ "mr2ms1.ss", 0x48000012, 0xb40005ed, "e,f,g", 0 }, +{ "mr2ms1.sd", 0x48000092, 0xb400056d, "e,f,g", 0 }, +{ "mr2ms1.dd", 0x48000192, 0xb400046d, "e,f,g", 0 }, +{ "mr2mst.ss", 0x48000013, 0xb40005ec, "e,f,g", 0 }, +{ "mr2mst.sd", 0x48000093, 0xb400056c, "e,f,g", 0 }, +{ "mr2mst.dd", 0x48000193, 0xb400046c, "e,f,g", 0 }, +{ "mi2s1.ss", 0x48000014, 0xb40005eb, "e,f,g", 0 }, +{ "mi2s1.sd", 0x48000094, 0xb400056b, "e,f,g", 0 }, +{ "mi2s1.dd", 0x48000194, 0xb400046b, "e,f,g", 0 }, +{ "mi2st.ss", 0x48000015, 0xb40005ea, "e,f,g", 0 }, +{ "mi2st.sd", 0x48000095, 0xb400056a, "e,f,g", 0 }, +{ "mi2st.dd", 0x48000195, 0xb400046a, "e,f,g", 0 }, +{ "mi2ms1.ss", 0x48000016, 0xb40005e9, "e,f,g", 0 }, +{ "mi2ms1.sd", 0x48000096, 0xb4000569, "e,f,g", 0 }, +{ "mi2ms1.dd", 0x48000196, 0xb4000469, "e,f,g", 0 }, +{ "mi2mst.ss", 0x48000017, 0xb40005e8, "e,f,g", 0 }, +{ "mi2mst.sd", 0x48000097, 0xb4000568, "e,f,g", 0 }, +{ "mi2mst.dd", 0x48000197, 0xb4000468, "e,f,g", 0 }, +{ "mrmt1s2.ss", 0x48000018, 0xb40005e7, "e,f,g", 0 }, +{ "mrmt1s2.sd", 0x48000098, 0xb4000567, "e,f,g", 0 }, +{ "mrmt1s2.dd", 0x48000198, 0xb4000467, "e,f,g", 0 }, +{ "mm12msm.ss", 0x48000019, 0xb40005e6, "e,f,g", 0 }, +{ "mm12msm.sd", 0x48000099, 0xb4000566, "e,f,g", 0 }, +{ "mm12msm.dd", 0x48000199, 0xb4000466, "e,f,g", 0 }, +{ "mrm1s2.ss", 0x4800001a, 0xb40005e5, "e,f,g", 0 }, +{ "mrm1s2.sd", 0x4800009a, 0xb4000565, "e,f,g", 0 }, +{ "mrm1s2.dd", 0x4800019a, 0xb4000465, "e,f,g", 0 }, +{ "mm12ttsm.ss",0x4800001b, 0xb40005e4, "e,f,g", 0 }, +{ "mm12ttsm.sd",0x4800009b, 0xb4000564, "e,f,g", 0 }, +{ "mm12ttsm.dd",0x4800019b, 0xb4000464, "e,f,g", 0 }, +{ "mimt1s2.ss", 0x4800001c, 0xb40005e3, "e,f,g", 0 }, +{ "mimt1s2.sd", 0x4800009c, 0xb4000563, "e,f,g", 0 }, +{ "mimt1s2.dd", 0x4800019c, 0xb4000463, "e,f,g", 0 }, +{ "mm12tsm.ss", 0x4800001d, 0xb40005e2, "e,f,g", 0 }, +{ "mm12tsm.sd", 0x4800009d, 0xb4000562, "e,f,g", 0 }, +{ "mm12tsm.dd", 0x4800019d, 0xb4000462, "e,f,g", 0 }, +{ "mim1s2.ss", 0x4800001e, 0xb40005e1, "e,f,g", 0 }, +{ "mim1s2.sd", 0x4800009e, 0xb4000561, "e,f,g", 0 }, +{ "mim1s2.dd", 0x4800019e, 0xb4000461, "e,f,g", 0 }, + +{ "fmul.ss", 0x48000020, 0xb40005df, "e,f,g", 0 }, /* fmul.p fsrc1,fsrc2,fdest */ +{ "fmul.sd", 0x480000a0, 0xb400055f, "e,f,g", 0 }, /* fmul.p fsrc1,fsrc2,fdest */ +{ "fmul.dd", 0x480001a0, 0xb400045f, "e,f,g", 0 }, /* fmul.p fsrc1,fsrc2,fdest */ +{ "pfmul.ss", 0x48000420, 0xb40001df, "e,f,g", 0 }, /* pfmul.p fsrc1,fsrc2,fdest */ +{ "pfmul.sd", 0x480004a0, 0xb400015f, "e,f,g", 0 }, /* pfmul.p fsrc1,fsrc2,fdest */ +{ "pfmul.dd", 0x480005a0, 0xb400005f, "e,f,g", 0 }, /* pfmul.p fsrc1,fsrc2,fdest */ +{ "pfmul3.dd", 0x480005a4, 0xb400005b, "e,f,g", 0 }, /* pfmul3.p fsrc1,fsrc2,fdest */ +{ "fmlow.dd", 0x480001a1, 0xb400045e, "e,f,g", 0 }, /* fmlow.dd fsrc1,fsrc2,fdest */ +{ "frcp.ss", 0x48000022, 0xb40005dd, "f,g", 0 }, /* frcp.p fsrc2,fdest */ +{ "frcp.sd", 0x480000a2, 0xb400055d, "f,g", 0 }, /* frcp.p fsrc2,fdest */ +{ "frcp.dd", 0x480001a2, 0xb400045d, "f,g", 0 }, /* frcp.p fsrc2,fdest */ +{ "frsqr.ss", 0x48000023, 0xb40005dc, "f,g", 0 }, /* frsqr.p fsrc2,fdest */ +{ "frsqr.sd", 0x480000a3, 0xb400055c, "f,g", 0 }, /* frsqr.p fsrc2,fdest */ +{ "frsqr.dd", 0x480001a3, 0xb400045c, "f,g", 0 }, /* frsqr.p fsrc2,fdest */ +{ "fadd.ss", 0x48000030, 0xb40005cf, "e,f,g", 0 }, /* fadd.p fsrc1,fsrc2,fdest */ +{ "fadd.sd", 0x480000b0, 0xb400054f, "e,f,g", 0 }, /* fadd.p fsrc1,fsrc2,fdest */ +{ "fadd.dd", 0x480001b0, 0xb400044f, "e,f,g", 0 }, /* fadd.p fsrc1,fsrc2,fdest */ +{ "pfadd.ss", 0x48000430, 0xb40001cf, "e,f,g", 0 }, /* pfadd.p fsrc1,fsrc2,fdest */ +{ "pfadd.sd", 0x480004b0, 0xb400014f, "e,f,g", 0 }, /* pfadd.p fsrc1,fsrc2,fdest */ +{ "pfadd.dd", 0x480005b0, 0xb400004f, "e,f,g", 0 }, /* pfadd.p fsrc1,fsrc2,fdest */ +{ "fsub.ss", 0x48000031, 0xb40005ce, "e,f,g", 0 }, /* fsub.p fsrc1,fsrc2,fdest */ +{ "fsub.sd", 0x480000b1, 0xb400054e, "e,f,g", 0 }, /* fsub.p fsrc1,fsrc2,fdest */ +{ "fsub.dd", 0x480001b1, 0xb400044e, "e,f,g", 0 }, /* fsub.p fsrc1,fsrc2,fdest */ +{ "pfsub.ss", 0x48000431, 0xb40001ce, "e,f,g", 0 }, /* pfsub.p fsrc1,fsrc2,fdest */ +{ "pfsub.sd", 0x480004b1, 0xb400014e, "e,f,g", 0 }, /* pfsub.p fsrc1,fsrc2,fdest */ +{ "pfsub.dd", 0x480005b1, 0xb400004e, "e,f,g", 0 }, /* pfsub.p fsrc1,fsrc2,fdest */ +{ "fix.sd", 0x480000b2, 0xb400054d, "e,g", 0 }, /* fix.p fsrc1,fdest */ +{ "fix.dd", 0x480001b2, 0xb400044d, "e,g", 0 }, /* fix.p fsrc1,fdest */ +{ "pfix.sd", 0x480004b2, 0xb400014d, "e,g", 0 }, /* pfix.p fsrc1,fdest */ +{ "pfix.dd", 0x480005b2, 0xb400004d, "e,g", 0 }, /* pfix.p fsrc1,fdest */ +{ "famov.ss", 0x48000033, 0xb40005cc, "e,g", 0 }, /* famov.p fsrc1,fdest */ +{ "famov.ds", 0x48000133, 0xb40004cc, "e,g", 0 }, /* famov.p fsrc1,fdest */ +{ "famov.sd", 0x480000b3, 0xb400054c, "e,g", 0 }, /* famov.p fsrc1,fdest */ +{ "famov.dd", 0x480001b3, 0xb400044c, "e,g", 0 }, /* famov.p fsrc1,fdest */ +{ "pfamov.ss", 0x48000433, 0xb40001cc, "e,g", 0 }, /* pfamov.p fsrc1,fdest */ +{ "pfamov.ds", 0x48000533, 0xb40000cc, "e,g", 0 }, /* pfamov.p fsrc1,fdest */ +{ "pfamov.sd", 0x480004b3, 0xb400014c, "e,g", 0 }, /* pfamov.p fsrc1,fdest */ +{ "pfamov.dd", 0x480005b3, 0xb400004c, "e,g", 0 }, /* pfamov.p fsrc1,fdest */ +/* Opcode pfgt has R bit cleared; pfle has R bit set. */ +{ "pfgt.ss", 0x48000434, 0xb40001cb, "e,f,g", 0 }, /* pfgt.p fsrc1,fsrc2,fdest */ +{ "pfgt.dd", 0x48000534, 0xb40000cb, "e,f,g", 0 }, /* pfgt.p fsrc1,fsrc2,fdest */ +/* Opcode pfgt has R bit cleared; pfle has R bit set. */ +{ "pfle.ss", 0x480004b4, 0xb400014b, "e,f,g", 0 }, /* pfle.p fsrc1,fsrc2,fdest */ +{ "pfle.dd", 0x480005b4, 0xb400004b, "e,f,g", 0 }, /* pfle.p fsrc1,fsrc2,fdest */ +{ "pfeq.ss", 0x48000435, 0xb40001ca, "e,f,g", 0 }, /* pfeq.p fsrc1,fsrc2,fdest */ +{ "pfeq.dd", 0x48000535, 0xb40000ca, "e,f,g", 0 }, /* pfeq.p fsrc1,fsrc2,fdest */ +{ "ftrunc.sd", 0x480000ba, 0xb4000545, "e,g", 0 }, /* ftrunc.p fsrc1,fdest */ +{ "ftrunc.dd", 0x480001ba, 0xb4000445, "e,g", 0 }, /* ftrunc.p fsrc1,fdest */ +{ "pftrunc.sd", 0x480004ba, 0xb4000145, "e,g", 0 }, /* pftrunc.p fsrc1,fdest */ +{ "pftrunc.dd", 0x480005ba, 0xb4000045, "e,g", 0 }, /* pftrunc.p fsrc1,fdest */ +{ "fxfr", 0x48000040, 0xb40005bf, "e,d", 0 }, /* fxfr fsrc1,idest */ +{ "fiadd.ss", 0x48000049, 0xb40005b6, "e,f,g", 0 }, /* fiadd.w fsrc1,fsrc2,fdest */ +{ "fiadd.dd", 0x480001c9, 0xb4000436, "e,f,g", 0 }, /* fiadd.w fsrc1,fsrc2,fdest */ +{ "pfiadd.ss", 0x48000449, 0xb40001b6, "e,f,g", 0 }, /* pfiadd.w fsrc1,fsrc2,fdest */ +{ "pfiadd.dd", 0x480005c9, 0xb4000036, "e,f,g", 0 }, /* pfiadd.w fsrc1,fsrc2,fdest */ +{ "fisub.ss", 0x4800004d, 0xb40005b2, "e,f,g", 0 }, /* fisub.w fsrc1,fsrc2,fdest */ +{ "fisub.dd", 0x480001cd, 0xb4000432, "e,f,g", 0 }, /* fisub.w fsrc1,fsrc2,fdest */ +{ "pfisub.ss", 0x4800044d, 0xb40001b2, "e,f,g", 0 }, /* pfisub.w fsrc1,fsrc2,fdest */ +{ "pfisub.dd", 0x480005cd, 0xb4000032, "e,f,g", 0 }, /* pfisub.w fsrc1,fsrc2,fdest */ +{ "fzchkl", 0x480001d7, 0xb4000428, "e,f,g", 0 }, /* fzchkl fsrc1,fsrc2,fdest */ +{ "pfzchkl", 0x480005d7, 0xb4000028, "e,f,g", 0 }, /* pfzchkl fsrc1,fsrc2,fdest */ +{ "fzchks", 0x480001df, 0xb4000420, "e,f,g", 0 }, /* fzchks fsrc1,fsrc2,fdest */ +{ "pfzchks", 0x480005df, 0xb4000020, "e,f,g", 0 }, /* pfzchks fsrc1,fsrc2,fdest */ +{ "faddp", 0x480001d0, 0xb400042f, "e,f,g", 0 }, /* faddp fsrc1,fsrc2,fdest */ +{ "pfaddp", 0x480005d0, 0xb400002f, "e,f,g", 0 }, /* pfaddp fsrc1,fsrc2,fdest */ +{ "faddz", 0x480001d1, 0xb400042e, "e,f,g", 0 }, /* faddz fsrc1,fsrc2,fdest */ +{ "pfaddz", 0x480005d1, 0xb400002e, "e,f,g", 0 }, /* pfaddz fsrc1,fsrc2,fdest */ +{ "form", 0x480001da, 0xb4000425, "e,g", 0 }, /* form fsrc1,fdest */ +{ "pform", 0x480005da, 0xb4000025, "e,g", 0 }, /* pform fsrc1,fdest */ + +/* Floating point pseudo-instructions. */ +{ "fmov.ss", 0x48000049, 0xb7e005b6, "e,g", 0 }, /* fiadd.ss fsrc1,f0,fdest */ +{ "fmov.dd", 0x480001c9, 0xb7e00436, "e,g", 0 }, /* fiadd.dd fsrc1,f0,fdest */ +{ "fmov.sd", 0x480000b3, 0xb400054c, "e,g", 0 }, /* famov.sd fsrc1,fdest */ +{ "fmov.ds", 0x48000133, 0xb40004cc, "e,g", 0 }, /* famov.ds fsrc1,fdest */ +{ "pfmov.ds", 0x48000533, 0xb40000cc, "e,g", 0 }, /* pfamov.ds fsrc1,fdest */ +{ "pfmov.dd", 0x480005c9, 0xb7e00036, "e,g", 0 }, /* pfiadd.dd fsrc1,f0,fdest */ +{ 0, 0, 0, 0, 0 }, + +}; + +#define NUMOPCODES ((sizeof i860_opcodes)/(sizeof i860_opcodes[0])) + + diff --git a/scratchpad/i860dis.py b/scratchpad/i860dis.py new file mode 100644 index 0000000..bd83a1d --- /dev/null +++ b/scratchpad/i860dis.py @@ -0,0 +1,99 @@ +"""Minimal i860 XR/XP disassembler driven by the binutils opcode table. + +Usage: python i860dis.py [len_hex] +Disassembles VREND.MNG code (loads at 0xf0400000; file header 0x1C). + +Rebuilt 2026-07-12 (the task-#55 original didn't survive); opcode truth = +scratchpad/i860_opcodes.h (binutils include/opcode/i860.h, haiku legacy +mirror). Operand syntax per the header comment. +""" +import re, struct, sys, os + +HERE = os.path.dirname(os.path.abspath(__file__)) +MNG = os.path.join(HERE, '..', 'content', 'VREND.MNG') +HDR = 0x1C +CODEBASE = 0xf0400000 +CODESZ = 0x3ac80 +DATASZ = 0x1e9c0 + +def load_ops(): + src = open(os.path.join(HERE, 'i860_opcodes.h')).read() + ops = [] + for m in re.finditer(r'\{\s*"([^"]+)",\s*(0x[0-9a-fA-F]+),\s*(0x[0-9a-fA-F]+),\s*"([^"]*)",\s*(\w+)\s*\}', src): + name, match, lose, args, expand = m.groups() + ops.append((name, int(match, 16), int(lose, 16), args)) + return ops + +OPS = load_ops() + +def fields(w): + return { + 'src2': (w >> 21) & 0x1F, + 'dest': (w >> 16) & 0x1F, + 'src1': (w >> 11) & 0x1F, + 'imm16': w & 0xFFFF, + 'imm5': (w >> 11) & 0x1F, + 'lbroff': w & 0x03FFFFFF, + 'sbroff': ((w >> 5) & 0xF800) | (w & 0x07FF), # split 16-bit branch offset + 'split16': ((w >> 5) & 0xF800) | (w & 0x07FF), # split 16-bit (st) immediate + 'creg': (w >> 21) & 0x1F, + } + +def sext(v, bits): + return v - (1 << bits) if v & (1 << (bits - 1)) else v + +def decode(w, pc): + for name, match, lose, args in OPS: + if (w & match) == match and (w & lose) == 0: + # binutils order guarantees no false positive only if we check + # match as EXACT on the fixed bits: (w & ~varmask) == match. + # Reconstruct: fixed bits = match | lose; variable = operands. + f = fields(w) + out = [] + for a in args: + if a in '#,()': continue + if a == '1': out.append('r%d' % f['src1']) + elif a == '2': out.append('r%d' % f['src2']) + elif a == 'd': out.append('r%d' % f['dest']) + elif a in 'iIJKLM': out.append('0x%x' % f['imm16']) + elif a == '5': out.append('%d' % f['imm5']) + elif a == 'l': + tgt = pc + 4 + (sext(f['lbroff'], 26) << 2) + out.append('0x%08x' % tgt) + elif a == 'r': + tgt = pc + 4 + (sext(f['sbroff'], 16) << 2) + out.append('0x%08x' % tgt) + elif a in 'sSTU': out.append('0x%x' % f['split16']) + elif a == 'e': out.append('f%d' % f['src1']) + elif a == 'f': out.append('f%d' % f['src2']) + elif a == 'g': out.append('f%d' % f['dest']) + elif a == 'c': out.append('creg%d' % f['creg']) + else: out.append('?%s' % a) + return name, out + return None, None + +def candidates(w): + """All table rows consistent with word w (the table has overlaps; the + first (most specific) is printed, rest shown when ambiguous).""" + res = [] + for name, match, lose, args in OPS: + if (w & match) == match and (w & lose) == 0: + res.append(name) + return res + +def main(): + data = open(MNG, 'rb').read() + code = data[HDR:HDR + CODESZ] + va = int(sys.argv[1], 16) + ln = int(sys.argv[2], 16) if len(sys.argv) > 2 else 0x100 + off = va - CODEBASE + for pc in range(va, va + ln, 4): + w = struct.unpack_from('