Repository navigation
HD2Runtime v0.28.0 — Gameplay Scripting and One Projectile System
HD2Runtime 0.28.0 — Gameplay Scripting and One Projectile System
0.28.0 is the largest release since the typed API. It adds two things, and many narrower ones:
- Event-driven gameplay scripting. Mods can react to missions, players, entities, combat and the weapon in
hand, and ask the game to heal, explode, fire a projectile or apply a status. - One projectile system. Primary, secondary, support and mounted weapons share one projectile host rule and one
donor pool, with a projectile builder for a projectile's direct hit and explosions.
It also covers much more of the game:
- rate-of-fire modes, weapon feeds and armory presentation;
- Resupply;
- underbarrel weapons;
- Guard Dogs, shields and packs;
- sentries and minefields;
- enemies and structures.
Two runtime diagnostics help separate Runtime from other mods in large setups. Correctness fixes include the SG-20
Halt bug that dropped other edits.
Live evidence stays exact. A capability is live-proven only for the targets and values a user test showed working
in game. Everything else keeps its acknowledgement: allow_unverified_effect, allow_unverified_reference,
allow_shared. See Live evidence and docs/live-evidence.md.
Gameplay scripting (events and actions)
hd2.events, hd2.mod(id), timers (hd2.after, hd2.every) and key events let a mod run Lua when the game
reports something. Each event comes from a named native source, and the catalog says which fields it carries. See
docs/event-scripting.md (guide), docs/events.md (reference) and sdk/EventCatalog.json.
-
19 events:
mission_started,mission_ended;player_spawned,player_died,player_damaged,player_healed;entity_spawned,entity_died,entity_killed,entity_damaged;player_fired,player_hit,player_damage_dealt,player_kill_credited;weapon_equipped,weapon_unequipped,weapon_changed;key_down,key_up.
entity_damage_pre(changing damage before it applies) is published as blocked, with its reason. -
Kill and damage attribution from the game's own stats.
player_kill_credited,player_hitand
player_damage_dealtare attributed per weapon source.player_damage_dealtnames the exact source that dealt
the damage; this is live-proven for the K-2 Throwing Knife. -
Weapon in hand. The three weapon events follow the local player's primary and secondary switches, and report
uncatalogued held items as uncatalogued. -
Actions (
hd2.actions):- heal:
hd2.actions.heal(amount)/player:heal(amount)heals the local player through the game's own
heal, clamped to maximum health; - explosions:
hd2.explosions.spawn(name, {position=...})spawns the Hellbombs and the catalogued weapon
explosions; - projectiles:
hd2.projectiles.spawn(weapon, {position=..., direction=...})fires catalogued weapon
projectiles; - statuses:
hd2.status.apply(entity, status, {buildup=...})applies statuses that player weapons apply.
Explosions, projectiles and statuses are host-only, need a mission, and are rate-limited per mod. Generic entity
spawning is published as blocked: its parameters and network replication are not proven. - heal:
-
Ownership and isolation. Every handler, timer and action is attributed to its mod (
hd2.mod(id)). One mod's
error is contained and logged; the other mods keep running. -
Example.
VampiricThrowingKnivesTestheals the local player by 25 whenever a K-2 Throwing Knife deals damage.
KillHealTest,KillStackDamageTest,DeathHellbombTest,ProjectileActionTest,StatusActionTestand
EquippedWeaponEventTestshow the other patterns.
Multiplayer. Events read the local game. The actions were live-tested hosting only. A client's explosion,
projectile or status request is refused (HOST_ONLY), because the host owns enemy health. An action's damage and
deaths are host state that the game synchronizes itself, but what other players see was not tested. No network send
was found for spawned projectiles.
One projectile system
Hosts and sources
A weapon's projectile reference is writable only where the game is shown to read it when firing. Each such
reference is the weapon's active source. attack:projectile_source() reports each attack's source status:
ACTIVE_DIRECT: fired from its own ProjectileWeapon member;INDIRECT: fired through its ammunition delta;AMBIGUOUSorBLOCKED: read-only, with the reason and, where one exists, the right target.
This came from a live failure. The AR-23 Liberator's base projectile member was writable but never fired: its
default ammunition patches the member at weapon build. That member is now read-only
(DORMANT_PROJECTILE_REFERENCE), and weapon:ammunition():projectile() writes the ammunition row it actually fires.
-
One host rule. Primary, secondary, support and mounted weapons follow the same structural rule, and loadout
category never decides compatibility. Only the native output family and the active source do. There are 52 hosts
(sdk/AttackOutputCapabilities.json):- 46 component hosts: 29 player, 8 support and 9 mounted weapons;
- 6 ammunition hosts.
-
One donor pool. Any of the 80 selectable projectile outputs is a donor for any host, whatever loadout slot,
stratagem or mount owns it (hd2.attack_output(name)). Another weapon's attack projectile handle works too. -
Support-weapon projectile swaps.
hd2.support_weapon(name):projectile_source()covers eight hosts: APW-1,
EAT-17, EAT-411, EAT-700, GL-21, M-105 Stalwart, MG-206 and S-11 Speargun. -
Mounted projectile hosts.
hd2.vehicle(name):weapon(mount):projectile_source()covers nine hosts:- the EXO-45 Patriot minigun;
- both EXO-49 Emancipator autocannons;
- the EXO-51 Lumberer cannon;
- the M-102 Gunner FRV gun, the Super Earth FRV gun and the M-103 Supply FRV gun;
- the GATER turret;
- the AX/AR-23 Guard Dog gun.
Every write re-proves the mount chain. The two FRV guns are one weapon entity, so a write there needs
allow_shared. -
Families. Beam, arc, spray and melee outputs are never forced into projectile APIs. They fail with
INCOMPATIBLE_OUTPUT_FAMILYand their structural reason. -
Assets. A donor's package loads first (the 0.27 asset loader). A donor whose package is unknown is refused
(ASSET_UNAVAILABLE).
Projectile builder
A projectile row has three references that can be re-pointed to another catalogued output's: its direct hit
(hd2.fields.projectile.direct_damage), its impact explosion and its expiry explosion
(projectile.impact_explosion, projectile.expiry_explosion).
- Guards on every write:
- the donor row is re-proven live and only read;
- its package loads first;
- a composition that would make a projectile spawn itself is refused (
RECURSIVE_COMPOSITION).
- Sharing. A slot write changes every entity that fires the row. Shared rows need
allow_shared; the catalog
lists their consumers. - Programmable ammo.
weapon:programmable_ammo()adds a ProgrammableAmmo mode to a free weapon-function input,
andhd2.fields.function_ammo.projectilechooses the donor it fires. Two live examples:- Speargun GAS/STUN: the Speargun keeps its GAS spear and gains a STUN mode that fires its spare twin with an
EMS Mortar field on expiry; - HMG special ammo: the MG-206 gains an alternate mode that fires R-4 Hyena, AR-32 Pacifier or P-35
Re-Educator bullets.
- Speargun GAS/STUN: the Speargun keeps its GAS spear and gains a STUN mode that fires its spare twin with an
- Mode labels and icons. Mode labels and icons are written on the output
(hd2.fields.presentation.mode_label,mode_icon). Only native strings and icons are allowed. A label without a native icon falls
back to the plain round. The stratagem-owned A/M-23 EMS Mortar shell is a donor for function projectiles.
Host swaps and row writes are separate operations. A host swap changes which row a host fires. A slot write
changes a row, for everything that fires it. Slot writes never follow a swap. A host swapped to a donor fires the
donor's row with the donor's effects, and its own row keeps any edit. Runtime logs a note: when a slot or label
write applies while the row's owner fires another row. To give a swapped projectile an effect, write the donor's
row explicitly. That changes the donor weapon too.
Native rows only. Every composition in 0.28.0 re-points references between the game's own ProjectileSettings
rows. The one distinct projectile identity, the Speargun spare twin, is an interim borrowed vanilla row that no
other entity references. It is offered only on the build it was researched on (SPARE_TWIN_UNVERIFIED_BUILD
elsewhere). A composed spare twin is a local edit: other players without the mod see the row's vanilla contents.
Fully Runtime-owned custom projectile rows, with their own ids above the vanilla range, are planned for later work
and are not part of 0.28.0.
Weapon composition
- Rate-of-fire modes (
sdk/WeaponFireRateCapabilities.json,docs/fire-rate-modes.md). Each weapon's three
native rate slots are published in weapon-menu order{X, Y, Z}(hd2.fields.fire_rate.modes). 60 of 115
weapons are writable. Where a weapon has no native selector, the slots can be filled and a free input bound
(hd2.fields.weapon_function.left/right). - Weapon feeds (
sdk/WeaponFeedCapabilities.json,docs/weapon-feeds.md):- rounds-fed alternate magazines, such as the SG-20 Halt's two feeds and their capacities;
- the ProgrammableAmmo projectile a weapon can host.
- Armory presentation (
sdk/WeaponPresentationCapabilities.json,docs/weapon-presentation.md). The armory's
armor-penetration label (100 weapons) and trait list (103) are writable, separately from gameplay damage. - Output composition (
sdk/OutputCompositionCapabilities.json). Why a projectile weapon cannot fire a beam,
blocker by blocker.
Coverage
-
Resupply (
hd2.stratagem('Resupply'),docs/stratagem-authoring.md). Its identity, cooldown and delivery
are mapped, and the pod rack is exposed asresupply:delivery():rack(). -
Underbarrels and subweapons (
docs/player-weapon-composition.md). The AR/GL-21 One-Two, AR-11 Arbitrator and
SMG/FLAM-34 Stoker underbarrels are separate weapon entities. They are nested sub-targets
(hd2.weapon(name):underbarrel(), catalogsubweapons), never flattened into the rifle. The One-Two launcher
publishes its grenade spread, fire rate, rounds, spare, starting and resupply grenades.The GP-31 Grenade Pistol and P-72 Crisper were misattributed duplicates of those launchers. They stay blocked,
with the exact reason. -
Equipment:
- the SH-32 Shield Generator Pack and SH-51 Directional Shield: recharge and barrier;
- the Warp and Hover packs;
- the Guard Dog drones and their weapons, reached through the backpack
(hd2.backpack(name):drone():weapon()); - the LAS-17 Double-Edge Sickle's heat levels;
- the LAS-98 Laser Cannon's beam fire rate;
- the M-1000 Maxigun's recoil multipliers;
- backpack ammunition, validated inside the game's 1023 limit.
-
Weapon movement. Movement restrictions of 107 weapons are mapped (105 writable, such as the Maxigun's
stationary firing). The B/FLAM-80 Cremator, EAT-17 and LAS-98 are resolved as support targets. -
Stratagems:
- sentry turret motion, targeting range and deployed lifetime;
- minefield salvos and mines per salvo (reduce-only);
- mine explosions.
-
Statuses (
sdk/StatusEffectCatalog.json,docs/status-effects.md). The catalog has 71 statuses, 11 of them
attachable. Typed status references are writable on the direct hits of player, support and mounted weapon
projectiles. -
Projectile physics. Player projectile penetration slowdown and lifetime are published generically for every
player projectile host. This is what ModBuilder needed for the MA5C. -
Enemies and structures (
hd2.enemy,hd2.structure,sdk/EnemyAuthoringCapabilities.json,
docs/enemy-authoring.md). 177 classes are covered, 138 enemies and 39 structures, with health and damage zones.
The mounted-weapon attack damage of 38 classes is mapped too (169 attacks).
Correctness and hardening
- SG-20 Halt fix (ModBuilder issue #2). Generic projectile fields on a rounds-feed attack now resolve to that
feed's branch. Published 0.27.0 refused them, and that refusal dropped every later operation of the mod. - Operation isolation.
hd2.patch,transaction,planandensurelog<kind> <id> rejected: <reason>and
return a rejected handle instead of raising. One invalid operation no longer aborts the rest of a mod. - Guarded read budget. The engine's reread allowance is now derived from each plan, under a 64 MiB hard ceiling.
Published 0.27.0 used a fixed 16 MiB cap. A 0.27-generated MG-206 plan with 19 fields ran out of it mid-commit,
after 8 writes, and the exhausted budget also starved its rollback. Contexts captured by several operations are now
checked once, and back-to-back rereads with nothing written between them are gone. A plan above the ceiling is
refused before any page opens. That MG-206 plan now applies with 10.9 MB of guarded reads
(tests/fixtures/user-reports/hmg-read-budget). - Conflict messages name the target, the expected, desired and observed values, and the field's sharing scope.
The conflict rule is unchanged. - Duplicate operation ids in one mod get one warning.
- Newer-runtime warning. A mod that needs a newer HD2Runtime is logged, and the player sees one message box per
session on the ship. - Refusal reasons. A guarded write that refuses its memory region says why; the guard itself is unchanged.
- Mod Options. Choices accept semantic names and list-element values. Typed values log readably.
- Game-update migration. The profile and migration tooling now carry
LoadoutEntryComponentData, event-source
proofs, the projectile builder, spare-twin build scoping and the mounted hosts.
Diagnostics (hd2.diagnostics, docs/diagnostics.md)
- Write conflicts (always on, cheap). An
hd2.ensurewhose owned bytes another writer changes 3 times within its
window logs one warning. The warning names the operation, target and fields.hd2.diagnostics.write_conflicts()
lists them. The counting runs only on a detected drift. - Telemetry (off by default).
hd2.diagnostics.telemetry({enabled=true})logs avg/p95/p99/max for the update,
byte checks and event polling, plus active ensures and re-applications. Disabled, it costs one boolean test.
Live evidence
In-game tests this cycle proved these exact scopes (docs/live-evidence.md, sdk/LiveEvidenceCatalog.json): 31
families, 54 recorded tests, 46 of them passed. Only the listed targets and values drop their acknowledgement.
allow_shared is never dropped by live proof.
| Area | Live-proven exactly |
|---|---|
| Events and actions | player_died with its death position; the weapon in hand; K-2 knife damage attribution; hd2.actions.heal(25); the NUX-223 Hellbomb explosion, R-36 Eruptor projectile and fire status actions (hosting) |
| Projectile hosts | SMG-32 Reprimand host path; AR-23 Liberator ammunition row; Liberator ← EAT-700 / GL-52; Reprimand ← EAT-700; EAT-17 ← PLAS-1 Scorcher; M-105 Stalwart ← APW-1; EXO-45 Patriot minigun ← EAT-17, LAS-58 Talon, PLAS-1 Scorcher |
| Projectile slots | AR-2 Coyote impact explosion ← GL-21 grenade, EMS Mortar field, Speargun gas, EAT-700 napalm (and none); Coyote direct hit ← AR-32 Pacifier stun; the Speargun spare twin's EMS expiry; the Patriot's own bullet ← the same four impact explosions |
| Donor-row composition | Patriot minigun swapped to the LAS-58 Talon, plus the GL-21 grenade blast written on the Talon row: the minigun's Talon bolts exploded |
| Programmable ammo | Speargun STUN (spare twin); MG-206 ← R-4 Hyena, AR-32 Pacifier, P-35 Re-Educator; their labels and icons |
| Weapons | MG-206 native rate slots; the AR-23 Liberator's added rate selector; SG-20 Halt feed capacities; AR-23C Concussive penetration label; direct-hit status slots (Liberator fire, Maxigun stun); Halt and Maxigun projectile damage; MA5C magazine; LAS-16 Sickle heat per shot |
| Stratagems | sentry turn speed and targeting range; minefield salvos; Orbital Precision Strike and Resupply cooldown; Resupply pod ← Grenade Box |
| Enemies | main health and zone armor of enemy classes |
Not proven, and kept behind acknowledgements or withdrawn:
- Partial: the M-1000 Maxigun backpack ammunition is
live_partial, because the game caps it at 1023. - Inconclusive: structure health.
- Not tested: enemy attack damage.
- Withdrawn after live failures:
- the SH-20 Ballistic Shield backpack's default-zone armor;
- the Liberator's dormant base projectile member.
- Pending: armory trait lists and the Ballistic Shield's plate armor.
- Other unproven scopes:
- the status bullets on the Patriot;
- every other host, donor, row, effect and status;
- shared-row side effects, such as sentries firing the Patriot bullet;
- remote players' view of any of it.
Known limitations
- Multiplayer. No capability is proven for what other players see. Memory edits are local to the machine that
runs the mod. Every live test ran on the local machine, hosting. - Borrowed rows. Advanced projectile composition still uses the game's native rows (see above). Runtime-owned
custom projectile rows are later work. - Migration. On a new game build, spare twins are refused until
scripts/research_projectile_builder.pyis
re-run. The loader and event-source code-byte proofs fail closed, as documented in
docs/game-update-migration.md. - Coverage gaps:
- the GP-31 Grenade Pistol and P-72 Crisper stay blocked until they are re-mapped on their own roots;
- the Guard Dog gun's package is unknown, so it is refused as a donor.
SDK metadata
New catalogs:
EventCatalog.json,StatusEffectCatalog.json,EnemyAuthoringCapabilities.json;AttackOutputCapabilities.json: outputs, hosts,projectileSources,projectileBuilder, slots, presentation,
sharing,provenCompositions, live values;WeaponFireRateCapabilities.json,WeaponFeedCapabilities.json,WeaponPresentationCapabilities.json,
OutputCompositionCapabilities.json,WeaponMovementCapabilities.json;LiveEvidenceCatalog.json.
Changes to existing catalogs:
- Player weapons:
PlayerWeaponAuthoringCapabilities.jsongainssubweapons. - Vehicle weapons:
VehicleWeaponCapabilities.jsongains the mounted projectile host fields with their
projectileReferencemetadata. - Every field: carries its acknowledgement, sharing and refusal reason, plus
liveEvidenceand
liveProvenValueswhere a test proved exact values. - Lua stubs: they cover the new API (
hd2.events,hd2.mod,hd2.actions,hd2.explosions,
hd2.projectiles,hd2.status,hd2.attack_output,hd2.enemy,hd2.structure,hd2.diagnostics,
:projectile_source(),:programmable_ammo(),:feed(),:underbarrel()).
For ModBuilder and other generators
- Versions. Bundle SDK 0.28.0 and declare
min_version = 0.28.0for anything that uses 0.28 metadata. This
includes the Halt feed fields. - Isolation. Emit each operation so an error while building its request cannot stop the ones after it
(docs/user-report-modbuilder-halt-2026-09-30.md). - Acknowledgements. Only drop an acknowledgement where the catalog's
liveProvenValuesnames that exact value.
Never dropallow_shared.
Minimum versions
- 48 examples use 0.28 APIs, so they now declare
min_version0.28.0. Each of them was also run against the
0.27.0 runtime, where it fails validation. - Older examples keep their floors.
- The example validator now pins the 0.28.0 features by kind: fields, resources, underbarrel sub-targets and the
Halt's generic feed fields. - The ModTemplate keeps
min_version0.25.0. Its README lists what needs 0.28.0.
Validation
- Unit suite: 659 tests.
- Generator freshness: every domain table, catalog, stub and SDK document checks as current.
- Snapshot validation: all 20 production snapshot validators pass against the retained snapshot.
- Examples: 103 of 103 validate against this SDK, including the snapshot baselines.
- Packaged runtime: 131 scenarios run from the released runtime ZIP, with late module lookups disabled. They
include the user reports: the AyakaMods project, the ModBuilder Halt exports and the HMG read-budget package. - Game-update migration, with 0 unsafe stale writes in every run:
- 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).
Checksums (SHA-256)
65325a3e823509b0d42be686de63ea52f4f77e5b3b44a8e5de78c26d05f31fad HD2Runtime-0.28.0-runtime.zip
d3dc8f17b0edab528eeb25aec34bdf8c81d6f17ea2616f3cff00254165d7e60c HD2Runtime-0.28.0-sdk.zip
f3a6768dd3da57652c0e95f5d37f843362389ac73f9fbd163cc60661bf39b57c HD2Runtime-ModTemplate-0.28.0.zip
b93beb8ff54ab4c21fba4b3f68cc321b4033a6f56cc2f0884445bfeda8017542 HD2Runtime-0.28.0-example-projects.zip