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.
| Report | Cause | Effect |
|---|---|---|
| Error | A wrong type. | Stops the build. |
| Warning | A 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 is | Shows |
|---|---|
| an expression | its value, which _ then holds; None shows nothing |
| a statement | nothing, after changing the mod's names |
a print | the printed text |
| an error | its 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.