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/statetake a name
(hashed via the engine's ownString.GetHash) or a bare0xHASH. 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 globalOnStateChangeand 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, andsetis 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(_Gpersistence) andEss.Human.setState(posture). Covered by a
checkpure.pyMachinegroup andsamples/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.GetHashreturns the exact vocabulary hashes
(CollapseState→0x694683EB,PristineState→0xACB51200, …) andname()reverses them;.set()drove
a real building's 8 structural nodes toDestroyedState(returned true for all 8) and the engine reported
each transition back through.onChange— chained onto the world's ownOnStateChange, 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 fromresident/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 hash —pandemic_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 asEss.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.Inspectreads the components the engine exposes via getters today. - Generated from
data/ecs_registry.tsv(the Mercs2 reflection RE) bybuild/ecs.py; covered by a
checkpure.pyEcsgroup andsamples/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)(orEss.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 its0xHASHthroughSys.GuidToString, andEss.Namesreverses the hash.
Verified live: a spawned Veyron's model handle →0xB4FE2B80→civ_veh_car_veyron. Without the names
table it degrades to the bare0x…. - Engine getters return
1/0for booleans (and0is 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 acheckpure.pyInspectgroup andsamples/recipes/inspect.lua.
- Recovers what nothing else can: a readable name and model.
-
Ess.Names— turn a0xHASHback into the name it was hashed from. The engine addresses everything by
a one-way 32-bitpandemic_hash_m2, soEss.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 anEss.Log.Ess.Names.installed()/.count()/.load(table).Ess.Named(guid)—Ess.Namewith 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 into1_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 trapEss.RNGexists to avoid. - Built by
build/names.pyfrom the committed, hash-verifieddata/names.json; covered by acheckpure.py
group (Names) and asamples/recipes/names.luasmoke recipe.