The entity system
How the typed entity libraries are generated from the game's own description of its entities.
Everything a player sees in a Deadlock match is an entity: heroes, troopers, Guardians, pickups, zip lines, lights, sounds, the shop. A mod creates any entity the server can create, reads and writes any field the game's schema declares, and sends any input a map's output could send. The typed libraries for TypeScript, Go and Luau are generated from the game itself. This page explains how those types are generated, and Entity calls explains what the host does and checks when a mod uses them.
Four names for one entity
An entity has four names, and each part of the system uses one:
| Name | Example | Where it comes from | What uses it |
|---|---|---|---|
| Designer name | npc_trooper_boss | The name a map places | createEntity, create, inputs |
| Schema class | CNPC_TrooperBoss | The C++ class the schema describes | Fields: readField, typed classes |
| Subclass | npc_boss_tier1 | A game data entry the entity reads | EntityOptions.subclass, spawnNpc |
| Entity name | arena_spawn_red | The targetname a map gives it | findEntitiesByName, entityName |
entityClass(handle) returns the first two for a live entity. The subclass is
the data an entity reads while it spawns, such as its health, model and weapons.
A lane Guardian and a base Guardian share a class and differ in their subclass.
The entity name is the one a map's author chose, often shared by a group of
entities, and an entity without one has an empty name. A mod gives the entities
it creates an entity name with the targetname key value, and finds a map's
markers with findEntitiesByName instead of hardcoding their positions:
import { entityPosition, findEntitiesByName } from 'modlock'
const spawns = findEntitiesByName('arena_spawn_red').map(entityPosition)The game dump
modlock-host --dump DIRECTORY starts a dedicated server, writes the game's
description of itself and quits. Each document follows
src/host_app/dump/dump.proto
and is sorted by name, so a diff of two builds' dumps lists only the entries
that changed.
data/dump contains
the current set:
| File | Contents | Count |
|---|---|---|
schemas.json | Every schema class with its base and fields, and every enum | 6,008 classes, 835 enums (build 6839) |
entities.json | Designer names, their classes, key values, inputs, outputs | 1,238 designer names, 472 data descriptions, 1,072 inputs, 433 outputs (6839) |
console.json | Console variables and commands with their types | 3,162 variables, 947 commands (6839) |
survey.json | What creating each designer name did on a dedicated server | 1,236 constructions of 1,191 names (build 6774) |
The first three come from the game's own registries: the schema system, the entity factories with their data descriptions, and the console. Nothing in them is written by hand, so a game update is a new dump.
- Schema classes.
schemas.jsonlists each class under its module, and theservermodule contains the entity classes. A field has its name, its type as the game spells it, and its offset. The generated libraries use the name and type. The host finds the offset by name the first time a mod uses the field. A game update that moves a field needs no new build of a mod. - Data descriptions. A designer name points at its data description, the
table a map reads when it places the entity. It adds key values and inputs to
its base and may embed others. A key value has a name, such as
rendercolor, and a type, such as a 32-bit color. An input has a name, such asColor, and the Pulse type of its one parameter, if it takes one. - Component keys. An entity also takes the key values of the components its
class has. The body component's scene node takes
origin,angles,scaleandModelScaleon every entity, soCBaseEntity's table lists them. Keys the scene node parses itself, such asscale, have a type but no field to read back.
The survey
survey.json records what happens when a mod creates each designer name on a
dedicated server with nothing else around it. A survey mod created each name,
waited a frame and recorded the result. A runner split the names over twelve
servers and resumed from the next name after each crash or hang. The survey
tried each name with the subclass the game's own data suggests, when there is
one. It then tried each name that lived without that subclass, to learn whether
the name needs one.
| Result | Names | Meaning |
|---|---|---|
| Lived | 578 | 543 alone; 35 only with their subclass: troopers, Guardians, Walkers, neutrals and the soul urn |
| Refused as an ability | 527 | Abilities, items and weapons, which only a hero holds |
| Failed | 57 | Abstract bases the server cannot create, entities that removed themselves during spawn, nodes with no identity |
| Crashed the server | 26 | Player pawns, bot brains, path nodes, capture points and others; 25 are now refused |
| Vanished | 3 | Created, then gone by the next frame |
The survey checks only that an entity lives one frame with default key values, not that it looks or behaves right.
Generated libraries
scripts/gen-entities.sh runs modlock-entitygen over the dump and writes the
typed libraries:
| Language | Classes and constructors | Console |
|---|---|---|
| TypeScript | js/src/entities.ts, imported as modlock/entities | modlock/console |
| Go | mod/entity | mod/console |
| Luau | luau/modlock/entities.luau, required as @modlock/entities | @modlock/console |
Each library has three parts.
A class per server schema class. Each field is a property in TypeScript
and Luau, and a method returning a getter and setter in Go. Each input the
class's designer names accept is a method. A class extends its base, so a
player pawn has CBaseEntity's health.
A constructor per designer name that lived. It takes only that entity's
spawn keys, each as the type the entity reads, and returns the entity as its
class. The constructor passes the subclass the survey found by default, so
create('npc_trooper_boss', ...) makes a lane Guardian without passing npc_boss_tier1. A designer name that never lived has no constructor: the
type checker rejects create('path_node', ...) before the mod runs.
Typed console values. Each variable takes the type the game declares; each command runs one line.
A mod's bundle contains only what the mod uses. The generator writes output that is stable across runs, so a game update shows up as a readable diff of what the game changed.
Updating for a new game build
After a game update:
- Run
modlock-host --dump data/dumpon a Windows server, or under Proton. - Run the survey and replace
data/dump/survey.json. - Run
scripts/gen-entities.shand review the diff of the generated libraries, which shows what the update changed.
The host looks up fields and inputs by name. A mod built against an earlier dump keeps working as long as the fields and inputs it uses still exist after the update.