Skip to Content
Modlock

Python

Python mods: decorators, dataclasses and type checking.

A Python mod imports modlock and registers its handlers with decorators in main.py:

import modlock


@modlock.command("hello")
def hello(player: modlock.Player, args: str) -> None:
    player.chat("Hello from Python!")

The guides show every call in Python: choose it with the switch in the sidebar.

Types and modules

Pyright, and Pylance in VS Code, check your mod's types.

ReportCauseEffect
ErrorA wrong type.Stops the build.
WarningA value that may be None, such as what find_hero returns, or a parameter without an annotation.The build prints it and continues.

For stricter or looser checking, edit typeCheckingMode and the report rules in the project's pyrightconfig.json. That file also tells the checkers where the library is, in .modlock, and they read it only from the folder the editor has open. Open the mod's own folder, because from any other folder the editor reports that modlock could not be resolved.

One module imports another by its name, such as import round. Mods run on Python 3.14 and can import standard modules such as dataclasses, enum, json, math, random and re.

Calls and types

The library has the TypeScript library's calls, spelled in snake_case. Each message is a dataclass, each enum is a set of string literals, and a 64-bit id is a Python integer. Build interfaces with modlock.ui, modlock.hud and modlock.theme.

# Headshots deal double damage.
@modlock.on_damage
def headshots(hit: modlock.DamageEvent) -> modlock.DamageResult | None:
    if hit.hit_group == 1:
        return modlock.DamageResult(amount=hit.amount * 2)
    return None


# Pay a soul bonus on /bonus.
@modlock.command("bonus")
def bonus(player: modlock.Player, args: str) -> None:
    player.adjust_souls(500)
    player.announce("Bonus", "500 souls")


# Answer the attack button.
@modlock.on_input
def fire(
    player: modlock.Player, pressed: modlock.Buttons, released: modlock.Buttons
) -> None:
    if pressed & modlock.Buttons.ATTACK:
        player.center_text("Fire!")

Typed entities and the console

modlock.entities has a class for each entity class, with its fields as attributes, and creates an entity from its designer name. modlock.console sets console variables and runs console commands. Import each beside modlock:

from modlock import console, entities

# Set a hero's health.
hero = entities.CCitadelPlayerPawn(pawn.entity)
hero.m_iHealth = 500

keys: entities.CNPC_TrooperBossKeys = {"LaneNum": 2}
boss = entities.create("npc_trooper_boss", modlock.Vector(x=0, y=0, z=0), keys=keys)

console.set_number("citadel_trooper_gold_reward", 40)
console.run("changelevel", "street_test")

When the server cannot read or write a field, such as hero.m_iHealth in the sample, the attribute reads as None, the server ignores writes to it, and the server log gives the reason.

Run a line in the running mod

While modlock dev or modlock play runs, type a line that starts with > in its terminal. The mod runs it in main.py's module, as Python's own prompt would:

> modlock.players()
[Connection(player=Player(slot=0), name='Player', ...)]
> _[0].name
'Player'
> score["kills"] += 1
A line that isShows
an expressionits value, which _ then holds; None shows nothing
a statementnothing, after changing the mod's names
a printthe printed text
an errorits traceback, and the mod keeps running

The mod runs the line between frames, both in the sandbox and in the game. Only Python mods have this prompt, and a mod in another language replies that it has no console.

On this page