merge(vt): slice 1 Part A — VTank .usd/.utl drop-in and metaf .af for metas and routes (review-closed)
Campaign VT slice 1 Part A: the .usd document model + 137-setting serializer with declared type tags and exact compare, metaf .af reader/ writer for metas and nav routes with real byte identity against the owner's fixtures, .utl gate fixes, the VtankProfiles host storage (ACDREAM_VTANK_PROFILE_DIR), and the cutover of all four profile stores to real VTank files with one-time JSON migration. Two Opus lenses, three fix rounds, two narrow re-reviews, final re-check: MERGE-READY. Contract-doc ledger conflict resolved by keeping the campaign branch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
f680bf234a
108 changed files with 62970 additions and 1860 deletions
|
|
@ -538,6 +538,23 @@ and `PetDeviceCatalog.cs` — but was not compared step-by-step against
|
|||
`CombatController.cs`. Effect: minor DPS/time loss from unnecessary
|
||||
re-wields when several adjacent monsters need different weapons,
|
||||
lower impact than 1–3 above.
|
||||
6. **The real `.usd` `MyMonsters` table is preserved but never read or
|
||||
written — a persistence gap, not a rule-grammar gap.** Round 3 item 11:
|
||||
`CombatSettings.Rules` (the live `MonsterRule` list `MonsterRules.cs`/
|
||||
`MonsterExpression.cs` evaluate against, item 5's own faithful port)
|
||||
lives ONLY in MossTank's JSON side-car
|
||||
(`MossTankProfileStore.SideCarDocument.CombatRules`); the real 21-column
|
||||
`MyMonsters` table `VtankSettingsProfileSerializer` round-trips inside
|
||||
the `.usd` file (`VtankSettingsProfileSerializer.cs:24-30`: "every table
|
||||
other than Settings … is preserved byte-for-byte") is never parsed into
|
||||
`MonsterRule`s on load and never regenerated from them on save. Effect:
|
||||
a drop-in `.usd` from real VTank (or a hand-edited one) keeps its
|
||||
`MyMonsters` rows completely inert in acdream — MossTank always uses
|
||||
whatever the side-car separately holds instead, and a real VTank opening
|
||||
an acdream-saved `.usd` would see stale/absent `MyMonsters` rows
|
||||
regardless of what MossTank's own rule editor currently shows. Deferred
|
||||
to a future slice (slice 3) that ports the table; NOT implemented this
|
||||
round (see `docs/architecture/retail-divergence-register.md`).
|
||||
|
||||
`AutoAttackPower.cs` (melee power table) and `MonsterRules.cs` /
|
||||
`MonsterExpression.cs` (rule expression grammar) were both checked in
|
||||
|
|
|
|||
|
|
@ -514,14 +514,20 @@ tests (`tests/AcDream.Plugins.MossTank.Tests/NavigationTests.cs`,
|
|||
|
||||
**Can a real `.nav` file load today? Yes.** `VtankNavRouteSerializer.TryLoad`
|
||||
implements the exact header/route-type/waypoint-record grammar in §1,
|
||||
including the correct discard of the placeholder line, the correct
|
||||
per-type payload for all ten waypoint types, and — notably — correctly
|
||||
special-cases Portal2/UseNPC to overwrite the (meaningless) outer header
|
||||
coordinate with the embedded `d`-record's own coordinate (`VtankNavRouteSerializer.cs:151-157`).
|
||||
It is wired to actual import via `MossTankRouteProfileStore.TryImportLegacy`,
|
||||
which reads a `.nav` text file from plugin storage by filename. Manual
|
||||
verification against `bunny_stuck_jump.nav` and `deathnav.nav` (§1.4) round-trips
|
||||
cleanly against the documented grammar.
|
||||
including the correct discard of the placeholder line, and the correct
|
||||
per-type payload for all ten waypoint types. Portal2/UseNPC now keep both
|
||||
coordinate triples on `RouteWaypoint` — the (meaningless, per §1.2) outer
|
||||
header coordinate in `Position` and the embedded `d`-record's own real
|
||||
target coordinate in `ReferencePosition` (`VtankNavRouteSerializer.cs:127-145`;
|
||||
Campaign VT slice-1 Part A fix round — a prior port overwrote `Position`
|
||||
with the `d`-record instead of keeping both, which made a `.af` round trip
|
||||
of the same waypoint lossy). `Navigation.TickUse` searches for the live
|
||||
world object using `ReferencePosition`, matching retail's "real target
|
||||
coordinate" role for that field. It is wired to actual import via
|
||||
`MossTankRouteProfileStore.TryImportLegacy`, which reads a `.nav` text file
|
||||
from plugin storage by filename. Manual verification against
|
||||
`bunny_stuck_jump.nav` and `deathnav.nav` (§1.4) round-trips cleanly against
|
||||
the documented grammar.
|
||||
|
||||
Ranked by impact (highest first):
|
||||
|
||||
|
|
@ -535,6 +541,7 @@ Ranked by impact (highest first):
|
|||
| 6 | **Portal2/UseNPC candidate filter omits the ground truth's `item.c()==0` gate.** | `e9.g()`/`fa.g()` only consider candidates where `item.c() == 0` — i.e. the object is neither contained nor equipped (`fu.c()` returns its Container or Wielder instance id, `fu.cs:66-87`) in addition to name+class+proximity. | `TryFindObject` is opaque from this file (defined elsewhere in the plugin abstraction); could not confirm whether an equivalent filter exists. | **Low** — flagged for follow-up rather than asserted as missing. |
|
||||
| 7 | **Chat-color gate on UseNPC's "got a response" detection is dropped.** | `fa.a(ChatTextInterceptEventArgs)` only accepts color-3 "tells you" or color-0 "gives you" lines (`fa.cs:160-177`). | `HasNpcResponse` (`Navigation.cs:829-848`) matches on text content and sender name only, with no color/channel check, plus an extra `Sender.Equals(npcName)` branch not present in ground truth. | **Low** — small false-positive risk (any channel's text matching the phrase would complete the node), unlikely to matter in practice given the fairly specific phrase match. |
|
||||
| 8 | **Door frame-count debounce vs. time-based retry.** | Portal2's post-use verification waits `PluginCore.@do >= startFrame + 2` — at least two *rendered frames*, not a duration — before checking arrival (`e9.cs:126-139`). | acdream's equivalents are all elapsed-seconds based (`UseRetrySeconds`, etc., `Navigation.cs:181` and throughout). | **Low** — a frame-based debounce doesn't map cleanly onto acdream's tick model in the first place; noted for completeness, not actionable. |
|
||||
| 9 | **`.af` (metaf) cannot represent a strafe jump's direction at all.** | The binary `.nav` format's Jump record ends with one combined line encoding both charge-ms and a direction digit ∈ {3=Forward,4=StrafeLeft,5=StrafeRight} (§1.2 row 9, `di.cs:116-172`). | metaf's `NJump` class (`metaf_monolithic.py:11708-11821`, both `ImportFromMetAF`/`ExportToMetAF`) has no direction field whatsoever — only x/y/z, heading, holdShift, and delay-ms. `RouteWaypoint.JumpDirection` survives a `.nav`⇄model round trip exactly, but a route saved to `.af` and reloaded always comes back `Forward` regardless of what it held before the save, because the `.af` text itself never carried the value. | **Medium** — a real, unavoidable format limitation (not a porting gap): confirmed by reading metaf's own class end to end, not inferred. `MetafSerializer`'s `.af` writer does not claim otherwise and the reader does not force-assign `Forward` (it leaves the model's own default), but the value is still lost across a save-as-`.af`/reload cycle for StrafeLeft/StrafeRight waypoints. |
|
||||
|
||||
Correctly and precisely ported (confirmed, not a gap — listed since they
|
||||
were non-obvious and worth recording as verified rather than re-litigated):
|
||||
|
|
|
|||
|
|
@ -727,6 +727,7 @@ line counts). This is an unusually precise reverse-engineering result — the
|
|||
| 3 | **`MonsterNameCountWithinDistance`'s name pattern is case-insensitive in MossTank, case-sensitive in retail** | `d7.c()` compiles with `RegexOptions.Compiled` only (`d7.cs:26`) — case-sensitive | `Meta.cs`'s `MonsterCount` helper (`Meta.cs:536-552`) compiles with `RegexOptions.IgnoreCase \| RegexOptions.CultureInvariant` | **Medium-high.** A pattern authored against retail's case-sensitive matching (e.g. deliberately excluding a differently-cased variant name) will over-match in MossTank. Note `ChatMessage`/`ChatMessageCapture` do **not** have this divergence — MossTank's `ChatMatch` (`Meta.cs:463-513`) is correctly case-sensitive (`RegexOptions.CultureInvariant` only), matching `hl.cs`/`c5.cs`. |
|
||||
| 4 | **`;` sequence-operator value convention is inverted vs. retail (but matches UtilityBelt, and was chosen deliberately)** | Retail VTank: `a;b` evaluates to **`a`** (the left/first operand), discarding `b`'s value, while still executing `b` for side effects (`ExpressionEvaluator.cs:787-790`); `;` is an ordinary operator that can nest anywhere via normal precedence | `ExpressionProgram.Evaluate` (`ExpressionEngine.cs:20-27`) treats top-level `;`-separated statements as a `Node[]` program and returns the value of the **last** statement — this matches UtilityBelt's own audited grammar ("multiple `;`-separated statements, returning the final result", `2026-08-26-mosstank-vtank-utilitybelt-research.md:237`), which the campaign explicitly chose as MossTank's baseline dialect (§3.1 there) | **Medium, and by design, not an oversight.** Retail VTank and UtilityBelt already disagree with each other on `;`'s return value; MossTank correctly implements UtilityBelt's convention. The compat risk is narrower than a plain bug: only a `.met` authored *against retail's own* semantics (chaining `sideeffect[]; realcheck[]` and relying on the *first* value) evaluates to the opposite result once imported. |
|
||||
| 5 | **Watchdog ring pre-fill differs (far sentinel vs. arm-time position)** | All 10 position-history slots start as a sentinel far from any real coordinate (`(1000,1000,1000)`, `h7.cs:45-49`), guaranteeing expiry cannot even be *reachable* until one full `timeSpan` window has elapsed and every slot has been overwritten with a real sample | `SetWatchdog` (`Meta.cs:585-596`) pre-fills all 10 slots with the **current** position at arm time | **Lower-medium.** Both converge on "earliest possible expiry is ~one `TimeSpanSeconds` after arming" in the common case, but they diverge for a watchdog that is armed, then the player leaves and returns to very near the arm point before the window closes — retail's sentinel-seeded ring cannot spuriously read the arm-time position as one of its 10 samples, MossTank's can. |
|
||||
| 6 | **`MetaRule.Enabled` (a per-rule disable-without-delete toggle) has no metaf-compatible representation at all** | Real VTank/metaf has no such concept — `metaf_monolithic.py` has zero occurrences of "enabled"/"disabled" anywhere; a rule in a `.met`/`.af` is simply present or absent | `MetafSerializer.SaveMeta` (Campaign VT slice-1 item H) **refuses to save** a profile containing any disabled rule by default (throws `InvalidOperationException` naming the count) rather than silently dropping it — the caller must opt in via `SaveMeta(profile, dropDisabledRules: true)` to accept the loss explicitly. Round 2 (Campaign VT slice 1 Part A) made `.af` the SOLE authoritative Meta store — `MossTankMetaProfileStore` has no separate JSON storage or `.af` "convenience mirror" any more, so this refusal now blocks the save of the profile itself: a disabled rule makes that profile file genuinely **unsaveable** until the user re-enables or deletes the rule (the file on disk keeps its last successfully-saved content in the meantime; round 3 item 12 corrected this row, which previously described the pre-cutover "legacy-export mirror" design). | **Low** (by design, not a bug): this is a MossTank-only UI extension with no VTank equivalent to diverge from, and the refusal is loud (a user-visible notice) rather than a silent data loss. |
|
||||
|
||||
### 5.3 Confirmed non-gaps (the format/engine is otherwise unusually faithful)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue