fix(vt): H test-restoration + disabled-rule refusal + double-side gate
Item H (slice-1 fix round), six sub-parts: 1. Restored VtankMetaProfileSerializerTests.LoadsKnownTypedCondActRecord and SignedHighBitLandblockIdRoundTripsExactly (the latter adapted from the deleted RoundTripPreservesEveryVtankConditionActionAndEmbeddedNav's LandblockEquals/LandcellEquals coverage of unchecked((int)0x8B370000u)), both deleted as collateral damage of an unrelated file move in commit0d10399e0. Neither calls the deleted VtankMetaProfileSerializer.Save writer (demoted to import-only in3ff9461ef) — both are pure reader assertions. 2. MetafSerializer.SaveMeta now refuses (throws InvalidOperationException naming the count) to silently drop a disabled MetaRule: real VTank/ metaf has zero concept of "disabled" (confirmed: metaf_monolithic.py has no "enabled"/"disabled" occurrences anywhere), so MetaRule.Enabled is a MossTank-only extension with no metaf-compatible marker. A new SaveMeta(profile, dropDisabledRules: true) overload lets a caller accept the loss explicitly. MossTankMetaProfileStore.WriteLegacyExport (the .af convenience mirror beside MossTank's own fully-fidelity JSON storage) deliberately does NOT opt in — it leaves that mirror stale and logs a warning via its existing try/catch rather than losing the rule. Recorded as gap 6 in docs/research/vtank-kb/07-meta-and-expressions.md section 5. 3. New VtankProfilesDefault (src/AcDream.App/Plugins/): the graphical host's default VtankProfiles root (<DataDirectory>/vtank), extracted out of Program.cs's inline Path.Combine call into its own pure, injectable-root function specifically so the "Path.Combine only, never a hard-coded Windows path" guarantee is a real, failable Linux-path unit test (VtankProfilesDefaultTests.ResolveIsBuiltWithPathCombineOnly) rather than something only checkable by reading the source — the pattern item F's VtankProfileDirectory rewrite removed when Resolve/PortableDefault moved out of the plugin. 4. New BuffedDoubleRequirementDoesNotApplyBonusWhenBaseKeyIsAbsent: the KeyExistsDouble gate (ComputedItemInfo.cs:234) already existed in BuffedDouble, but only the int side (BuffedIntRequirementDoesNotApplyBonusWhenBaseKeyIsAbsent) had a pinning test. 5/6. DoubleSpellBonuses gained an explicit Change field (KB doc 05 section 2.2: "additive unless the static table's Change==1, in which case multiplicative", ComputedItemInfo.cs:244), replacing BuffedDouble's prior `(int)bonus.Bonus == 1` magnitude-based proxy — that proxy only worked because every multiplicative bonus in the current 19 rows happens to fall in [1.0, 2.0) and every additive one happens to be under 1.0; it would have silently mis-branched on a future row like an additive 1.5 or a multiplicative 2.0+. Every existing row's Change value was derived mechanically from its old proxy result (no behavior change for the current table), and BuffedDoubleRequirementAppliesAdditiveBonusWhenBaseKeyExists/ AppliesMultiplicativeBonusWhenChangeIsSet pin both branches through the real named field. Full MossTank suite: 574 -> 581. App.Tests (Plugin|LaunchOptions|RuntimeOptions filter): 135 -> 137. Core.Tests (Plugin filter): 50/50 (no change, no Core-side edits this item). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
e53507396f
commit
a3b4a8e2e5
10 changed files with 358 additions and 16 deletions
|
|
@ -723,6 +723,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) now **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. `MossTankMetaProfileStore.WriteLegacyExport` (the `.af` convenience mirror written alongside MossTank's own fully-fidelity JSON storage) deliberately does NOT opt in: it leaves that mirror stale rather than losing the disabled rule, and logs a warning via the existing try/catch. | **Low** (by design, not a bug): this is a MossTank-only UI extension with no VTank equivalent to diverge from; the representational loss is confined to the legacy-export mirror, never the authoritative profile data, and is now impossible to hit silently. |
|
||||
|
||||
### 5.3 Confirmed non-gaps (the format/engine is otherwise unusually faithful)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue