Skip to content

Gotchas & Best Practices

Key rules and common pitfalls when writing in-process game thread Lua scripts.


  • No synchronous network I/O: Never call blocking HTTP or unzip operations inside render or createmove.
  • Pre-cache assets: Load fonts, images (render.load_image), and SVGs during script init rather than every frame.
  • Limit raycasts: Keep trace rays and bullet penetration calls within reasonable limits.

Accessing fields on invalid or dead entities can crash the game engine:

-- BAD: Crashes if local player is dead/nil
local origin = entity.get_local_player():get_abs_origin()
-- GOOD: Validate entity handles first
local me = entity.get_local_player()
if not me or not me:is_alive() then
return
end
local origin = me:get_abs_origin()

All drawing functions (render.text, render.rect, render.line, etc.) are valid only inside the render callback.

Calling render methods from createmove or game_events does nothing and risks graphics buffer desynchronization.


Netprop array wrappers provide raw pointer access:

  • Index from 0 to size - 1.
  • The # length operator does not return array length.
  • Indexing past the bounds reads raw memory.

Calling el:set(val) programmatically does not invoke callbacks registered via el:set_callback (except for combos). If you update a widget in code, invoke your state handler manually.


  • Return 0: Script takes over rendering (requires materials.draw_chams).
  • Return 1: Engine draws the model normally.
  • Omitting return evaluates to 0, hiding the model.

json.stringify serializes userdata (vector, qangle, color) as null. Convert them to plain tables first:

local pos = me:get_abs_origin()
local str = json.stringify({ x = pos.x, y = pos.y, z = pos.z })

Saved widget configs in scripts/data/<script>.cfg map to Groupbox + Widget Name + Type.

Renaming a widget or moving it to another groupbox resets its saved value.