The manifest
Every field of mod.json.
The manifest is the mod.json file at a project's root. It has the mod's
slug, name, version and language, and may add its map, movement, definition
changes, settings, metrics and boards. The smallest manifest has the four
required fields:
{
"slug": "my-mod",
"name": "My Mod",
"version": "0.1.0",
"language": "LANGUAGE_TYPESCRIPT"
}| Field | Required | Value |
|---|---|---|
slug | yes | Identifier: lowercase letters, digits and hyphens. |
name | yes | The name players see. |
version | yes | A semantic version: up to 64 letters, digits, ., + and -. Raise it for each publication. |
language | yes | LANGUAGE_TYPESCRIPT, LANGUAGE_JAVASCRIPT, LANGUAGE_LUAU, LANGUAGE_PYTHON or LANGUAGE_GO. |
runtime | RUNTIME_WASM publishes a TypeScript or JavaScript mod compiled ahead of time. | |
map | The map the mod plays on. | |
movement | A movement model that replaces how heroes move. | |
definitions | Definition changes to heroes and abilities. | |
settings | Choices each player makes about the mod. | |
metrics | Running totals for each player, which the mod adds to. | |
boards | Rankings the mod reports scores to. | |
freeForAll | true starts the mod in free-for-all. |
Map
"map": "hl_parry_ball"dev and play start the server on the first map in the mod's list that is
in the game's citadel/maps. When the mod leaves map out, it starts on
dl_midtown, both locally and on hyperline.gg.
A mod plays on the stock Deadlock map, dl_midtown, or on a map of its own.
For your own map, call it hl_<name> and put the compiled map in
maps/hl_<name>.vpk. build copies it into the build, and publish refuses a
build that lacks it. The build refuses any other map name, because a map that is
only in your own game's citadel/maps would load for you and for no one else.
Movement
"movement": { "model": "MODEL_QUAKEWORLD", "scale": 1.25 }| Field | Value |
|---|---|
model | MODEL_QUAKEWORLD: heroes move by QuakeWorld's rules against the Quake collision in maps/<map>.bsp. |
scale | Game units per Quake unit. |
The server moves the heroes by the model's rules while each player's game
stays stock, so a player's game predicts its own movement and the server
corrects it. Each time a hero lands, the mod's onLanded handler gets the
player and anyone they landed on.
Definitions
"definitions": [
{
"file": "FILE_HEROES",
"entry": "hero_priest",
"fields": { "CitadelHeroData_t.m_mapStartingStats[EStatsType.EMaxHealth]": 10000 }
},
{
"file": "FILE_BOT_DIFFICULTY",
"entry": "k_ECitadelBotDifficulty_Hard",
"fields": { "CBotDifficultyDefinition.low_health": 0, "CBotDifficultyDefinition.low_health_pushed": 0 }
},
{
"file": "FILE_NPC_UNITS",
"entry": "npc_player_bot_brain",
"fields": { "CCitadelPlayerBotNPCBrainVData.m_flDisengageFromEnemyToLaneDist": 100000 }
},
{
"file": "FILE_MODIFIERS",
"entry": "ability_doorman_hotel/pre_teleport",
"fields": {
"CModifierVData.m_flDuration": 12,
"CModifierVData.m_vecScriptValues[EModifierValue.MODIFIER_VALUE_FALL_SPEED_MAX]": 42
}
},
{
"entry": "gunslinger_rocket_launcher",
"properties": { "AbilityCooldown": 0.8, "Damage": 120 },
"fields": {
"CitadelAbilityVData.m_projectileInfo/ProjectileInfo_t.m_flSpeed": 1250,
"CitadelAbilityVData.m_mapWeaponInfos[primary]/CCitadelWeaponInfo.m_flBulletSpeed": 4000
},
"copyFields": {
"CAbilityRocketLauncherVData.m_ExplosionParticle": "CitadelAbilityVData.m_skillshotMissParticle"
},
"textFields": {
"CitadelAbilityVData.m_strCastSound": "Hero.Warden.Flask.Throw"
}
}
]Each entry changes one stock definition on the server only. Players' games keep reading their own stock definitions, so tooltips and parts of the HUD show the stock values while play uses the changed ones.
| Field | Value |
|---|---|
file | FILE_ABILITIES, the default, for abilities and items; FILE_HEROES for heroes; FILE_BOT_DIFFICULTY for bot difficulties; FILE_NPC_UNITS for units and the bot brain; or FILE_MODIFIERS for modifiers. |
entry | The entry to change, such as hero_priest, gunslinger_rocket_launcher, upgrade_colossus/modifier_colossus_active or k_ECitadelBotDifficulty_Hard. |
properties | Ability properties and their new values, such as AbilityCooldown. |
fields | Float, range-float and modifier-level-float schema fields by Class.m_field/Class.m_field path, and their new values. |
copyFields | Copy the field at each value's path over the field at its key's path. |
textFields | Text, symbol, sound event and Panorama image schema fields by Class.m_field/Class.m_field path, with {addon} replaced by the mod's content addon. |
A step on a map can end in [key] to select one value: [Enum.Value] for a
map keyed by an enum, such as a hero's starting stats, or the key's name for a
map keyed by name, such as an ability's m_mapWeaponInfos[primary]. The next
step then reads a field inside that value. On a modifier's m_vecScriptValues
list, a last step ending in [EModifierValue.Value] selects one entry. A single value
you give fills every level of a level float or script value, and both bounds of
a range. The server writes the changes when it loads the definitions, and again
on each new map.
A bot difficulty entry is an ECitadelBotDifficulty value, and every bot of
that difficulty reads the changed record, including bots already playing. A bot
whose difficulty has no record in the file uses the easy one. A bot retreats to
its base when its health falls below low_health percent, or below
low_health_pushed percent when it is near an enemy tower. Setting both to 0
makes bots fight to the death.
Every bot reads the one npc_player_bot_brain unit. When an enemy stands more
than m_flDisengageFromEnemyToLaneDist beyond the front of the bot's lane, the
bot gives up the chase and walks back to the lane. Raise that field to let bots
fight away from the lanes.
Settings
"settings": [
{
"key": "layout",
"label": "HUD layout",
"kind": "KIND_CHOICE",
"choices": [
{ "value": "full", "label": "Full" },
{ "value": "compact", "label": "Compact" }
],
"default": "compact"
},
{ "key": "sounds", "label": "Sounds", "kind": "KIND_SWITCH" },
{ "key": "volume", "label": "Volume", "kind": "KIND_NUMBER", "min": 0, "max": 100, "step": 5 }
]| Kind | Extra fields | Value | Starts at without default |
|---|---|---|---|
KIND_CHOICE | choices | One choice's value | The first choice |
KIND_SWITCH | "true" or "false" | Off | |
KIND_NUMBER | min, max, step | A number on the step | min |
A key is up to 64 letters, digits and underscores and starts with a letter
or underscore, and every value is stored as text. modlock build rejects a
setting that gives players nothing to choose from, such as a choice setting
with no choices or a number whose min is not below its max. Settings shows how a mod reads and changes them.
Metrics
"metrics": [
{ "name": "hud.opened", "kind": "KIND_COUNT", "labels": ["full", "compact"] },
{ "name": "round.best", "kind": "KIND_MAX" }
]| Kind | Total |
|---|---|
KIND_COUNT | How many times it was added. |
KIND_SUM | The sum of the added values. |
KIND_MAX | The largest added value. |
A name uses letters, digits, underscores and dots. To keep a separate total
for each value of something, such as one total per HUD layout, list the values
in labels. Then pass the label as the third argument of
player.addMetric(name, value, label). The host keeps the totals in memory and
passes them to its metrics service once, when the player leaves or the mod
stops. When no service takes them, the mod logs them instead. Bots never get
totals.
Boards
"boards": [
{
"name": "aim.score",
"label": "Best score",
"unit": "UNIT_NUMBER",
"order": "ORDER_DESCENDING",
"keep": "KEEP_BEST",
"partitions": [
{ "name": "scenario" },
{ "name": "difficulty", "values": ["easy", "medium", "hard"] }
],
"windows": ["WINDOW_ALL_TIME", "WINDOW_WEEK", "WINDOW_DAY"],
"tier": "TIER_ADMITTED",
"lowest": "0",
"highest": "100000"
}
]| Field | Value |
|---|---|
name | The name the code reports to. It uses letters, digits, underscores and dots. |
label | The board's title on screen. |
unit | UNIT_NUMBER or UNIT_MILLISECONDS. |
order | ORDER_DESCENDING ranks the highest value first. ORDER_ASCENDING ranks the lowest first, as for a time. |
keep | The rule that turns a player's scores into their standing, from the table below. |
rating | RATING_ELO or RATING_GLICKO2, for a KEEP_RATING board. |
partitions | Up to four dimensions that split the board into separate rankings. Each dimension may list allowed values. |
windows | WINDOW_ALL_TIME, WINDOW_DAY and WINDOW_WEEK. All time alone by default. Windows follow UTC, and a week starts Monday. |
tier | The least trusted server whose scores rank: TIER_HOSTED, TIER_ADMITTED or TIER_DEDICATED. |
lowest, highest | The plausible values. A host holds a score outside them for review. |
version | Raise it to start a fresh ranking when the unit, order, keep rule, rating or partitions change. |
hud | HUD_END_CARD asks the host to show the player's place after each score. |
| Keep | Standing |
|---|---|
KEEP_BEST | The best value by the board's order. |
KEEP_SUM | The sum of the values. |
KEEP_COUNT | How many scores the player reported. |
KEEP_LATEST | The latest value. |
KEEP_RATING | A rating each reportMatch moves from the places. |
When your mod reports a score, it gives a value for each partition and may give
a key for the run. A score sent twice with the same key counts once. Between equal
values, the lower tiebreak ranks first. The host's scores service ranks the
scores, decides who may report and how far it trusts each server. Under
modlock dev, where no such service runs, the host checks each score against
its board and logs it, and the mod logs any score the host does not accept.
The built manifest
modlock build writes a mod.json into build/ that adds the entry file and
sets the runtime that runs it. modlock-host --plugin build loads the mod
from that file.