Skip to content

Ess 0.6.1

Latest

Choose a tag to compare

@github-actions github-actions released this 05 Aug 02:22

Four community contributions from @headless-rebase, merged together. Every one of them attacks the same
underlying problem from a different side: this engine addresses almost everything by a one-way 32-bit hash, so
the things you can reach at runtime are mostly opaque numbers. These make them legible.

Ess.Names reverses a hash to its name. Ess.Inspect uses that to turn an entity into a readable, typed
record. Ess.Machine does it for the destruction state vocabulary. Ess.Ecs catalogues the component classes
an entity is assembled from. Together they are the read side of the framework growing up.

Fully backwards compatible: all four are new namespaces, nothing existing changed.

Data verified independently, not taken on trust

All three contributions that ship hash tables claim the hashes are engine-verified. Rather than believe the
claim, pandemic_hash_m2 (FNV-1a, |0x20 case-fold, (^0x2A) * prime finaliser) was reimplemented from the
documentation and run over every row:

table rows mismatches
Ess.Names name map 23,110 0
Ess.Ecs component registry 232 (9 families, no duplicates) 0
Ess.Machine state vocabulary 13 0

Every name provably hashes to its own key, across all three tables, using one shared hash function. The
"never a fabricated name" property these namespaces promise is therefore demonstrable rather than asserted.
The Ess.Ecs anchors called out as RE-verified (Health=0x06BE1ABF, RuntimeHealth=0xF9B9B2A5,
RuntimeNodeHealth=0x76927BF5) all reproduce exactly.

Known follow-up (non-blocking, agreed before merge)

Ess.Machine.onChange chains the global OnStateChange correctly (prior handler preserved, every handler
pcalled so one cannot break the chain or the mission). But ensureDispatcher guards on the persistent
boolean Ess.Machine._installed, and Ess.Machine survives a level reload via or {}. If a resident mission
script later defines its own OnStateChange and displaces the dispatcher, the flag stays true and it can
never reinstall, so handlers go quiet permanently even if re-armed. Comparing _G.OnStateChange against the
dispatcher itself, instead of a flag, would let re-arming recover.

A note for anyone rebasing work across these

All four touch CHANGELOG.md, tools/checkpure.py and CAPABILITIES.md, so the later ones needed rebasing.
The checkpure.py conflict is worth knowing about: both sides open a TESTS block and share its closing
tail, so a plain keep-both-sides resolution leaves one block unterminated, silently swallows the other's tests
into a Lua string, and still reports green. It was resolved properly here; all 15 groups are present.

Added

  • Ess.Machine — the object destruction / state machine as a live control surface. Every destructible
    runs a state machine over a global vocabulary of state hashes (PristineState, DamagedState,
    DestroyedState, GoneState, CollapseState, …, shared across all destructibles, not per-object labels);
    damage drives the transitions. This lets you drive and watch it — the "force this building to CollapseState
    and see it" loop.

    • Ess.Machine.set(guid, node, state) — force a node of the machine to a state. node/state take a name
      (hashed via the engine's own String.GetHash) or a bare 0xHASH. A state name outside the global
      vocabulary is refused
      (Ess.DEBUG) rather than issued, because the damage system only ever reaches the
      known set — a novel state ships but is dead.
    • Ess.Machine.onChange(fn)stop()fn(guid, sState, sNode) on every transition, with the state
      and node hashes resolved to names (via the vocabulary + Ess.Names). Installs one dispatcher for the
      engine's global OnStateChange and chains any existing one (both fire) rather than clobbering it.
    • Ess.Machine.link(guid, hardpoint) (ObjectState.GetLinkGuid — a multi-part building's pieces are
      addressed this way, and set is node-keyed), .name(hash) (state hash → its vocabulary name, else the
      bare hash — never a guess), .print(guid) (ObjectState.PrintStateMachine), .STATES/.vocab().
    • The vocabulary is the cracked global state set (9 authoritative + 4 shipped-script names); the two
      uncracked core hashes are deliberately absent so .name() returns their bare hash rather than a label.
    • Distinct from Ess.State (_G persistence) and Ess.Human.setState (posture). Covered by a
      checkpure.py Machine group and samples/recipes/machine.lua.
    • Smoke-tested live over the lua-bridge in a running retail game: every native is present and callable
      (ObjectState.SetState/GetLinkGuid/PrintStateMachine, String.GetHash, Sys.GuidToString/
      StringToGuid, Object.GetHealth); String.GetHash returns the exact vocabulary hashes
      (CollapseState0x694683EB, PristineState0xACB51200, …) and name() reverses them; .set() drove
      a real building's 8 structural nodes to DestroyedState (returned true for all 8) and the engine reported
      each transition back through .onChange — chained onto the world's own OnStateChange, hashes resolved to
      names, uncracked states falling back to the bare hash; and .set() refused an out-of-vocabulary state
      before calling the engine. Note .set() is a logical state change (the object stays alive — visible
      destruction is the damage path, Ess.Object.kill; .set(node, "StartDestroyedState") plays the wreck).
      Call shapes are from resident/oilrig.lua.
  • Ess.Ecs — the engine's ECS component-class registry as a Lua-queryable typed vocabulary. An entity
    is assembled from reflection component classes (RuntimeHealth, StateMachine, Explosive, AiPatrol, …);
    this is the catalogue of all ~232, in 9 families, each with its component hashpandemic_hash_m2(name),
    the value the engine's component resolver keys on (verified against the RE: Health=0x06BE1ABF,
    RuntimeHealth=0xF9B9B2A5, RuntimeNodeHealth=0x76927BF5, …).

    • Ess.Ecs.classes() / .get(name) / .hash(name) / .family(name) / .find(query) (name-or-family
      substring, case-insensitive) / .families(). Misses return nil, never a guess; hashes are the canonical
      "0x…" string form (dodging the Lua-5.1-float trap, same as Ess.Names).
    • Scope: this is the naming half — the "what is a live entity made of" vocabulary. A generic raw
      per-entity component read (dump an arbitrary component's fields off an arbitrary entity) still needs a
      native memory-read verb the bridge doesn't expose; the path is reversed (an object→component resolver and
      the entity's 256-slot component table) and these hashes are its keys, so this ships the vocabulary that
      read will name things with. Ess.Inspect reads the components the engine exposes via getters today.
    • Generated from data/ecs_registry.tsv (the Mercs2 reflection RE) by build/ecs.py; covered by a
      checkpure.py Ecs group and samples/recipes/ecs.lua.
  • Ess.Inspect — a structured, NAMED read of an entity: the "remote inspector" side of the bridge (Plan
    03's "typed reads, not eval"). Ess.Inspect.read(guid) (or Ess.Inspect(guid)) returns a typed record
    grouped the way the engine's components are — identity / transform / health / physics / vehicle / faction —
    each field pulled through its confirmed getter and guarded, so a field the engine won't answer is simply
    absent rather than an error. .print(guid) logs it grouped for the console; .line(guid) is a one-line
    summary.

    • Recovers what nothing else can: a readable name and model. Object.GetName / Object.GetModelName
      return an opaque interned HANDLE, not a string (Ess.Object's own header says you "cannot read it back") —
      but that handle stringifies to its 0xHASH through Sys.GuidToString, and Ess.Names reverses the hash.
      Verified live: a spawned Veyron's model handle → 0xB4FE2B80civ_veh_car_veyron. Without the names
      table it degrades to the bare 0x….
    • Engine getters return 1/0 for booleans (and 0 is truthy in Lua), so the record coerces them to real
      bools. Composed from confirmed Ess wrappers (Ess.Object/Ess.Vehicle/Ess.Probe) + Ess.Names; covered
      by a checkpure.py Inspect group and samples/recipes/inspect.lua.
  • Ess.Names — turn a 0xHASH back into the name it was hashed from. The engine addresses everything by
    a one-way 32-bit pandemic_hash_m2, so Ess.Name(guid) gives you "0x4000563D" and there was no way
    back. This is the reverse side of that bridge — a lookup table, hash-verified against the retail WADs, that
    inverts the ~23k names the game actually ships.

    • Ess.Names.of(hash) → the name, or nil on a miss — never a fabricated name (the hash is one-way and
      only 32 bits; past a few million candidates a "match" is a collision, so a miss is reported honestly).
    • Ess.Names.label(hash)"name (0xHASH)", or the bare hash when unknown — always a string, drops
      straight into an Ess.Log. Ess.Names.installed() / .count() / .load(table).
    • Ess.Named(guid)Ess.Name with the meaning put back: "refinery_doc_warehouse01 (0x4000563D)"
      for a placed, named object (whose guid IS its name hash); the bare hash for a transient spawn handle.
    • The table is optional and shipped separately (scripts/OnLoad/2_EssNames.lua, ~1 MB) because it is
      far too large to fold into 1_Ess.lua. Opt in with one [OnLoad] line (see GETTING_STARTED / the README).
      With it absent, every call degrades cleanly to nil / the bare hash.
    • Keys are the "0x…" string form on purpose: this is Lua 5.1 with 32-bit floats, so a table keyed by
      the numeric hash would silently collide high hashes — the same class of trap Ess.RNG exists to avoid.
    • Built by build/names.py from the committed, hash-verified data/names.json; covered by a checkpure.py
      group (Names) and a samples/recipes/names.lua smoke recipe.