Skip to content

HD2Runtime v0.28.1 — SDK 0.27 Compatibility and Validation Hardening

Choose a tag to compare

@SkyeShade SkyeShade released this 30 Sep 23:18
· 275 commits to master since this release

HD2Runtime 0.28.1 — SDK 0.27 Compatibility and Validation Hardening

0.28.1 is a compatibility and stability release. It adds no gameplay mappings, and every 0.28.0 field, guard,
baseline and live proof is unchanged. It does three things:

  • Backwards compatibility for SDK 0.27-era mods. Operations that 0.28.0 refused only because they lack an
    allow_unverified_effect that 0.28.0 introduced now apply again, for mods that declare an older SDK.
  • Diagnostics for legacy operations. Each such operation is logged with what to rebind, and
    hd2.diagnostics.operations() lists every registered operation, refused ones included.
  • The validator detects refused and skipped writes. A packaged scenario now fails when an operation it expects
    to apply is refused or never applies, even when the resulting state looks right.

The custom projectile work is not part of this release.

Backwards compatibility for SDK 0.27-era mods

What broke. 0.28.0 added allow_unverified_effect to 146 fields that 0.27.0 let mods write without it:

Target Fields Why 0.28.0 asks for the acknowledgement
PLAS-101 Purifier 28 Only the charge levels that name this row fire it; the others fire projectile 129.
PLAS-15 Loyalist 28 Every charge level fires another projectile; the weapon never fires this row.
P-34 Breacher 28 The weapon fires a spawned entity, so the row is not established as what it fires.
P-33 Missile Pistol 28 The same: a spawned entity.
P-92 Warrant 16 The same: a spawned entity.
LAS-17 Double-Edge Sickle 16 Every heat level fires another projectile; its projectile fields write the LAS-16 Sickle's row.
SH-51 Directional Shield (backpack) 2 entity.health and entity.armor are the backpack body's, not the energy barrier's.

The 144 weapon fields are those six weapons' projectile, damage, explosion and terminal-explosion fields. The rule
came from researching the Purifier's charge levels (the AyakaMods report). Mods exported for SDK 0.27.0 carry no
such acknowledgement, so 0.28.0 refused those operations. Each refusal was logged, and the mod's other operations
kept applying.

This is not a ModBuilder defect. A project keeps its SDK binding until it is rebound, and ModBuilder 1.4.x already
emits the acknowledgement for projects bound to SDK 0.28.0.

The rule in 0.28.1. Runtime decides by the SDK version each mod declares:

Mod declares Field Without allow_unverified_effect
SDK 0.27.x or older gained the acknowledgement in 0.28.0 (the 146 above) applied, logged as a legacy operation
SDK 0.27.x or older already needed it in 0.27.0, or is new in 0.28 refused, as before
SDK 0.28.0 or later any field that needs it refused, as in 0.28.0
unknown any field that needs it refused, as in 0.28.0
  • Nothing else is relaxed. allow_shared, allow_unverified_reference, live proof, reviewed ranges, expected
    baselines and the guarded write are unchanged for every mod.
  • Read-only stays read-only. Fields that 0.28.0 made read-only stay read-only, because those writes do not
    change the game. Examples are the AR-23 Liberator's dormant attack.primary.projectile and the SH-20 Ballistic
    Shield's default-zone armor.
  • The list is exact. schemas/legacy_acknowledgements.json records the 146 fields. They were derived by comparing
    the SDK catalogs published at v0.27.0 with 0.28.0 (scripts/generate_legacy_acknowledgements.py). A check fails the
    build if an entry stops naming a field. The runtime lookup is domains/legacy_acknowledgements.lua.
  • Versions. Only MAJOR.MINOR.PATCH is compared. An SDK 0.28.0 prerelease already carried the acknowledgements.

How the SDK version is determined. Mods do not pass their SDK version to Runtime. Every published addon wrapper
keeps it in a local of its main chunk while it runs the mod's startup. ModBuilder (every version) and the ModTemplate
use local x,y,z=version('<sdk>'), where ModBuilder writes the project's bound SDK version. The SDK's hd2.py
wrapper uses local minimum='<min_version>'. When a mod registers an operation (hd2.patch, hd2.transaction,
hd2.plan or hd2.ensure), Runtime reads those locals from the call stack with debug.getlocal. It reads values
only, and it recognises only these wrapper shapes. The declaration is remembered per mod resource, so the same mod's
later registrations from callbacks use it too. A re-validation after an option change keeps the declaration of the
mod that registered the operation. When no declaration can be read, the mod is unknown and the 0.28.0 rule applies.

Not proven here.

  • Effect. The legacy path restores what 0.27.0 wrote; it does not prove the effect. A Purifier row edit still
    changes only the charge levels that fire that row.
  • The game's Lua environment. Reading the declaration was tested on the game's own lua51.dll, but not yet in
    game. It needs the Lua debug library. If the game's Lua state lacked it, every mod would be unknown, and 0.28.1
    would behave as 0.28.0. The refusal says so (see below), so one in-game test with an SDK 0.27 export confirms it.

Improved diagnostics

  • Legacy operations are logged once per operation and field, when the operation registers:

    [HD2Runtime] ensure gui-object-64f6c65514d7e06d97274943: legacy SDK 0.27.0 operation from
    mods/nephelym/nephelym_s_weaponry_rebalance: PLAS-101 Purifier projectile.drag needs allow_unverified_effect=true
    since SDK 0.28.0 and is applied without it, as SDK 0.27.0 allowed. Rebind the project to SDK 0.28.0 or later and
    re-export it, or add allow_unverified_effect=true to the operation. Why it is needed: ...
    
  • Refusals of these fields now say which rule applied, for example [required since SDK 0.28.0; mods/x declares SDK 0.28.0] or [required since SDK 0.28.0; Runtime could not read the SDK version of mods/x, so the 0.28.0 rule applies].

  • hd2.diagnostics.operations() (new) returns every operation registered this session, refused ones included,
    in registration order: {kind, id, mod, sdk, sdk_source, status, result, code, error, runs, legacy}. An addon
    that keeps no handle is covered too (docs/diagnostics.md). The Lua stubs and SDK docs document it, and the
    example validator requires min_version 0.28.1 for a mod that calls it.

The validator detects unexpectedly refused or skipped mutations

Before 0.28.1 the packaged validator (scripts/validate_packaged_runtime.py) checked only the handles an addon
returned, and the resulting state. Two kinds of failure could pass:

  • a refused operation whose handle the addon did not return;
  • an operation declared as an expected refusal. The user-report scenario declared the Purifier edit that way, which
    hid the compatibility break.

Now every scenario reads every registered operation from hd2.diagnostics.operations() and classifies each one:

  • applied;
  • not applicable: an option-bound operation that its option keeps inactive. It must be declared, or be applied
    once the scenario turns its option on;
  • refused: allowed only for a declared negative control, with its reason;
  • skipped: never applied.

A refused or skipped operation that is part of the expected mutation fails the scenario. An operation that applied
only through the legacy path must be declared with its fields and must have its log line. A mutation scenario must
write, and a read-only scenario must not. Before, that write check ran only in the unit suite. The report lists the
outcome counts of each scenario.

What the stricter check found. Nothing was silently broken. It surfaced two groups of operations that no scenario
had examined before, both intended:

  • the Liberator dormant-member refusal in projectile-active-sources, the live FAIL control, now declared;
  • default-off toggle operations in the equipment and projectile-builder test mods. They are inactive at startup and
    apply when the scenario turns them on.

Regression cases:

  • legacy-sdk-027-purifier: the user report's Purifier edit in a ModBuilder SDK 0.27.0 wrapper. The write applies
    as a legacy operation. The control that already needed the acknowledgement in 0.27.0 is still refused.
  • sdk-028-purifier-without-ack: the same edit in an SDK 0.28.0 wrapper is refused.
  • sdk-028-purifier-with-ack: with allow_unverified_effect=true, it applies, with no legacy note.
  • user-report-full-project: the exact 133-operation AyakaMods export now applies all 133 operations, 408 writes.
    The Purifier edit applies as a legacy operation; 0.28.0 refused it.

For ModBuilder and other generators

  • No generator change is needed. Projects bound to SDK 0.28.0 already emit the acknowledgements. Rebinding an
    older project to SDK 0.28.0 and exporting it again adds them, and the legacy log line goes away.
  • Keep the wrapper's version locals. Runtime reads the declaration from local x,y,z=version('<sdk>') next to
    local key='HD2RuntimeMod:<resource>' and local function start(). A wrapper that renames them is read as
    unknown and gets the current rule. That is safe, but its SDK 0.27 projects would lose the legacy path.
  • Validating exports. hd2.diagnostics.operations() can check an export after it registers: every operation's
    status, and which ones applied only as legacy operations.

Validation

All of it ran from the release-candidate artifacts built at 449e7fd.

  • Unit suite: 686 tests. They include 13 legacy-compatibility tests on the game's lua51.dll, 13
    validator-outcome tests, and an end-to-end packaged test in which a refused write fails its scenario.
  • Generator freshness: every domain table, catalog, stub and SDK document checks as current, including the
    new legacy acknowledgement table.
  • Snapshot validation: all 19 production snapshot validators pass against the retained snapshot.
  • Examples: 103 of 103 validate against this SDK, including the snapshot baselines.
  • Packaged runtime: 134 scenarios run from the built runtime ZIP, with late module lookups disabled:
    • 345 applied operations, 2 of them legacy;
    • 7 declared refusals and 1 declared inactive operation;
    • none refused or skipped unexpectedly.
  • Package contents. The runtime archive adds exactly two modules to 0.28.0's: core/sdk_compatibility and
    domains/legacy_acknowledgements. No test or scratch files are included, telemetry is off and the fixture
    fallback is disabled.
  • Game-update migration, with 0 unsafe stale writes in every run and 27/27 checks in each re-validation:
    • same build: 100% recovered;
    • previous build D8E23968D141: 78.64% recovered automatically, 21.36% downgraded, 15.8% for manual review;
    • fresh reports: 19,902 exact on the same build, 89.87% recovered on the previous build.

The asset checksums are published with the release (SHA256SUMS.txt).

Known limitations

  • In-game declaration reading is not live-tested yet (see above). If it fails, 0.28.1 behaves exactly as 0.28.0.
  • ModTemplate floors. A ModTemplate mod that keeps min_version 0.25.0 declares SDK 0.25.0, so it gets the
    legacy path for these fields. Raise min_version to get the current rule.
  • Custom projectiles. Runtime-owned custom projectile rows remain later work.