Skip to Content
Modlock

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:

NameExampleWhere it comes fromWhat uses it
Designer namenpc_trooper_bossThe name a map placescreateEntity, create, inputs
Schema classCNPC_TrooperBossThe C++ class the schema describesFields: readField, typed classes
Subclassnpc_boss_tier1A game data entry the entity readsEntityOptions.subclass, spawnNpc
Entity namearena_spawn_redThe targetname a map gives itfindEntitiesByName, 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:

FileContentsCount
schemas.jsonEvery schema class with its base and fields, and every enum6,008 classes, 835 enums (build 6839)
entities.jsonDesigner names, their classes, key values, inputs, outputs1,238 designer names, 472 data descriptions, 1,072 inputs, 433 outputs (6839)
console.jsonConsole variables and commands with their types3,162 variables, 947 commands (6839)
survey.jsonWhat creating each designer name did on a dedicated server1,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.json lists each class under its module, and the server module 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 as Color, 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, scale and ModelScale on every entity, so CBaseEntity's table lists them. Keys the scene node parses itself, such as scale, 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.

ResultNamesMeaning
Lived578543 alone; 35 only with their subclass: troopers, Guardians, Walkers, neutrals and the soul urn
Refused as an ability527Abilities, items and weapons, which only a hero holds
Failed57Abstract bases the server cannot create, entities that removed themselves during spawn, nodes with no identity
Crashed the server26Player pawns, bot brains, path nodes, capture points and others; 25 are now refused
Vanished3Created, 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:

LanguageClasses and constructorsConsole
TypeScriptjs/src/entities.ts, imported as modlock/entitiesmodlock/console
Gomod/entitymod/console
Luauluau/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:

  1. Run modlock-host --dump data/dump on a Windows server, or under Proton.
  2. Run the survey and replace data/dump/survey.json.
  3. Run scripts/gen-entities.sh and 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.

On this page