Combat: AUTHENTIC weapon groups -- streamed per-mech button bindings + the real fire chain (task #5)
The recovered system: fire channels = LBE4ControlsManager buttonGroups (0x40/0x45/0x46/0x47); default groups = the per-mech type-6 controls-map resource in BTL4.RES, installed by the T0 CreateStreamedMappings the port already called -- it needed only the TriggerState attribute (id 0x13 PINNED to the binary value; fireImpulse@0x31C is the binary's TriggerState) and an input feed. Keyboard/harness now push press/release edges into the button groups; the gBT*Trigger bypasses, per-type keyboard split and 1,0 pulse hack are retired -- weapons sharing a button fire TOGETHER (madcat Trigger = 4 weapons). Myomers @4b9550/@4b95b8 misattribution corrected (they are MechWeapon ConfigureMappables/ChooseButton). Verified 2-node: kill through the authentic chain (12 hits vs ~36 pre-groups). Config-mode session (regrouping UI) = the remaining stage, KB-scoped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -110,16 +110,26 @@ authentic path scoped.
|
|||||||
- **MechControlsMapper look/eyepoint commit** — reconstructed but its offsets collide with declared
|
- **MechControlsMapper look/eyepoint commit** — reconstructed but its offsets collide with declared
|
||||||
members; arbitrate before enabling (no port consumer yet). The fire-trigger-through-mapper is
|
members; arbitrate before enabling (no port consumer yet). The fire-trigger-through-mapper is
|
||||||
still the `gBTWeaponTrigger` bring-up.
|
still the `gBTWeaponTrigger` bring-up.
|
||||||
- **Weapon groups / per-button fire channels (decomp-anchored 2026-07-09)** — MechWeapon message
|
- **✅ Weapon groups — the AUTHENTIC defaults + fire path LIVE (task #5, 2026-07-11) [T1/T2].**
|
||||||
handlers id 9 **`ConfigureMappables`** (`FUN_004b9550`) and id 10 **`ChooseButton`**
|
The system decoded end-to-end: a fire "channel" = a physical `buttonGroup` slot in the global
|
||||||
(`FUN_004b95b8`), table @0x511860 region: ConfigureMappables binds the weapon's
|
`LBE4ControlsManager` (fire buttons 0x40 Trigger / 0x45 Pinky / 0x46 ThumbLow / 0x47 ThumbHigh);
|
||||||
`fireImpulse`@0x31C to mapper fire-channel #N (msg value 1-based; <1 unbinds; mapper = roster
|
binding = a permanent mode-0x10000 DIRECT mapping button → the weapon's **TriggerState** attr
|
||||||
slot 0, vtable +0x38 register / +0x3c unregister; weapon+0x110 = the bound-state), ChooseButton
|
(id **0x13**, = `fireImpulse@0x31C`; the binary MechWeapon table's ONLY attribute @0x511890);
|
||||||
re-binds (vtable +0x44) — the pod's in-cockpit weapon→button GROUPING. Weapons sharing a button
|
the weapon self-edge-detects (CheckFireEdge @4b9608). **The default grouping is NOT messages —
|
||||||
fire together; grouping was a PILOT choice (heat management: alpha vs split). The port's
|
it is the per-mech type-6 controls-map resource in BTL4.RES** ("Thrustmaster"/"L4" variants;
|
||||||
SPACE=all-energy / CTRL=missiles split is a stand-in default; the authentic system needs the
|
e.g. madcat: Trigger={4 weapons}, ThumbHigh={2}), installed at `MakeViewpointEntity` by the
|
||||||
mapper channel registration + who SENDS the default ConfigureMappables (mission/player setup —
|
ENGINE's `CreateStreamedMappings` (T0, L4CTRL.cpp) — which the port already called; it needed
|
||||||
not yet traced). Payoff: authentic weapon groups + a path to chain-fire.
|
only the TriggerState attribute published (id PINNED to 0x13 — our attr-id chain is 6 short of
|
||||||
|
the binary's; full binary attr-table dump in the task #5 scan) + an input feed. Keyboard now
|
||||||
|
pushes press/release edges into the buttonGroups (SPACE→Trigger, '2'→ThumbLow, '3'/CTRL→
|
||||||
|
ThumbHigh; BT_AUTOFIRE pulses the Trigger); the gBT*Trigger bypasses + pulse hack are retired.
|
||||||
|
VERIFIED 2-node: full kill through the authentic chain (12 hits vs ~36 pre-groups = the
|
||||||
|
4-weapon trigger volley + crit cascade). **Remaining (the config-mode session):** handlers
|
||||||
|
id 9/10 (@4b9550/@4b95b8 — corrected: they were mislabeled as Myomers fns), the L4 mapper
|
||||||
|
Enter/ExitConfiguration completion (StartMappableButtonsConfigure + mode 0x8000), the
|
||||||
|
ConfigMapGauge unguard — the pod's in-cockpit REGROUPING UI (pilot heat-management choice);
|
||||||
|
mapper vtable corrections: +0x38 Enter / +0x3c Exit / +0x44 RIO AddOrErase (Thrustmaster
|
||||||
|
no-ops it).
|
||||||
- **Myomers authentic coupling** — the structural un-stub is INERT (mover feed + heat-gen no-op).
|
- **Myomers authentic coupling** — the structural un-stub is INERT (mover feed + heat-gen no-op).
|
||||||
Real coupling needs the advanced-damage gate (`OwnerAdvancedDamage`/`FUN_004ad7d4` → the owning
|
Real coupling needs the advanced-damage gate (`OwnerAdvancedDamage`/`FUN_004ad7d4` → the owning
|
||||||
**BTPlayer** `mech+0x190`+0x260, NOT 0xBD3 — see the mech+0x190 item above) + `MoverAttach` routing
|
**BTPlayer** `mech+0x190`+0x260, NOT 0xBD3 — see the mech+0x190 item above) + `MoverAttach` routing
|
||||||
|
|||||||
@@ -267,19 +267,13 @@ void
|
|||||||
// what makes weapon self-heat propagate to the mech's heat sinks.
|
// what makes weapon self-heat propagate to the mech's heat sinks.
|
||||||
PoweredSubsystem::PoweredSubsystemSimulation(time_slice);
|
PoweredSubsystem::PoweredSubsystemSimulation(time_slice);
|
||||||
|
|
||||||
// E8 (bring-up trigger): write the weapon's fireImpulse from the player fire input
|
// AUTHENTIC TRIGGER (task #5): fireImpulse IS the published TriggerState
|
||||||
// (the controls mapper is bypassed). The triggers are pulsed per-FRAME by mech4
|
// attribute (id 0x13); the streamed per-mech controls map (type-6 resource,
|
||||||
// (1,0,1,0...) so CheckFireEdge sees clean rising edges for repeated auto-fire under a
|
// installed at MakeViewpointEntity) binds each grouped fire BUTTON directly
|
||||||
// held/forced trigger. HasActiveTarget (E3) limits firing to the mech with a locked
|
// onto it, and the keyboard/harness pushes press/release edges into the
|
||||||
// target (the player) -- enemy emitters read this too but never fire (no target).
|
// LBE4 buttonGroup channels (mech4). The old gBT*Trigger bypass + the
|
||||||
// WEAPON GROUPS (task #43, interim keyboard split standing in for the authentic
|
// per-type keyboard split are retired -- weapons sharing a button now fire
|
||||||
// ConfigureMappables/ChooseButton mapper channels): lasers (0xBC8=3016) and PPCs
|
// TOGETHER (the pod's authored groups: e.g. madcat Trigger = 4 weapons).
|
||||||
// (0xBD4=3028) read SEPARATE fire channels, like two pod buttons.
|
|
||||||
extern int gBTWeaponTrigger; // channel 1: lasers
|
|
||||||
extern int gBTPPCTrigger; // channel 2: PPCs
|
|
||||||
fireImpulse = (Scalar)(((int)GetClassID() == 3028) ? gBTPPCTrigger
|
|
||||||
: gBTWeaponTrigger);
|
|
||||||
|
|
||||||
Logical fireEdge = CheckFireEdge(); // @004b9608
|
Logical fireEdge = CheckFireEdge(); // @004b9608
|
||||||
targetWithinRange = UpdateTargeting(); // @004b9bdc -> this+0x34c
|
targetWithinRange = UpdateTargeting(); // @004b9bdc -> this+0x34c
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,17 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#include <bt.hpp>
|
#include <bt.hpp>
|
||||||
|
// LBE4ControlsManager (the fire buttonGroup push, task #5) -- needs the
|
||||||
|
// btl4app/btl4mode forward-decl order btl4mppr.cpp uses.
|
||||||
|
#if !defined(BTL4APP_HPP)
|
||||||
|
# include <btl4app.hpp>
|
||||||
|
#endif
|
||||||
|
#if !defined(BTL4MODE_HPP)
|
||||||
|
# include <btl4mode.hpp>
|
||||||
|
#endif
|
||||||
|
#if !defined(L4CTRL_HPP)
|
||||||
|
# include <l4ctrl.hpp>
|
||||||
|
#endif
|
||||||
#pragma hdrstop
|
#pragma hdrstop
|
||||||
|
|
||||||
#if !defined(MECH_HPP)
|
#if !defined(MECH_HPP)
|
||||||
@@ -2990,12 +3001,48 @@ void
|
|||||||
// (user-reported regression). targetInArc is the explicit BT_FIRE_ARC
|
// (user-reported regression). targetInArc is the explicit BT_FIRE_ARC
|
||||||
// presentation clamp (default true). BT_AUTOFIRE holds all three.
|
// presentation clamp (default true). BT_AUTOFIRE holds all three.
|
||||||
{
|
{
|
||||||
const int laserWanted = gBTDrive.fireForced || gBTLaserKey;
|
// AUTHENTIC FIRE INPUT (task #5): key states become PRESS/RELEASE
|
||||||
const int ppcWanted = gBTDrive.fireForced || gBTPPCKey;
|
// edges pushed into the LBE4 buttonGroup channels -- the pod's own
|
||||||
const int missileWanted = gBTDrive.fireForced || gBTMissileKey;
|
// joystick path (L4CTRL Execute: Update(&v, mode) with v =
|
||||||
gBTWeaponTrigger = (laserWanted && targetInArc) ? (gBTWeaponTrigger ? 0 : 1) : 0;
|
// +(button+1) press / -(button+1) release). The streamed per-mech
|
||||||
gBTPPCTrigger = (ppcWanted && targetInArc) ? (gBTPPCTrigger ? 0 : 1) : 0;
|
// controls map binds these buttons DIRECTLY onto each grouped
|
||||||
gBTMissileTrigger = (missileWanted && targetInArc) ? (gBTMissileTrigger ? 0 : 1) : 0;
|
// weapon's TriggerState@0x31C; CheckFireEdge does the rest. This
|
||||||
|
// retires the gBT*Trigger globals + the 1,0 pulse hack.
|
||||||
|
// Desktop map: SPACE/'1' -> Trigger(0x40) [the main fire group],
|
||||||
|
// '2' -> ThumbLow(0x46), '3'/CTRL -> ThumbHigh(0x47) [missiles on
|
||||||
|
// most mechs]. BT_AUTOFIRE pulses the Trigger (edge per 2 frames).
|
||||||
|
// targetInArc stays the opt-in BT_FIRE_ARC presentation clamp.
|
||||||
|
LBE4ControlsManager *cm =
|
||||||
|
(LBE4ControlsManager *)application->GetControlsManager();
|
||||||
|
ModeMask modeMask = application->GetModeManager()->GetModeMask();
|
||||||
|
static int s_prevBtn[4] = {0, 0, 0, 0};
|
||||||
|
static int s_afPhase = 0;
|
||||||
|
s_afPhase ^= 1;
|
||||||
|
const int autofire = (gBTDrive.fireForced && targetInArc) ? s_afPhase : 0;
|
||||||
|
const int buttons[4] = {
|
||||||
|
LBE4ControlsManager::ButtonJoystickTrigger, // 0x40
|
||||||
|
LBE4ControlsManager::ButtonJoystickPinky, // 0x45
|
||||||
|
LBE4ControlsManager::ButtonJoystickThumbLow, // 0x46
|
||||||
|
LBE4ControlsManager::ButtonJoystickThumbHigh, // 0x47
|
||||||
|
};
|
||||||
|
int want[4];
|
||||||
|
want[0] = ((gBTLaserKey && targetInArc) || autofire) ? 1 : 0;
|
||||||
|
want[1] = 0; // pinky: unmapped on desktop
|
||||||
|
want[2] = (gBTPPCKey && targetInArc) ? 1 : 0;
|
||||||
|
want[3] = (gBTMissileKey && targetInArc) ? 1 : 0;
|
||||||
|
if (cm != 0)
|
||||||
|
{
|
||||||
|
for (int b = 0; b < 4; ++b)
|
||||||
|
{
|
||||||
|
if (want[b] != s_prevBtn[b])
|
||||||
|
{
|
||||||
|
s_prevBtn[b] = want[b];
|
||||||
|
ControlsButton v = (ControlsButton)(want[b]
|
||||||
|
? (buttons[b] + 1) : -(buttons[b] + 1));
|
||||||
|
cm->buttonGroup[buttons[b]].Update(&v, modeMask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ Simulation::AttributeIndexSet MechWeapon::AttributeIndex; // legacy empty member
|
|||||||
const MechWeapon::IndexEntry
|
const MechWeapon::IndexEntry
|
||||||
MechWeapon::AttributePointers[]=
|
MechWeapon::AttributePointers[]=
|
||||||
{
|
{
|
||||||
|
// The authentic entry (binary table @0x511890): the streamed controls maps
|
||||||
|
// bind the pod fire buttons directly onto this Scalar -- fireImpulse IS the
|
||||||
|
// binary's "TriggerState" (the edge-detected fire channel destination).
|
||||||
|
ATTRIBUTE_ENTRY(MechWeapon, TriggerState, fireImpulse), // @0x31C id 0x13
|
||||||
ATTRIBUTE_ENTRY(MechWeapon, OutputVoltage, rechargeLevel), // @0x320 (charge/ready level)
|
ATTRIBUTE_ENTRY(MechWeapon, OutputVoltage, rechargeLevel), // @0x320 (charge/ready level)
|
||||||
ATTRIBUTE_ENTRY(MechWeapon, PercentDone, rechargeLevel) // @0x320 (recharge dial fraction)
|
ATTRIBUTE_ENTRY(MechWeapon, PercentDone, rechargeLevel) // @0x320 (recharge dial fraction)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -122,8 +122,21 @@ class CockpitHud;
|
|||||||
//
|
//
|
||||||
public:
|
public:
|
||||||
enum {
|
enum {
|
||||||
OutputVoltageAttributeID = PoweredSubsystem::NextAttributeID,
|
// TriggerState is the BINARY's only MechWeapon attribute
|
||||||
PercentDoneAttributeID,
|
// ({0x13, "TriggerState", +0x31d} @0x511890 -- fireImpulse@0x31C).
|
||||||
|
// The streamed per-mech controls maps (type-6 resources) bind the
|
||||||
|
// fire buttons DIRECTLY onto it by this NUMERIC id, so it is
|
||||||
|
// PINNED to the binary value: the binary's subsystem chain reaches
|
||||||
|
// 0x12 before MechWeapon (ConfigureActivePress@MechSubsystem=2,
|
||||||
|
// heat family 3..0xC, powered/aux-screen family 0xD..0x11 -- full
|
||||||
|
// dump in the task #5 attr-table scan), while our reconstruction
|
||||||
|
// publishes fewer base attrs (chain value 0xD). The id gap wastes
|
||||||
|
// a few dense-index slots (Build sizes by max id) -- harmless.
|
||||||
|
// OutputVoltage/PercentDone are port gauge additions
|
||||||
|
// (name-resolved) renumbered after it.
|
||||||
|
TriggerStateAttributeID = 0x13,
|
||||||
|
OutputVoltageAttributeID, // 0x14
|
||||||
|
PercentDoneAttributeID, // 0x15
|
||||||
NextAttributeID
|
NextAttributeID
|
||||||
};
|
};
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -464,9 +464,16 @@ void Myomers::ResetToInitialState(Logical powered)
|
|||||||
|
|
||||||
|
|
||||||
//***************************************************************************
|
//***************************************************************************
|
||||||
// Myomers::ConnectToMover @004b9550
|
// Myomers::ConnectToMover (mislabeled)
|
||||||
//***************************************************************************
|
//***************************************************************************
|
||||||
//
|
//
|
||||||
|
// ⚠ MISATTRIBUTION (task #5 correction): @004b9550/@004b95b8 are NOT Myomers
|
||||||
|
// functions -- the weapon message-table @0x511860 proves they are
|
||||||
|
// MechWeapon::ConfigureMappables (id 9) / ChooseButton (id 10), the weapon->
|
||||||
|
// button grouping handlers (the +0x31C coincidence: Myomers speedEffect vs
|
||||||
|
// MechWeapon TriggerState). The bodies below are retained as the port's
|
||||||
|
// Myomers stand-ins but carry no binary address.
|
||||||
|
//
|
||||||
// Attaches the live SpeedEffect output (@0x31C) into the Mech's JointedMover
|
// Attaches the live SpeedEffect output (@0x31C) into the Mech's JointedMover
|
||||||
// roster (the mover at **(Mech[0xD0]+0x128)). Driven by a connect message
|
// roster (the mover at **(Mech[0xD0]+0x128)). Driven by a connect message
|
||||||
// whose payload index is at message+0xC:
|
// whose payload index is at message+0xC:
|
||||||
|
|||||||
@@ -534,21 +534,15 @@ void
|
|||||||
DrawFiringCharge(time_slice); // @004bbc78 -- bleed recoil (+0x3E8)
|
DrawFiringCharge(time_slice); // @004bbc78 -- bleed recoil (+0x3E8)
|
||||||
UpdateEject(time_slice); // @004bbb50 -- idle unless ejectState==0
|
UpdateEject(time_slice); // @004bbb50 -- idle unless ejectState==0
|
||||||
|
|
||||||
// 2. Rising-edge trigger from the owning Mech's discharge signal.
|
// 2. AUTHENTIC TRIGGER (task #5): fireImpulse = the TriggerState attribute
|
||||||
// Bring-up: the controls mapper is bypassed, so drive fireImpulse from the
|
// the streamed controls map binds to the pod fire buttons (see emitter.cpp
|
||||||
// per-frame trigger pulse -- else CheckFireEdge never sees an edge and a
|
// note). The gBTMissileTrigger bypass is retired.
|
||||||
// projectile weapon never fires. SPLIT CONTROLS (interim, toward the real
|
|
||||||
// controls-mapper weapon groups): projectile weapons (the missile
|
|
||||||
// launchers, cannon) fire on the MISSILE channel (CTRL), not the
|
|
||||||
// energy-weapon channel (SPACE) -- one key no longer fires everything.
|
|
||||||
extern int gBTMissileTrigger;
|
|
||||||
fireImpulse = (Scalar)gBTMissileTrigger; // this+0x31C
|
|
||||||
Logical trigger = CheckFireEdge(); // MechWeapon @004b9608
|
Logical trigger = CheckFireEdge(); // MechWeapon @004b9608
|
||||||
|
|
||||||
static int s_pwtick = 0;
|
static int s_pwtick = 0;
|
||||||
if (getenv("BT_PROJ_LOG") && (((++s_pwtick) % 240) == 0 || trigger))
|
if (getenv("BT_PROJ_LOG") && (((++s_pwtick) % 240) == 0 || trigger))
|
||||||
DEBUG_STREAM << "[projweap] tick " << GetName() << " trig=" << (int)trigger
|
DEBUG_STREAM << "[projweap] tick " << GetName() << " trig=" << (int)trigger
|
||||||
<< " gTrig=" << gBTMissileTrigger << " ready=" << (int)ReadyToFire()
|
<< " trigState=" << (float)fireImpulse << " ready=" << (int)ReadyToFire()
|
||||||
<< " state=" << weaponAlarm.GetState() << " recoil=" << recoil << " x\n" << std::flush;
|
<< " state=" << weaponAlarm.GetState() << " recoil=" << recoil << " x\n" << std::flush;
|
||||||
|
|
||||||
// 3. Firing state machine (weaponAlarm @0x350). Modelled on the RP analog
|
// 3. Firing state machine (weaponAlarm @0x350). Modelled on the RP analog
|
||||||
|
|||||||
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 290 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 276 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
from PIL import Image
|
||||||
|
import numpy as np
|
||||||
|
a=np.array(Image.open('scratchpad/cop_fwd_s.png').convert('RGB').resize((360,270)))
|
||||||
|
b=np.array(Image.open('scratchpad/cop_flip_s.png').convert('RGB').resize((360,270)))
|
||||||
|
gap=np.full((270,6,3),255,np.uint8)
|
||||||
|
Image.fromarray(np.concatenate([a,gap,b],axis=1)).save('scratchpad/cop_ab.png')
|
||||||
|
def stat(x): return "mean=%s greenfrac=%.2f"%(x.reshape(-1,3).mean(0).round(0), ((x[:,:,1]>100)&(x[:,:,0]<80)).mean())
|
||||||
|
print("forward:",stat(a)); print("flip: ",stat(b))
|
||||||
|
print("saved cop_ab.png [forward | flip]")
|
||||||
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 140 KiB |
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Robust BGF (DIV-BIZ2) parser for the cockpit-shell reconstruction harness.
|
||||||
|
Handles the vertex-format chunks the engine loader (bgfload.cpp) does:
|
||||||
|
0x80 XYZ(12) 0x81 XYZ_N(24) 0x82 XYZ_RGBA(28)
|
||||||
|
0x88 XYZ_UV(20) 0x89 XYZ_N_UV(32) 0x8A XYZ_RGBA_UV(36)
|
||||||
|
Faces: 0x47 index list (u32 tri indices), 0x4d poly list (ppf-prefixed u32).
|
||||||
|
Tree: 0x40 container / 0x41 object / 0x42 patch / 0x46 mesh; 0x2037 SV_SPECIAL
|
||||||
|
(carries "PUNCH"); 0x2030 material name. Face indices are patch-local (base=patch
|
||||||
|
vert start). Returns per-patch geometry so we see frame vs glass vs punch."""
|
||||||
|
import struct
|
||||||
|
|
||||||
|
VSTRIDE = {0x80:12, 0x81:24, 0x82:28, 0x88:20, 0x89:32, 0x8A:36}
|
||||||
|
VHASN = {0x81, 0x89}
|
||||||
|
VHASUV = {0x88, 0x89, 0x8A}
|
||||||
|
CONTAINERS = {0x40,0x41,0x42,0x46,0x48,0x70,0x10,0x20,0x30,0x3}
|
||||||
|
|
||||||
|
def parse(path):
|
||||||
|
d = open(path,"rb").read()
|
||||||
|
patches = [] # each: dict(material, punch, verts[list (x,y,z,has_n,nx,ny,nz,u,v)], faces[list (a,b,c)])
|
||||||
|
cur = {"material":"", "punch":False}
|
||||||
|
def read_chunk(p, end):
|
||||||
|
if p+2 > end: return None
|
||||||
|
tw = struct.unpack_from("<H", d, p)[0]
|
||||||
|
cid = tw & 0x2fff
|
||||||
|
lw = 4 if (tw & 0x8000) else (2 if (tw & 0x4000) else 1)
|
||||||
|
if p+2+lw > end: return None
|
||||||
|
if lw==1: ln = d[p+2]
|
||||||
|
elif lw==2: ln = struct.unpack_from("<H", d, p+2)[0]
|
||||||
|
else: ln = struct.unpack_from("<I", d, p+2)[0]
|
||||||
|
body = p+2+lw
|
||||||
|
return cid, body, ln, body+ln
|
||||||
|
def walk(p, end, patch):
|
||||||
|
while p < end:
|
||||||
|
r = read_chunk(p, end)
|
||||||
|
if not r: break
|
||||||
|
cid, body, ln, nxt = r
|
||||||
|
if cid == 0x42: # new patch
|
||||||
|
patch = {"material":"", "punch":False, "verts":[], "faces":[]}
|
||||||
|
patches.append(patch)
|
||||||
|
walk(body, nxt, patch)
|
||||||
|
elif cid == 0x2030: # material name (string)
|
||||||
|
if patch is not None:
|
||||||
|
patch["material"] = d[body:body+ln].split(b'\0')[0].decode('latin1','ignore')
|
||||||
|
elif cid == 0x2037: # SV_SPECIAL -> PUNCH?
|
||||||
|
if patch is not None and b"PUNCH" in d[body:body+ln]:
|
||||||
|
patch["punch"] = True
|
||||||
|
elif cid in VSTRIDE: # vertex chunk -> this submesh's face indices are
|
||||||
|
st = VSTRIDE[cid]; hasn = cid in VHASN; hasuv = cid in VHASUV
|
||||||
|
patch["_base"] = len(patch["verts"]) # local to THIS chunk (base offset)
|
||||||
|
n = ln // st
|
||||||
|
for i in range(n):
|
||||||
|
o = body + i*st
|
||||||
|
x,y,z = struct.unpack_from("<fff", d, o)
|
||||||
|
nx=ny=nz=0.0; u=v=0.0; off=12
|
||||||
|
if hasn:
|
||||||
|
nx,ny,nz = struct.unpack_from("<fff", d, o+off); off+=12
|
||||||
|
if hasuv:
|
||||||
|
u,v = struct.unpack_from("<ff", d, o+off)
|
||||||
|
patch["verts"].append((x,y,z,hasn,nx,ny,nz,u,v))
|
||||||
|
elif cid == 0x47: # tri index list (indices local to last vertex chunk)
|
||||||
|
b = patch.get("_base",0); n = ln//4
|
||||||
|
idx = [struct.unpack_from("<I", d, body+i*4)[0] for i in range(n)]
|
||||||
|
for f in range(0,(n//3)*3,3):
|
||||||
|
patch["faces"].append((b+idx[f],b+idx[f+1],b+idx[f+2]))
|
||||||
|
elif cid == 0x4d: # poly list (ppf-prefixed)
|
||||||
|
if ln>=1:
|
||||||
|
b = patch.get("_base",0); ppf = d[body]; n=(ln-1)//4
|
||||||
|
idx=[struct.unpack_from("<I",d,body+1+i*4)[0] for i in range(n)]
|
||||||
|
for f in range(0,(n//ppf)*ppf,ppf):
|
||||||
|
poly=[idx[f+j] for j in range(ppf)]
|
||||||
|
for k in range(1,ppf-1):
|
||||||
|
patch["faces"].append((b+poly[0],b+poly[k],b+poly[k+1]))
|
||||||
|
elif cid in CONTAINERS:
|
||||||
|
walk(body, nxt, patch)
|
||||||
|
p = nxt
|
||||||
|
walk(8, len(d), None)
|
||||||
|
return [pt for pt in patches if pt["verts"] and pt["faces"]]
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys, numpy as np
|
||||||
|
from collections import Counter
|
||||||
|
path = sys.argv[1] if len(sys.argv)>1 else "content/VIDEO/GEO/MAX_COP.BGF"
|
||||||
|
P = parse(path)
|
||||||
|
print(f"{path}: {len(P)} patches")
|
||||||
|
allV=[]; ec=Counter(); tot_faces=0; punch_faces=0
|
||||||
|
for i,pt in enumerate(P):
|
||||||
|
V=np.array([(v[0],v[1],v[2]) for v in pt["verts"]])
|
||||||
|
hasuv = any(v[7]!=0 or v[8]!=0 for v in pt["verts"])
|
||||||
|
hasn = any(v[3] for v in pt["verts"])
|
||||||
|
F=pt["faces"]
|
||||||
|
lo=V.min(0); hi=V.max(0); cen=V.mean(0)
|
||||||
|
for a,b,c in F:
|
||||||
|
if max(a,b,c)>=len(V): continue
|
||||||
|
for e in ((a,b),(b,c),(c,a)): ec[tuple(sorted(e))]+=1
|
||||||
|
tot_faces+=len(F)
|
||||||
|
if pt["punch"]: punch_faces+=len(F)
|
||||||
|
print(f" patch{i}: mtl='{pt['material'][:30]}' punch={pt['punch']} "
|
||||||
|
f"verts={len(V)} faces={len(F)} uv={hasuv} n={hasn} "
|
||||||
|
f"cen=({cen[0]:.2f},{cen[1]:.2f},{cen[2]:.2f}) "
|
||||||
|
f"bbox=[{lo[0]:.2f},{lo[1]:.2f},{lo[2]:.2f}]..[{hi[0]:.2f},{hi[1]:.2f},{hi[2]:.2f}]")
|
||||||
|
bound=sum(1 for k,vv in ec.items() if vv==1)
|
||||||
|
print(f"TOTAL faces={tot_faces} punch_faces={punch_faces} boundary_edges={bound}/{len(ec)}")
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import struct,sys
|
||||||
|
|
||||||
|
def rdU16(b,p): return struct.unpack_from('<H',b,p)[0]
|
||||||
|
def rdU32(b,p): return struct.unpack_from('<I',b,p)[0]
|
||||||
|
def rdF32(b,p): return struct.unpack_from('<f',b,p)[0]
|
||||||
|
|
||||||
|
TAGNAME={0x0003:'HEADER',0x0005:'BIZ_DONE',0x0040:'OBJECT',0x0041:'LOD',0x0042:'PATCH',
|
||||||
|
0x0046:'PMESH',0x0047:'CONN_LIST',0x0048:'SPHERE_LIST',0x004d:'PCONN_LIST',0x0070:'BOUND',
|
||||||
|
0x0080:'VTX_XYZ',0x0081:'VTX_XYZ_N',0x0082:'VTX_XYZ_RGBA',0x0088:'VTX_XYZ_UV',
|
||||||
|
0x0089:'VTX_XYZ_N_UV',0x008A:'VTX_XYZ_RGBA_UV',0x2030:'SV_F_MATERIAL',0x2037:'SV_SPECIAL',
|
||||||
|
0x0010:'TEXTURE',0x0011:'TEXTURE_MAP',0x0018:'BITSLICE',0x0020:'MATERIAL',
|
||||||
|
0x0021:'MAT_TEXTURE',0x0023:'AMBIENT',0x0024:'DIFFUSE',0x0026:'EMISSIVE',
|
||||||
|
0x0028:'RAMP_REF',0x0030:'RAMP',0x0031:'RAMP_DATA',0x2008:'NAME',
|
||||||
|
0x0043:'FACE',0x0044:'FACE?',0x0045:'FACE?'}
|
||||||
|
CONTAINERS={0x0003,0x0070,0x0040,0x0041,0x0042,0x0046,0x0048,0x0010,0x0020,0x0030}
|
||||||
|
|
||||||
|
def parse(b,p,end,depth,out):
|
||||||
|
while p+3<=end:
|
||||||
|
tw=rdU16(b,p); p+=2
|
||||||
|
idv=tw & 0x2fff
|
||||||
|
lw=4 if (tw&0x8000) else (2 if (tw&0x4000) else 1)
|
||||||
|
if p+lw>end: break
|
||||||
|
ln = b[p] if lw==1 else (rdU16(b,p) if lw==2 else rdU32(b,p))
|
||||||
|
p+=lw
|
||||||
|
if p+ln>end: break
|
||||||
|
out.append((depth,idv,p,ln))
|
||||||
|
if idv in CONTAINERS:
|
||||||
|
parse(b,p,p+ln,depth+1,out)
|
||||||
|
p+=ln
|
||||||
|
if idv==0x0005: break
|
||||||
|
return
|
||||||
|
|
||||||
|
fn=sys.argv[1]
|
||||||
|
b=open(fn,'rb').read()
|
||||||
|
assert b[:8]==b'DIV-BIZ2', b[:8]
|
||||||
|
out=[]
|
||||||
|
parse(b,8,len(b),0,out)
|
||||||
|
for depth,idv,p,ln in out:
|
||||||
|
nm=TAGNAME.get(idv,'?%04x'%idv)
|
||||||
|
extra=''
|
||||||
|
if idv==0x2008 or idv==0x0011 or idv==0x0028:
|
||||||
|
s=b[p:p+ln].split(b'\0')[0].decode('latin1',errors='replace')
|
||||||
|
extra=' "%s"'%s
|
||||||
|
if idv==0x2037: # SV_SPECIAL
|
||||||
|
s=b[p:p+ln].split(b'\0')[0].decode('latin1',errors='replace')
|
||||||
|
extra=' SPECIAL="%s"'%s
|
||||||
|
if idv==0x2030: # SV_F_MATERIAL
|
||||||
|
s=b[p:p+ln].split(b'\0')[0].decode('latin1',errors='replace')
|
||||||
|
extra=' MAT="%s" (len %d)'%(s,ln)
|
||||||
|
if idv==0x0018 and ln>=1: extra=' slice=%d'%b[p]
|
||||||
|
print(' '*depth + '%-14s off=0x%04x len=%d%s'%(nm,p,ln,extra))
|
||||||
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 331 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 231 KiB |
|
After Width: | Height: | Size: 233 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 292 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 297 KiB |
|
After Width: | Height: | Size: 432 KiB |
|
After Width: | Height: | Size: 358 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 365 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 459 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 432 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 115 KiB |