Repository navigation
HD2Runtime v0.28.1 — SDK 0.27 Compatibility and Validation Hardening
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_effectthat 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 dormantattack.primary.projectileand the SH-20 Ballistic
Shield's default-zone armor. - The list is exact.
schemas/legacy_acknowledgements.jsonrecords 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 isdomains/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 Luadebuglibrary. 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 requiresmin_version0.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: withallow_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>'andlocal 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_compatibilityand
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_version0.25.0 declares SDK 0.25.0, so it gets the
legacy path for these fields. Raisemin_versionto get the current rule. - Custom projectiles. Runtime-owned custom projectile rows remain later work.