Skip to main content

Python scripting

Python scripting is a Phoenix Engine addition. It mirrors the Lua system: the same file locations, the same callback names, and a subset of the same functions, so a script can be ported between the two languages mostly by changing syntax.

Requirements​

  • A build with PYTHON_ALLOWED — -DMODDING_LEVEL=2, which is the default on desktop and mobile.
  • Nothing else. The interpreter is Hython (haxelib hython), a pure-Haxe Python implementation, so no CPython runtime is installed or bundled.
lime test windows -DMODDING_LEVEL=2 # Lua + Python (default)
lime test windows -DMODDING_LEVEL=1 # Lua only — .py files are ignored

Where scripts go​

Exactly where Lua scripts go, with a .py extension:

LocationLoaded
mods/<mod>/scripts/*.pyevery song
mods/<mod>/data/<song>/<song>.pythat song
mods/<mod>/stages/<stage>.pythat stage
mods/<mod>/custom_notetypes/<type>.pythat notetype
mods/<mod>/custom_events/<event>.pythat event

Lua and Python scripts coexist; both are loaded and both receive every hook.

Softmod sandbox

Python files under mods/ are source-scanned before Hython is created. Dangerous OS/process/network/file imports and APIs are blocked by default, and host Haxe class resolution is disabled. A finding stops that script and logs its mod, file and line. See Script security sandbox for the rules and per-mod allowlist.

A minimal script​

def onCreate():
# some variables don't exist yet
pass

def onCreatePost():
makeLuaSprite('myBg', 'myImage', 0, 0)
setObjectCamera('myBg', 'camGame')
addLuaSprite('myBg', False)

def onUpdate(elapsed):
pass

def onBeatHit():
if curBeat % 4 == 0:
cameraShake('camGame', 0.005, 0.1)

def onDestroy():
pass

docs/TemplateScript.py (and example_mods/scripts/TemplateScript.py) is the commented template with every hook — Lua and Python share the same hook list; the shipped mods/bf-clicker/ mod is a complete working example.

Differences from Lua​

LuaPython
Hook definitionfunction onCreate()def onCreate():
Booleanstrue / falseTrue / False
NilnilNone
Tables as arguments{1, 2, 3}[1, 2, 3]
Function count273168
Comments--#

Both languages receive every hook listed in Script hooks; missing hooks silently never fire, so parity is load-bearing. Python callbacks are dispatched through the same PlayState.callOnScripts path as Lua (#87).

The Python library covers sprites, text, cameras, characters, properties, sound, score, timers, tweens, inter-script calls, Discord and window effects. Where a Lua function has no Python twin yet it is explicitly unsupported — use Lua for that part, or add the twin in psychlua/pystdlib/ and send a PR (how). Intentionally unsupported areas remain file I/O / shader / effect helpers, which are documented as Lua-only in the Python API reference.

Function names are intentionally identical to Lua, including the luaX prefixes (makeLuaSprite, addLuaText, …), so that porting is mechanical.

Control flow​

The same sentinels exist, as globals:

def onStartCountdown():
return Function_Stop # suppress the countdown

def onEndSong():
return Function_Continue

PythonScript names the third sentinel Function_StopAll internally; from a script both that name and the Lua-compatible Function_StopLua global are available (they are the same sentinel value, #51).

Debug output​

debugPrint('health is', getProperty('health'))

Output goes to the on-screen pythonDebugGroup, the Python counterpart of the Lua debug text. Errors are caught per call — a raised exception prints and the song keeps playing.

Performance​

Hython is an interpreter written in Haxe; it is slower than LuaJIT. For per-frame work in a demanding chart, prefer Lua. For setup, event reactions and beat-synced logic, the difference is irrelevant.

Next​