Skip to Content
Modlock

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"
}
FieldRequiredValue
slugyesIdentifier: lowercase letters, digits and hyphens.
nameyesThe name players see.
versionyesA semantic version: up to 64 letters, digits, ., + and -. Raise it for each publication.
languageyesLANGUAGE_TYPESCRIPT, LANGUAGE_JAVASCRIPT, LANGUAGE_LUAU, LANGUAGE_PYTHON or LANGUAGE_GO.
runtimeRUNTIME_WASM publishes a TypeScript or JavaScript mod compiled ahead of time.
mapThe map the mod plays on.
movementA movement model that replaces how heroes move.
definitionsDefinition changes to heroes and abilities.
settingsChoices each player makes about the mod.
metricsRunning totals for each player, which the mod adds to.
boardsRankings the mod reports scores to.
freeForAlltrue 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 }
FieldValue
modelMODEL_QUAKEWORLD: heroes move by QuakeWorld's rules against the Quake collision in maps/<map>.bsp.
scaleGame 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.

FieldValue
fileFILE_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.
entryThe entry to change, such as hero_priest, gunslinger_rocket_launcher, upgrade_colossus/modifier_colossus_active or k_ECitadelBotDifficulty_Hard.
propertiesAbility properties and their new values, such as AbilityCooldown.
fieldsFloat, range-float and modifier-level-float schema fields by Class.m_field/Class.m_field path, and their new values.
copyFieldsCopy the field at each value's path over the field at its key's path.
textFieldsText, 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 }
]
KindExtra fieldsValueStarts at without default
KIND_CHOICEchoicesOne choice's valueThe first choice
KIND_SWITCH"true" or "false"Off
KIND_NUMBERmin, max, stepA number on the stepmin

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" }
]
KindTotal
KIND_COUNTHow many times it was added.
KIND_SUMThe sum of the added values.
KIND_MAXThe 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"
  }
]
FieldValue
nameThe name the code reports to. It uses letters, digits, underscores and dots.
labelThe board's title on screen.
unitUNIT_NUMBER or UNIT_MILLISECONDS.
orderORDER_DESCENDING ranks the highest value first. ORDER_ASCENDING ranks the lowest first, as for a time.
keepThe rule that turns a player's scores into their standing, from the table below.
ratingRATING_ELO or RATING_GLICKO2, for a KEEP_RATING board.
partitionsUp to four dimensions that split the board into separate rankings. Each dimension may list allowed values.
windowsWINDOW_ALL_TIME, WINDOW_DAY and WINDOW_WEEK. All time alone by default. Windows follow UTC, and a week starts Monday.
tierThe least trusted server whose scores rank: TIER_HOSTED, TIER_ADMITTED or TIER_DEDICATED.
lowest, highestThe plausible values. A host holds a score outside them for review.
versionRaise it to start a fresh ranking when the unit, order, keep rule, rating or partitions change.
hudHUD_END_CARD asks the host to show the player's place after each score.
KeepStanding
KEEP_BESTThe best value by the board's order.
KEEP_SUMThe sum of the values.
KEEP_COUNTHow many scores the player reported.
KEEP_LATESTThe latest value.
KEEP_RATINGA 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.

On this page