Skip to content

C#: M2 record family bases — collection counts read through the common base - #6

Merged
skarndev merged 5 commits into
mainfrom
feature/csharp-m2-record-bases
Aug 23, 2026
Merged

skarndev merged 5 commits into
mainfrom
feature/csharp-m2-record-bases

Conversation

@skarndev

Copy link
Copy Markdown
Owner

Two user reports, one PR. Rides skarndev/welder-csharp#6 (pinned at 3cdf95b) — merge that first.

Report 2 (the big one): root.Sequences.Count needed a six-arm era switch

The M2 record families were the one group still welding standalone, so their collection members never hoisted onto M2Root. They now have version-agnostic welded bases — the same architecture every other family already had:

  • ~24 bases: M2SequenceBase, M2CompBoneBase, M2CameraBase, M2ParticleBase, the material/scene/effects records, both skin records, M2EventTrackBase, plus a template M2TrackFamilyBase<T> alias-welded per value type for the nine M2Track families. All marked for the family surface; all joined the facade BASED set (ForVersion now works on every record family).
  • The multi-level family surface then hoists M2Root.Sequences/Bones/ParticleEmitters/Attachments/… as FamilyVector<RecordBase> — the user's switch collapses to:
new ModelCounts(root.Sequences.Count, root.ParticleEmitters.Count,
                root.Bones.Count,     root.Attachments.Count);

directly on the base-typed M2Root, with era-checked element access riding along.

Two upstream rod bugs this shape exposed, fixed in welder-csharp#6: base upcast thunks collapsed alias-welded specialization bases to their namespace (cpp_name/upath collect no identifiers from a specialization → invalid ^^ns splice + colliding symbols; now the deferred anchor + symtok), and C++-side, defaulted operator== on the records requires comparable bases (every new base defaults its own).

Report 1: "AsSpan() requires a scalar or enum element type" on a specific ADT

Not data-dependent: the consumer called scalar AsSpan() on chunk.Layers (Vector<SMLayer> — records, the right call is AsDataSpan()) and chunk.AlphaMaps (Vector<Vector<byte>> — nested, iterate). Their per-tile catch made the first tile reaching the layered path look special. welder-csharp#6 makes all three failure messages name the element type and the correct call.

Verified

FamilySurfaceTests locks the reported scenario (counts via the base, population via the concrete observed through the base view, M2Sequence.ForVersion). Full native rebuild (new upcast thunks); dotnet test: 36/36.

🤖 Generated with Claude Code

skarndev and others added 5 commits August 23, 2026 10:44
… M2Root

User report: reading root.Sequences.Count through the common base needed a
six-arm per-era switch — the M2 record families were the ONE group still
welding standalone (the old BASED distinction), so their collection
members never hoisted onto M2Root.

Structural fix, riding welder-csharp 3cdf95b (pin bumped):

- ~24 version-agnostic bases: M2SequenceBase / M2CompBoneBase /
  M2CameraBase / M2ParticleBase / M2RibbonBase / the material and scene
  records / both skin records / M2EventTrackBase (the versioned
  detail::M2TrackBase event family — mind the pre-existing name), plus
  template M2TrackFamilyBase<T> for the nine M2Track value families
  (alias-welded in gen_m2 via M2Track*Fam aliases in m2_ranges.hpp). All
  carry weld_as(family name) + the family-surface macro, and every base
  defaults operator== — the concretes' defaulted equality requires
  comparable bases (first build failure).
- All 24 joined the facade BASED set: ForVersion works on every record
  family, and the standalone factory partials became real base partials.
- The multi-level family surface then hoists M2Root.Sequences / Bones /
  ParticleEmitters / Attachments / ... as FamilyVector<RecordBase> — the
  user's switch collapses to root.Sequences.Count on the base, with
  era-checked element access riding along. Same for M2SkinProfile.Sections
  and every other record collection.

Upstream fixes this shape forced (welder-csharp PR#6): base upcast thunks
now survive alias-welded SPECIALIZATION bases (cpp_name/upath collapse to
the namespace for a specialization — deferred anchor + symtok instead),
and the AsSpan diagnostics name the right call per element kind (the
"specific ADT" report was Vector<SMLayer>/Vector<Vector<byte>> misuse of
the scalar AsSpan, unconditional but caught per tile).

FamilySurfaceTests locks the reported scenario: counts through the base,
population through the concrete observed via the base view, and
M2Sequence.ForVersion. Full native rebuild; dotnet test 36/36.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… main

Tree byte-identical to the 3cdf95b branch head this previously pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n track-base welds

Three pieces riding the record-bases branch:

1. THE CANONICAL TIMELINE SURFACE (C++, welded -> all three languages):
   timeline_count / key_count / timeline_timestamps / timeline_values on
   both M2Track layouts, and the values-less trio on both event-track
   layouts. Pre-WotLK slices the shared global timeline by the inclusive
   interpolation ranges (clamped; a global-sequence-driven or rangeless
   track exposes the whole array as timeline 0); WotLK+ indexes the
   per-sequence arrays (an external sequence's timeline is empty until its
   .anim loads); out-of-range timelines error (InvalidEntityState).
   Identical signatures per family -> the family surface hoists them onto
   M2TrackC3Vector etc., so the TBC->WotLK track redesign no longer forces
   a branch: C# reads through the base, typed Python writes one
   Protocol[V] the stubs satisfy structurally. Accessors COPY (correctness
   first); hot loops keep the two-arm concrete/span path. A native
   semantics test covers slicing, clamping and errors on both layouts;
   guide/m2.md walks the same loop trilingually.

2. IAnimTimeline (facade-declared, user-requested): the timing trio is
   signature-identical across every track family and the event tracks, so
   the facade partials declare ONE interface the synthesized dispatchers
   implement implicitly - timing-driven systems hold IAnimTimeline over
   all ten families, era-agnostic. Value access stays per-family typed.

3. CI FIX: the record-bases commit broke the Python module - the module
   walk registers the fifteen PLAIN record bases, but the nine
   M2TrackFamilyBase<T> INSTANTIATIONS are invisible to a walk, so the
   track concretes bound an unregistered base and nanobind aborted at
   import (the macOS bindings job's stubgen crash). welds_m2.cpp now welds
   the nine Fam aliases before the concretes; wowlib_py + wowlib_pyi build
   clean locally and the stubs carry the bases + timeline defs.

dotnet test: 37/37 (timeline dispatch through the base across both eras,
the IAnimTimeline loop over track+event families, out-of-range errors).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-Werror=sign-conversion

The timeline copy accessors added begin() + size_t (operator+ takes the
SIGNED difference_type); local builds carry no warning flags, so it only
surfaced in the CI bindings jobs. std::span::subspan takes size_t
natively - same windows, no arithmetic on iterators. All four track
layouts instantiate clean under -Werror=sign-conversion locally; the
semantic test still passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ord-bases

# Conflicts:
#	cmake/Dependencies.cmake
@skarndev
skarndev merged commit 1873a10 into main Aug 23, 2026
8 checks passed
@skarndev
skarndev deleted the feature/csharp-m2-record-bases branch August 23, 2026 18:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant