Commit Graph

13 Commits

Author SHA1 Message Date
MechaCat02
c6f7a18e30 fix(lint): the hand-judged clippy sites in library code
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 16:36:43 +02:00
ed54f95d54 style: rustfmt sweep -- 774 hunks across 154 files -> 0
`cargo fmt --all -- --check` has failed on every run in this repository's
history, identically on `main` and on every branch. This is #12.

Mechanical: `cargo fmt --all`, nothing else. 154 files, all `.rs`, no other
extension touched. `cargo check --workspace` exits 0 afterwards, so nothing
changed semantically.

ON THE ORDERING, WHICH WAS THE REAL QUESTION.

HANDOFF-2026-09-06 section 7 warns this is the expensive fix: a whole-tree
reformat before #7 and #8 return "would put a conflict in every file of 861
commits and make the reviews those items exist to enable unreadable".

That is measurably too pessimistic, and it had been reasoned rather than
tested. Measured here by three-way merging a rustfmt'd `main` against both
unmerged branches, file by file:

  file/branch pairs tested   32
  merges CLEAN               28
  merges CONFLICTING          4   (8 conflict hunks total)

    sylpheed-cli/src/main.rs      1 hunk
    sylpheed-export/src/check.rs  1
    sylpheed-export/src/screen.rs 4
    sylpheed-export/src/video.rs  2

All four are against `auto/frame-blend-draw-path` only;
`auto/port-p6-audio` does not conflict anywhere. The earlier framing --
154 dirty files, 133 that cannot collide, 21 that can, the collision set
carrying 147 of 774 hunks (19%) -- reproduces exactly. What it did not say
is that most of the 21 still merge cleanly, because rustfmt's edits and the
branches' edits rarely land on the same lines.

So the cost of sweeping now is 4 files and 8 hunks for one branch, against
a check that is otherwise red forever. Deliberately NOT folded into the
WASM PR: 154 reformatted files would make that one unreviewable.

Closes #12
2026-09-08 20:07:01 +02:00
Sylpheed RE agent
39954414f4 ship: pin the e106 ground truth as a multiset, not a set of names
`static_assembly_matches_runtime_capture` walks the capture's parts and looks
each up in ours, so an extra static placement was originally invisible to it —
that is how `e303_wep_01` sat here decoded 100x too large. `a21aa5f` closed that
with a set-of-names check; BACKLOG.md still says otherwise, and it is wrong.

One direction stayed open, though: a set cannot fail on a resource placed
*twice* when the capture lists it once. That is not hypothetical — a duplicated
instance is what a bad node walk emits, and the two legitimate duplicates here
(`e106_eng_01`, `e303_wep_01`) are why the test had to special-case counts at
all. So pin the whole multiset — 9 resources, 11 placements, against the
capture's 8 dedup'd parts — which subsumes both hand-written counts and fails on
an extra, a missing, or a duplicated placement.

Refuted before believing: perturbing the expected table (declaring
`e106_bdy_01` twice) fails with the real multiset on the left, so the assertion
runs against the disc rather than skipping for want of SYLPHEED_ISO.

The remaining `for want in &cap.parts` loop guards the table itself: editing it
cannot quietly drop a part the runtime actually draws.
2026-08-18 17:14:24 +00:00
5f566ce1f8 fix(mesh): distinct anchor assignment -- no two resources may claim one buffer
Selection was per-resource and greedy, so two resources could take one vertex
buffer while a valid one sat unused. A runtime capture proved that wrong for the
mirrored e106 hull twins: the container holds both halves and the engine draws
each from its own address. Now the first claimant keeps a buffer and later
resources re-anchor past everything already claimed (coverage can never regress;
grouped-pool models untouched).

Against the 46 capture-named Stage_S02 buffers: exact anchors 29 -> 40, unclaimed
12 -> 4. Disc-wide: 5480 resources decoded (unchanged), cross-container
inconsistency 125 -> 46.

The twins' mirror therefore lives in the DATA, not in the placement matrix: the
embedded e106_bdy_02 row and the two assertions encoding the old convention are
updated, each with the reason recorded. Full suite green incl. disc/ISO gates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 03:16:51 +00:00
a21aa5fa45 revert(mesh): withdraw the neighbourhood anchor -- it regressed the e106 twin mirror
The neighbourhood anchor (1b1936c) and its refinement (f0dc201) took
cross-container inconsistency from 125 to 51 with coverage unchanged, and made
e106 render as a destroyer rather than a slab. Both are reverted.

ship::tests::static_assembly_matches_runtime_capture is gated on SYLPHEED_ISO, so
it SKIPS in an ordinary cargo test -- which is why the regression was invisible
in every suite run so far. With the ISO it fails:

  e106_bdy_01: static M row0 [-1.0, 0.0, 0.0] != captured [1.0, 0.0, 0.0]

e106_bdy_01 and _02 are a mirrored pair whose two buffers hold the same geometry
reflected in X, and BOTH resources currently decode to the SAME buffer (identical
counts, span and mean_x). apply_twin_mirrors picks which instance to reflect from
the sign of that mean_x, so which buffer wins flips the decision:

  before  both twins mean_x = -66.83  -> mirror bdy_02  (matches the capture)
  after   both twins mean_x = +66.83  -> mirror bdy_01  (contradicts it)

Neither is right -- two resources sharing one decode is itself the bug and the
mirror heuristic has been compensating. The capture is ground truth, so a change
that contradicts it does not ship. The real fix must give each twin its own
buffer first.

Kept from the attempt: this test now also asserts the SET of static placements
against the capture (allow-list {e303_wep_01} for vbase dedup), so extra
placements can finally fail it -- the direction it could never fail in before.

Docs, backlog, INDEX and the ignored test's message all corrected to say
diagnosed-not-fixed rather than fixed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 01:08:41 +00:00
b1f30a7f1f ship: index-less brg/eng/sld parts never matched their GN frame — 34 ships assembled without a bridge
Tier 3 matched a part to its hardpoint by trailing index, so `e105_brg`
compared "01" == "" against GN_Bridge_01 and fell through silently. The runtime
capture is what exposed it: the game draws the bridge and places it at
[0, 70, -1850] rel e105_bdy_01, and assemble_ship emitted nothing there.

With no index to match on, take the lowest-numbered frame of the category.
Diffing assemble_ship part counts across every container: 34 (stage, ship)
entries gain parts — e102 +2 (bridge and engine), e104 +1, e105 +1, Stages
02-29. ship_audit is unchanged, so nothing regressed, and the capture now agrees
to dT 0.03 / dR 0.000.

Also fixes the diff itself: correlate_frames compared static against a rotation
sampled from the first block, which can belong to another INSTANCE of the class.
Scoped to the position-agreeing cluster, e105_eng_01 goes 1.711 -> 0.000 and
both e106 nacelles to 0.000. The one remaining rotation delta (e106_wep_02_01,
0.134) is a turret whose rotation varies by 0.182 between blocks that agree on
its position — the runtime disagrees with itself more than with the assembler.
The new rotVar column makes that distinction visible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:16:06 +00:00
MechaCat02
ea32e77e45 ship: engine placement CONFIRMED by 43-instance capture; exhaust markers added
capture_verify example: clusters every part's ship-relative transform across
the new multi-snapshot, multi-instance F10 captures (5 snapshots, 43 e106
instances, all angles). Verdict: the dominant clusters match the static
assembly to ~1 unit on EVERY part — including BOTH engine nacelles at
(+-131, -133, -131) — so the "engines inside the hull" appearance is the
game's own placement: the engine geometry sits recessed in the aft hull, and
the visible "thrusters" in-game are exhaust FX drawn at the GN_Jet/GN_SJet
frames (Z ~ -570, past the stern).

To close that perception gap the viewer now draws simple exhaust cones at the
game's own jet frames (ship::exhaust_frames; part of the external-parts
toggle). The jet frames flip Z, so the cone apex is authored at +Z and lands
trailing aft — verified in the offline render.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 19:44:12 +02:00
MechaCat02
31f2637e6c viewer,ship: external parts ON by default + Ships catalog = composite ships only
Fixes the two things the user saw in the Ships browser:
- "Can't find the thrusters": show_external defaulted OFF, hiding the engine
  rig / bridge / turrets entirely. Externals are EXACT since the node-TRS fix,
  so the toggle now defaults ON (relabelled; off = bare hull bodies).
- "Some parts very far away": families without a composite scene graph (fighter
  morph sets like f002 whose detached parts are authored far from the origin,
  shared turret/prop part families) fell into the stack-at-origin fallback and
  rendered as piles with stray pieces. ShipModel now carries has_composite and
  the catalog lists only real assembled ships.

Offline audit across ALL stages (ship_audit example): the only far-away
outliers were composite-less f002 morph parts (now filtered); every capital
ship assembles coherently (e108 places its 7 turret instances, f105 its
mirrored shield generators). All primary composites are single-key tracks —
multikey exists only in animation composites (e901 attacks, missile-open),
which assemble_ship does not select.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 19:15:34 +02:00
MechaCat02
737b61242e ship: STATIC assembly is now EXACT — node-TRS off-by-one cracked via the capture
The runtime capture served its true purpose: as the answer key that exposed the
static encoding. The XBG7 node joint table (node+0x44) holds NINE keyframe
channels [TX TY TZ RY RX RZ SX SY SZ] behind EIGHT pointer slots shifted one
track forward: channel k+1 = f64@(ptr[k]+8), and channel 0 -- TX, which no
pointer names -- sits at ptr[0]-0x48 (tracks are 0x50-byte records, value at
+8). The old 8-slot read took TY as X, RY as Y (usually 0 -- why both hulls
stacked on the centreline) and never saw TX at all (the +-264 hull offsets).
Euler order is Ry(ch3)*Rx(ch4)*Rz(ch5) with angles applied directly, pinned by
the captured engine nacelle Rx(-15)*Rz(30) decomposition. mesh.rs gains
read_trs9/node_rotation; scene_world_nodes and node_transforms both fixed
(saber fin overrides unaffected).

assemble_ship rewritten fully static and exact:
- every composite-node instance placed (alias duplicates collapsed, real
  instances kept) including cross-id turret mounts (rou_e303_wep_01_root x2)
- the e_rou_<id>_eng cluster rig mounts at GN_Engine_01 (two mirrored nacelles
  +-131 with -+30-degree cant + centre engine) -- world = frame o rig_local
- brg/sld at their exact GN frames (frames were always exact)
- shared-geometry twin pairs at +-TX: X-reflect the instance whose offset
  opposes the geometry's dominant side (viewer reverses winding on det<0)
- squeezed node names resolve (wep02 -> wep_02_01)

Ground-truth gate: ship::tests::static_assembly_matches_runtime_capture asserts
static == the baked e106 capture for all 8 parts (T<1.0, R<0.02) plus both
nacelles, both turrets, and the starboard-hull mirror. Viewer now uses pure
static assembly; ship_capture + the baked table remain as the verification
oracle. Renders show a coherent, bilaterally-symmetric destroyer.

81 formats-lib + all disc tests green; viewer builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 19:00:49 +02:00
MechaCat02
79c78cc7e1 ship: apply scale + gate approximate external parts behind a toggle
Two changes after user feedback that externals sit near-but-wrong (shield inset
into the hull, thruster floating close):

- Scale: the joint table's slots 5–7 are per-axis scale (a GN_ShieldG frame ships
  at 0.5, GN_Jet FX at 2.4–6.5) and were being ignored, rendering scaled parts at
  the wrong size. ScenePart now carries `s` and applies R·(S·v)+T.
- External placement is fundamentally approximate: a GN_* frame is the mount
  PIVOT (f105's two GN_ShieldG frames are both on the centreline), while the
  part's outboard/rotational offset lives in a detail sub-rig / runtime code this
  static pass can't recover. So assemble_ship gains `include_external`: the hull
  tier (rou_ nodes) is exact and always drawn; the external tier (bridge / shield
  / engine at GN_ frames) is opt-in via a "Show external parts" checkbox in the
  Ships browser, off by default so the clean, correct hull is the default view.

The module doc is corrected to the scene-graph mechanism (the old "draw parts
untransformed" claim was wrong).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 22:11:47 +02:00
MechaCat02
8c8e2dbeb4 ship: use only the primary composite — fixes parts floating mid-ship
The sub-composites (`e_rou_<id>_eng`, `_wep_*`) are LOCAL detail rigs: their
nodes are posed in their own frame, not ship-space, so folding them in placed
the engines at ~mid-ship (Z≈99) instead of aft — the "floating in air" parts.

The PRIMARY composite (`e_rou_<id>`, the one with the most nodes) already carries
the full ship-space layout: every hull body AND every `GN_*` hardpoint frame
(GN_Bridge at the stern, GN_Engine aft, GN_ShieldG amidships…). Assemble from
that one composite only; engines/bridge/shield now resolve to their real aft/mid
frames. Robust to the odd single-composite case (e108's `e_rou_e108_Missile_open`
is still picked as its own primary).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 21:52:20 +02:00
MechaCat02
a8b5691d1c ship: assemble capital ships from the composite scene graph (real placement)
The first cut drew every part at the origin, overlapping into a mess — the parts'
vertex buffers are authored around the origin, NOT pre-placed. The real world
placement lives in the composite scene-graph resource `e_rou_<id>`, exactly like
node_transforms poses the DeltaSaber's fins within one resource — but here the
nodes reference SEPARATE geometry resources.

Two tiers, matching how the game splits a ship (as the user described):
- Hull — `rou_<id>_*` scene nodes name the hull body resources and carry their
  world transform (bodies that together form the hull, not individually
  destructible). Composed parent∘child down the first-child/next-sibling tree.
- External hardpoints — `GN_*` frame nodes are the Vessel mounts (`GN_Bridge_01`,
  `GN_ShieldG_01`, `GN_Engine_01`, matching the IDXD recipe Frame names). Each
  bridge / shield-generator / engine part is placed at its frame by category+idx —
  the individually-destructible parts.

- mesh: `scene_world_nodes` — walk a composite, emit every `rou_`/`GN_` node with
  its composed world transform (same record layout + TRS convention as
  node_transforms, cross-resource). ScenePart{resource,m,t}+apply.
- ship: `assemble_ship(bytes, id)` — resolve hull nodes to the ship's own parts
  (shared cross-id turrets excluded — they need the per-mount recipe), match
  external parts to GN frames, fall back to a raw draw when a prop has no
  composite. is_base_part now also drops `_dNN`/`_lNN`/`_mNN` LOD copies.
- viewer: build_ship_model runs assemble_ship, decodes only referenced resources,
  bakes each part's world pose into its vertices (one posed copy per placement),
  then assembles via the shared prepare path. RequestShipRender carries the id.

Verified headless: parts now spread into coherent single-ship extents
(e108 185×174×683, f106 703×479×2079, f101 carrier 560×539×1712) instead of
piling at the origin. Turret placement (shared eNNN gun models on many GN_*Gun*
frames) still pending the per-mount recipe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 21:44:25 +02:00
MechaCat02
cacb576ecd formats: ship module — reconstruct capital ships from XBG7 part families
Project Sylpheed never stores a capital ship as a single mesh. Each ship is
modelled, then split into destructible parts (hull segments, bridge, engines,
integral turrets), each shipped as its own XBG7 resource inside a
hidden/resource3d/Stage_SNN.xpr container. The split lets the game hide/replace
a part when the player destroys it. Crucially each part keeps its ship-local
position, so reconstructing the whole ship is: draw every base part of one id
together, untransformed.

New `ship` module: Faction (from the e/f/n id prefix — ADAN/TCAF/Neutral),
ShipModel, ship_id_of, is_base_part (skips _l/_m/_d LODs, _bNN/_dead/_break
destruction geometry, e_rou_ scene nodes), and ships_in_container to group a
stage's resources into whole ships.

Supporting:
- mesh::xbg7_resource_names — list a container's XBG7 resource names (directory
  walk only, no geometry decode).
- Xbg7Model::models_named — decode only the named resources (assemble a ship
  from its ~8 parts instead of the whole ~300-resource stage).
- game_data::Vessel gains `model` (the rou_<id> hull stem) to link stats to the
  3D part family.

Tests: id/part classification + a disc-gated reconstruction of the ADAN e108
frigate from Stage_S02.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 21:16:04 +02:00