Repository navigation
HD2Runtime v0.31.2 — No More Stutter, Four Statuses per Row, Ammunition Limits, the Game's Own Names and Support Weapon Payloads
HD2Runtime 0.31.2 — no more stutter, four statuses per row, ammunition limits, the game's own names and support weapon payloads
0.31.2 is a hotfix release built from player reports: custom stratagems no longer stutter every 5 seconds, beam
conversion values can be changed again and reverted, a damage row takes up to four statuses (the GL-15 Evictor's gas
and gas confusion), ammunition maxima follow the game's network limits (the TD-220 Bastion coax), the RL-77's bomblet
explosion is found under both rockets, the game's own names and custom stratagem texts work in Chinese and every other
game language, Acid Storm can be attached to weapons (asked for the TX-41 Sterilizer), the Sterilizer's hellpod is
editable, the LAS-99 Quasar's lockout after an overheat is editable, and support weapons take impact and expiry
payloads.
Each item says whether it was seen working in game ("live") or is covered by the offline tests and the snapshot
validators only. Two fixes are live, reported working in game on a development build (without a log): the custom
stratagem stutter and the beam conversion re-edit. Everything else is offline only, and its new fields need
allow_unverified_effect. Nothing in this release was tested in multiplayer.
Custom stratagems: no stutter every 5 seconds (live)
- A player reported "a pulse, a quick stutter" with custom stratagem mods. The F10 Performance tab showed the custom
stratagems at 0.65 ms on average with a peak of 53.8 ms: one long update about every 5 seconds. - Cause: aboard the ship, the Runtime checks the custom stratagems' carriers again every 5 seconds: the ship allocation
and the custom panel's availability view (M.REVALIDATE_EVERY). Each check ran one carrier discovery per registered
custom stratagem. Each discovery read every catalogued stratagem again: its row, its ownership, its look and its code,
about 1,700 reads. Live, each read is one ReadProcessMemory. With many custom stratagems, one update made tens of
thousands of reads. - Now (the same reads, made fewer times):
- an allocation runs one discovery for all the definitions that ask the same one (the same token, exclusions and Eagle
mode), not one per definition (runtime/carrier_allocator.lua); - what a discovery reads of each catalogued stratagem is read once per update and shared by every discovery in that
update. It is read again when any Runtime write happens in that update (runtime/stratagem_slot_conversion.lua); - a stratagem type scan that finds nothing is read once per update, not once per id (
runtime/stratagem_loadout.lua).
- an allocation runs one discovery for all the definitions that ask the same one (the same token, exclusions and Eagle
- The results are unchanged: the same carriers, verdicts, availability and ordering, and the same multiplayer rules. No
guard changed. - Offline, aboard the ship over a simulated minute (
tests/test_custom_stratagem_performance.py), the worst update
went:- with the three example custom stratagems: from 93,275 reads (about 120 ms) to 590 reads (about 3 ms);
- with fifteen: from 465,467 reads (about 570 ms) to 590 reads.
It no longer grows with the number of custom stratagems. The test fails if it does again.
- Live: tested in game on a development build of this fix and reported working (no log or numbers sent). A mission
and several players were not reported separately (schemas/live_evidence.json
custom_stratagem_revalidation_stutter_0_31_2).
Beam conversion: a field changes again, and back (live)
- A player report (HD2R Editor, APW-1):
beam.fire_rate300 -> 60 applied, then every later change was refused until
a restart, the reset too:CONFLICT: the APW-1 Anti-Materiel Rifle's beam.fire_rate is 60, neither the expected 300 nor the requested 300 (another operation set it). - Cause: an option-bound ensure (the editor holds each field with one, its value bound to a script value) may move the
value it applied itself on: every typed domain accepts those bytes (change.owned,core/ownership.lua). The beam
conversion domain checked onlyexpectandvalue, so the ensure's own 60 read as another operation's. Its restore
(the ensure'senabledoption off) failed the same way. - The beam conversion settings and per-weapon rows now accept the ensure's own previous value too
(domains/beam_conversion_writes.lua). Another mod's operation, and a writer outside HD2Runtime, are still
refused withCONFLICT. - A request naming
beam.fire_ratealone no longer refits a pulse that was set explicitly: only the Trident's 0.15 s
or the pulse fitted to the current rate is refitted. Before, the rate's and the pulse's ensures overwrote each other
at every steady-state check. - Live: tested in game on a development build with the HD2R Editor on the APW-1: repeated edits and the revert were
reported working (no log sent). Only this re-edit and revert behaviour is live. The gameplay effect of each beam value,
other weapons, another mod's refusal and multiplayer are not: every beam conversion write still needs
allow_component_swapandallow_unverified_effect(schemas/live_evidence.json
beam_conversion_same_mod_reedit_0_31_2). - See
docs/beam-conversion.md("Rules", "The pulse length limits the rate").
Status slots 1 to 4 on every damage row (offline only)
- Every DamageInfo row now offers all of its empty status slots, up to slot 4, each with its strength
(damage.status_<k>_type/_strength,explosion.damage.status_<k>_*). Before, only the first empty slot was
offered, so a row with no status could gain only one: the GL-15 Evictor's blast could not take bothgasand
gas_confusion. This applies to player, support and mounted weapons and to throwable explosions. - The slots stay packed from slot 1, now judged on the row as the whole write leaves it (
core/status_slots.lua):
slot k takes a status only when slots 1 to k-1 hold one, live (vanilla, or set by an earlier operation) or set by
the same transaction or another operation of the same plan phase, in any order. Slot 2 alone on an empty row is
refused (STATUS_SLOT_GAP), and so is clearing a slot while a later one holds a status. Each slot's own bytes are
still checked against their exact reviewed value. - The new slots after a row's first empty one always need
allow_unverified_effect, also on the live-proven
projectile direct-hit rows: a second attached status on one row has not been live-tested. - See
docs/status-effects.md("API", "Guards").
Ammunition maxima: 2047 rounds, 31 spare magazines (offline only)
- A player reported a TD-220 Bastion coax capacity of 2500 that showed about 2100 rounds for its owner and 500 for a
friend. The live round count is an 11-bit network field (0 to 2047) and the spare-magazine count a 5-bit one (0 to
31). The game clamps larger values at the next shot, so teammates see a different count. weapon.capacity/magazine.capacity/attachment.magazine_capacitynow take at most 2047. The magazine counts
(starting_magazines,magazines_from_supply,spare_magazines, and the attachment ones) take at most 31. This
covers player, support and mounted weapons and magazine attachments. Smaller existing maxima are kept. The refusal
names the network field.- Older mods keep working. A mod that declares an SDK older than 0.31.2 had no maximum: its larger value is applied
as the maximum and logged as a legacy operation. It is not refused. A mod that declares 0.31.2, or whose SDK cannot
be read, is refused. Seedocs/legacy-sdk-compatibility.md("Maxima added in 0.31.2"). - The TD-220 Bastion coax and the Exosuit arms own no reload component. Their spare-magazine fields are flagged
reloadComponent.present = falseas expected to have no effect. They are not blocked, and this is not proven from
game code. - See
docs/vehicle-weapons.md("Ammunition limits").
RL-77 bomblet explosion (offline only)
- A player looked for the RL-77 Airburst's bomblet explosion (500 damage per bomblet) under its normal rocket. The
bomblets explode as one row, both those of its normal rocket and those of its cluster round.
The catalogue named that row only after the cluster round:
support_weapon/rl77_airburst_rocket_launcher/function_expiry/shrapnel_expiry. - It now also has the owner-neutral alias
support_weapon/rl77_airburst_rocket_launcher/bomblet. The old id keeps
working. Seedocs/explosions.md("Chains with several sources"). - The support-weapon catalogue links the wiki branch "RL-77 P1 IE" to that explosion, and "EAT-700 P1 IE" to the
EAT-700's napalm shrapnel explosion. The link is structural: the burst's shrapnel projectile and that projectile's
impact explosion. Both are authored throughhd2.explosion(name). Seedocs/support-weapon-api.md. scripts/research_explosion_identities.pynow records every named source of a chain explosion as an owner: the
bomblet row lists both RL-77 bursts insdk/ExplosionCatalogue.json(owners). Ids, sharing and packages do not
change.
The game's own names in the game's language (offline only)
- A translator reported that the object names a tool shows (the Runtime's keys, such as
AR-23 Liberator) cannot be
localized. The game has every one of these names in all 15 languages.hd2.textnow reads them from the game, in
the language it is set to:hd2.text.name(kind, key)andtarget:display_name()return'AR-23 解放者'and its parts. Kinds: weapons,
support weapons, throwables, backpacks, vehicles, boosters and stratagems.hd2.text.language()/hd2.text.available()say whether the texts can be read now;hd2.text.game(id)reads one text by id.
- Read-only: nothing is written and nothing needs an acknowledgement. A lookup that cannot answer returns
nil, a code
and a reason, and the caller shows the key: before the game registers its text (UNAVAILABLE), for another language
than the game's (LANGUAGE_NOT_LOADED: only the current one is in memory) or on another build. - Names are composed as the armory shows them,
<designation> <name>. The keys do not change. Where the game's name
differs from the key, the game's is shown: CQC-73 Entrenchment Tool shows as CQC-72, the RL-77 backpack as Airburst
Rocket Backpack, the Super Earth FRV variant as M-102 GUNNER FRV, the GATER rig by its full name. The three
underbarrels have no game name (NO_NAME). - The text ids are internal: they are in the runtime catalogues only, not in the SDK's capability files.
- A mod that uses
hd2.textordisplay_nameneedsmin_version0.31.2. - See
docs/game-text.md.
Custom stratagem texts in every language (offline only)
name,name_cased,descriptionanddelivery.presentation.nameofhd2.custom_stratagem.register(and of a
custom_stratagems.jsonproject) now also take a table of the game's languages,{default = ..., cn = ..., tc = ...},
ashd2.resources.textdoes. The keys aredefaultand the 15 game language codes.traitstake any UTF-8 text without control characters. Before, only printable ASCII was accepted, so Chinese was
refused. Upper case is Unicode-aware (runtime/i18n.lua); CJK characters are kept as they are.- The limits are bytes, and the documentation and
sdk/CustomStratagemSchema.jsonnow say so (lengthUnit: bytes):- 64 for a name (about 21 CJK characters);
- 400 for a description;
- 32 for a trait (about 10 CJK characters).
- The project compiler (
hd2.py custom-stratagem) counts bytes too. Before, it counted characters, so a 64-character
Chinese name passed the compiler and was refused by the Runtime. - See
docs/custom-stratagem-api.md("Texts in every language").
Acid Storm on weapons and grenades (offline only)
acid_stormis now an attachable status (a player request: the TX-41 Sterilizer applying Acid Storm to enemies).
It is on the experimentalother_systemtier, so every write needsallow_unverified_effect. 35 of the 71
statuses are attachable now (other_system: 16).- Correction to the 0.31.1 notes. They said Acid Storm's definition is "a one-second marker with no damage" and
that "what the storm does comes from the weather system". The effect is in the status itself: while it is applied,
the game's own handler for this status lowers the armour of every armoured zone of the target by one level (never
below 0), and restores it when the status ends, about 1 s after the last hit that applied it. The weather only
applies the status, to enemies as well as Helldivers. It still deals no damage and shows no particles or sound.
About a dozen enemy types are not susceptible to it and are unaffected. - It is attachable as a reviewed, per-status family exception:
schemas/status_attachment_policy.jsongives it a
familyExceptionreason, and the generator accepts that key only on theother_systemtier, with a non-empty
reason. Every other weather, terrain, stim and system status stays refused (not attachable). - Use strength 1: the status then triggers on the first hit that penetrates the zone. See
docs/status-effects.md
("Values"). - No mod is affected: it is a newly allowed value, so no legacy entry is needed.
LAS-99 Quasar: the cooldown after an overheat (offline only)
- The Quasar's 15 s lockout after an overheat is its own member, WeaponHeat +140: the heat lost per second while the
weapon is overheated (and while it charges). The game's heat step was traced offline
(research/overheat-cooling-F5FEE03DCFDB.json): it cools atheat.cool_per_secondonly while the weapon is neither
overheated nor charging, and otherwise at +140, when WeaponHeat +144 is 0. The overheat ends at the release
threshold (+100, 0 on the Quasar): (100 - 0) / 6.66 = 15 s, about 17.5 s hot and 12.5 s cold. - New field
heat.cool_per_second_overheatedon the LAS-99 Quasar Cannon
(hd2.fields.heat.cool_per_second_overheated; 0.1 to 1000 per second; needsallow_unverified_effect). The
Quasar is the only weapon whose record has +144 = 0, and it owns that record alone. A write applies on the next
frame, also to a Quasar in hand. - Correction.
heat.overheat_cooldownsaid to editheat.capacityorheat.cool_per_second. That is wrong:
heat.cool_per_secondnever governs the lockout (a user project set it 6.66 -> 10 for this). Its reason now names
the weapon's own case: the Quasar's lockout formula with its values. On every other heat weapon, +144 is set: an
overheated weapon does not cool and needs a heatsink change (a reload). The LAS-17 Double-Edge Sickle never
overheats. The LAS-98's "100 / 5 = 20 s" indocs/support-weapon-api.mdwas wrong for the same reason. - WeaponHeat +144 ("no passive cooling while overheated") and +100 (the release threshold) are explained, not exposed.
- Live test pending (
schemas/live_evidence.jsonquasar_overheated_cooling_0_31_2), solo:
heat.cool_per_second_overheated20 should give about 5 s. As the A/B,heat.cool_per_second20 alone should
leave about 15 s. - See
docs/support-weapon-api.md("Warm-up and cooldown after overheat").
TX-41 Sterilizer hellpod (offline only)
- The TX-41's pod had no "Slot n" rows and a locked "Items spawned". Its rack is ordinary (one item, one consumer,
unshared, no random draw), but its pickup has no resource path, and the pod catalogue offered only pickups with
one. One missing path made the whole rack read-only, with the misleading reason "no resolvable name". - A typed pickup without a path is now offered when it has a catalogue name that no other offered pickup uses, an
entity row and a vanilla rack that delivers it. Its identity is proven as for every pickup. The generator asserts
that the TX-41 is the only one. Its rack is writable like any other:hd2.fields.payload.entityon its slots
(allow_unverified_referencefor anything but its own TX-41) andhd2.fields.payload.spawn_count
(allow_unverified_effect). - Its own loadout package (
chemgun) is recorded (research/package-residency-F5FEE03DCFDB.json, only this entry
added), so Runtime loads it before a TX-41 is placed in another pod. - The racks that stay read-only say why accurately: the Dark Fluid Backpack's item has neither a path nor a unique
name. The MS-11 Solo Silo's pod holds the silo and its remote, not pickups; the custom silo API covers it. - Knock-on: the TX-41 can now be delivered by a custom stratagem and register a
family='weapon'variant (33 hosts
register instead of 32). Neither is live-tested. - See
docs/pod-payloads.md("Read-only racks").
Impact and expiry payloads on support weapons (offline only)
- A support weapon could not change what its rounds explode into: the APW-1 had no payload row at all, and the AC-8
showed only its explosion's values. Support weapons now have the primaries' payload field on every projectile row an
attack fires, per fire mode:hd2.fields.terminal.explosionon
hd2.support_weapon(name):attack(role):projectile():terminal_action('impact' | 'expiry')
(terminal.<role>.<phase>.explosion). 51 fields on 23 weapons:primary(the normal round) on every support weapon that fires a projectile row;programmable, the ProgrammableAmmo feed: the AC-8's flak, the GR-8's and the RL-77's cluster round;- the Epoch's charge levels (
primary,full_charge) and the SG-88's rounds feed (feed_primary); - the Spear, Commando and W.A.S.P. missiles: impact only. A missile ends by its own lifetime, and whether its row's
expiry explosion ever fires is not traced, so no expiry is offered (the target names the reason).
- Values:
terminal:explosion()(the row's own),terminal:no_explosion(),hd2.explosion(name)(any explosion with a
known package) or another weapon's terminal explosion handle.terminal:current()is the expect. Every write needs
allow_sharedandallow_unverified_effect; a catalogued explosion alsoallow_unverified_reference. - The write uses the row the role's projectile fields already write, re-resolved through the weapon every time (a
swapped feed refuses). A catalogued explosion's package is loaded first, its row re-proven, and an explosion whose
shrapnel would release the written row again is refused (RECURSIVE_COMPOSITION): the W.A.S.P. cluster explosion on
the W.A.S.P.'s own row. - Caveats travel with each field (
terminal:describe(),payloadin the SDK file): who else fires or releases the row,
the MG-43 / MGX-42 tracer rows a magazine pattern also fires, an expiry on a row with no lifetime, the Epoch's charge
level. - These are shared row writes, as on the primaries. The per-shot payload for one weapon alone
(hd2.projectiles.payload) stays 0.32.0 work. - Proven offline on the retained snapshot: all 51 fields resolve to their reviewed rows and values; the APW-1 impact set
to a grenade's explosion, the AC-8 flak to an orbital shell's, the RL-77 cluster expiry removed, and the recursion
refusal. Live test pending (schemas/live_evidence.jsonsupport_payload_0_31_2). - See
docs/explosions.md("Payloads on support weapons") anddocs/support-weapon-api.md.
Payload options: every explosion a payload takes (offline only)
- An editor listed only the 14 explosions of the primaries as payloads. A payload has taken any catalogued explosion
with a known package since 0.30.0: 138 of them, from weapons, grenades, orbitals, eagles, Hellbombs, sentries, mines,
vehicles and more, and now 139 with the EXCESSIVELY LARGE NUKE (below). They are now published for a picker, grouped by source family:hd2.explosions.payload_options(filter): every option with its label, group, damage, radius, statuses, what it
leaves (a lingering field, fire, shrapnel, an arc), warnings, package and acknowledgements, andvalue
(hd2.explosion(name));terminal:payload_options(filter)(primary, secondary and support terminal actions): what one field takes: its own
explosion, none, every option with the value to write and the acknowledgements, and the ones it refuses with the
reason;hd2.explosions.payload_groups(),hd2.explosions.payload_warnings();terminal:current()on primaries too;sdk/PayloadOptions.json(contracthd2runtime.payload_options.v1, no raw ids): the options, the groups and every
payload field (185: 134 primary and secondary, 51 support) with its acknowledgements and refusals.
- Groups: weapons 18, support weapons 31, grenades and throwables 23, orbitals 13, eagles 12, Hellbombs 2, sentries
and emplacements 7, mines 4, vehicles and exosuits 11, backpacks 1, enemies 7, other 10. - Warnings inform and never refuse:
very_large(18, the Hellbombs' 10000 damage among them),lingering_field(12:
the game holds at most 4096 status fields and does not check the count),fire,shrapnel,arc,no_damage,
objective_packages(1). - Choosing an option loads its package before the write (
waiting_for_assets); a mission effects option needs none. A
package that is not resident within 90 s, or cannot be requested, rejects the operation (ASSET_UNAVAILABLE) and the
row keeps its value. - Generated by
scripts/generate_payload_options.py(domains/payload_options.lua,sdk/PayloadOptions.json). See
docs/explosions.md("Payload options").
EXCESSIVELY LARGE NUKE as a payload (offline only)
- The MS-N223 Shredder Silo's nuclear blast is now a payload option:
EXCESSIVELY LARGE NUKEin
hd2.explosions.payload_options()(groupother, warningsvery_largeandobjective_packages, anote). It is
the Cyborg Production Unit's explosion, the native catalogued row the silo requests, unmodified: 10000 damage, AP 10,
a 50 m inner and a 100 m outer radius. Its value ishd2.explosion('entity/cyborg_production_unit/ability'). - Before, the catalogue refused it as a payload: it knew only the objective package that holds its effect, not the
one that holds its sound. The catalogue entry now lists both (package.keys;sdk/ExplosionCatalogue.json
package.objective,packages = 2). A weapon payload, an attack output slot and a custom projectile row load
both before the write (core/assets.luaexplosion_dependencies), about 300 MB, which can take up to 90 s. - Why it is offered: both packages are in this build's bundle database, and the silo loads the same two packages and
requests the same explosion in a mission (seen live,custom_stratagem_silo). The write only references the row,
re-proven live first; it releases no shrapnel. As a projectile's impact it is not live-tested: it needs
allow_unverified_referenceandallow_unverified_effect, as every catalogued payload. - Anything within 100 m of the impact is caught, the shooter too. See
docs/explosions.md("EXCESSIVELY LARGE
NUKE").
Projectile swaps on the LAS-99 Quasar and the RS-422 Railgun (offline only)
- Two more support weapons are projectile hosts:
hd2.support_weapon('LAS-99 Quasar Cannon'):projectile_source()and
the RS-422 Railgun's are writable (hd2.fields.attack.projectile). 10 support weapons swap now instead of 8. - They were refused only because they are not magazine-fed. That rule keeps every host to the structure of the two
live controls: for another structure, something other than ProjectileWeapon +0 might select the fired projectile.
An offline trace of the game's fire path now shows that nothing else selects it for these two
(research/fired-type-callers-F5FEE03DCFDB.json). The Quasar's heat levels and the Railgun's charge levels hold no
projectile, so their fire passes none. The resolver then takes ProjectileWeapon +0: for a heat weapon directly, and
for the Railgun by the magazine path of the live-proven hosts. Each owns its record alone. The reasoning is in
docs/attack-outputs.md("Hosts outside the magazine-fed structure"). - Guard. No live control of these structures exists yet. So every donor on them needs
allow_unverified_referenceas well asallow_unverified_effect, even one of the host's own class
(HOST_STRUCTURE_UNVERIFIED). Restoring the weapon's own projectile needs nothing. Every other host guard applies. - Not relaxed: the M-1000 Maxigun and the GL-28. The trace found that a linked-ammunition weapon fires the type of
its linked ammunition source (its backpack) whenever one is linked. What that source holds is not traced, so they
stay read-only, now with that reason. - Live test pending (
schemas/live_evidence.jsonsupport_projectile_traced_hosts_0_31_2), solo: the Quasar fires
EAT-17 rockets and the Railgun APW-1 rounds after a fresh call-in.
Stats of the projectile an output fires (read-only)
- For an editor's "Fires: " view after a projectile swap:
hd2.attack_output(id):stats()returns the vanilla
stats of the projectile that output fires, from the game data. Nothing is read live and nothing is written.flight:velocity,mass,drag,gravity,lifetime,pellet_count,diameter,lifetime_variance,
penetration_slowdown;direct_damage:standard_damage,durable_damage,ap_direct...ap_extreme,demolition,stagger,
push_force,statuses;impact/expiry: thehd2.explosioncatalogue name, label, radii, shrapnel count, arc and damage.- Also
output,owner,compatibilityClassandconsumers(how many entities fire that row).
hd2.attack_output(id):owner_projectile()returns the owner weapon's own editable projectile handle when the output
is its primary attack. Edits there are shared with the owner (allow_shared).- Generated offline:
scripts/research_projectile_stats.py->research/projectile-stats-F5FEE03DCFDB.json, checked
against the projectile component catalogue and the explosion catalogue research. Then
scripts/generate_projectile_stats.py->domains/projectile_stats.lua. Live values:hd2.inspect. - See
docs/attack-outputs.md("Stats of an output projectile") andsdk/docs/api.md.
F10 Performance tab: lighter while it is shown (offline only)
- With the Performance tab open, its own drawing showed as "overlay settings" at about 0.8 ms a frame. Its numbers were
already computed once per graph bin (0.25 s), but every frame formatted, translated and fitted the texts again and
recomputed every rectangle. - What the tab draws is now built once per bin (and per window size, scale and language) and replayed in the other
frames: the same calls in the same order (runtime/performance_panel.lua). - Offline, with the real overlay builder, the tab's drawing went from about 0.48 to 0.32 ms a frame, and the whole
panel frame from 0.76 to 0.56 ms. Most of what remains is the overlay's own work for the graph's 360 rectangles.
Compatibility
- Mods built for 0.31.1 work unchanged. A mod that declares an SDK older than 0.31.2 and sets an ammunition value
above the new maxima keeps working: the value is clamped and logged as a legacy operation (see "Ammunition maxima"). - The startup line reads
HD2Runtime 0.31.2 initialized (API 1). - A mod that uses
hd2.textortarget:display_name()needsmin_version0.31.2. The other new fields and APIs
(status slots after a row's first empty one,heat.cool_per_second_overheated, the support weapon payloads, the
payload options,hd2.attack_output(id):stats(), the Quasar and Railgun swaps, the TX-41 rack,acid_storm, the
bombletalias, custom stratagem language tables) are unknown on 0.31.1: declare 0.31.2 for them too.