Halo: Scripting with Chimera (Client-Side Lua)
Table of Contents
- Loading table of contents...
Chimera is a client-side mod for Halo PC/CE. It provides event hooks, commands, built-in map downloads, and a lot of quality-of-life fixes. This guide covers its Lua API, with practical examples for HUDs and gameplay tweaks.
| Property | Value |
|---|---|
| Lua runtime | Lua 5.5.1 |
| Lua 5.2+ syntax | Available: goto, _ENV, integer division //, native bitwise operators & \| ~ << >> |
| Sandboxing | Global scripts: not sandboxed. Map scripts: sandboxed by default. |
| Reload all scripts | Console command chimera_lua_scripts_reload (~ opens the console) |
| Player index | 0-15 everywhere |
Script locations
Chimera looks for Lua scripts in two folders and treats them differently:
-
Global scripts: Put
.luafiles inchimera\lua\scripts\global. They load when Chimera starts and stay active until you reload them. Use this for tools and HUD mods you want all the time. Global scripts are not sandboxed. -
Map scripts: Put scripts in
chimera\lua\scripts\map. They load when their map is active and unload when you leave it. Map scripts are sandboxed by default (see Sandbox restrictions).
By default, the Chimera folder is located inside your Halo data folder:
- Halo Custom Edition (CE):
%USERPROFILE%\Documents\My Games\Halo CE\chimera - Halo PC:
%USERPROFILE%\Documents\My Games\Halo\chimera
Linux: ~/Documents/My Games/Halo CE/chimera or ~/Documents/My Games/Halo/chimera, depending
on your setup.
Script skeleton
Every Chimera Lua script must define clua_version as the Chimera API version it targets (for
example 2.056). Without it, Chimera will not load the script. See Chimera Lua API
for every callback and function.
clua_version = 2.056
set_callback("map load", "OnMapLoad")
set_callback("tick", "OnTick")
function OnMapLoad()
console_out("Map: (" .. map .. ")")
end
function OnTick() end -- runs every tick; keep this lean
Chimera Lua API
This doubles as an API reference and EmmyLua / LuaLS script for Lua-aware editors (such as VS Code).
---@meta
-- Chimera Lua API for EmmyLua and lua-language-server.
-- Runtime is Lua 5.5.
--
-- Copyright (c) 2026 Jericho Crosby (Chalwk).
--
---@alias ChimeraEvent
--- | "command" # (cmd: string): boolean|nil. Return false to block.
--- | "frame" # Each rendered frame. Draw calls go here or in preframe.
--- | "preframe" # Before frame processing.
--- | "map_load" # A map is being loaded.
--- | "map_preload" # Before map loading.
--- | "precamera" # (x, y, z, fov, fx, fy, fz, ux, uy, uz). Memory Addresses & Offsets
--- | "rcon_message" # (message: string): boolean|nil. Return false to block.
--- | "tick" # Each game tick (1/30 s).
--- | "pretick" # Before each game tick.
--- | "unload" # The script is unloading.
---@alias ChimeraPriority "before" | "default" | "after" | "final"
---@alias ChimeraGametype "ctf" | "slayer" | "oddball" | "king" | "race"
-- Script lifecycle -------------------------------------------------------------------------
--- Required. The script refuses to load without it.
---@type number
clua_version = nil
-- Globals ----------------------------------------------------------------------------------
--- Chimera build number (Git commit count).
---@type integer
build = 0
--- Chimera version string.
---@type string
full_build = ""
--- Current gametype.
---@type ChimeraGametype
gametype = "slayer"
--- Local player index (0-15). nil until assigned after joining a server.
---@type number | nil
local_player_index = nil
--- Currently loaded map name.
---@type string
map = ""
--- True if the map uses protected tag data.
---@type boolean
map_is_protected = false
--- True if the script runs sandboxed.
---@type boolean
sandboxed = false
--- Filename for global scripts, map name for map scripts.
---@type string
script_name = ""
---@type "global" | "map"
script_type = "global"
--- "none" is single player, "local" is hosting, "dedicated" is joined to a server.
---@type "none" | "local" | "dedicated"
server_type = "none"
-- Callbacks and timers ---------------------------------------------------------------------
--
-- NOTE: "spawn" and "prespawn" are accepted by set_callback at runtime but are not hooked
-- into any engine event by the current Chimera build.
--- Callback names for set_callback. These are the global function names.
---@alias ChimeraCallbackName
--- | "OnCommand"
--- | "OnFrame"
--- | "OnMapLoad"
--- | "OnMapPreload"
--- | "OnPreCamera"
--- | "OnPreFrame"
--- | "OnPreTick"
--- | "OnRconMessage"
--- | "OnScriptUnload"
--- | "OnTick"
--- Event names for set_callback. Hover a field for the callback signature.
---@class ChimeraCallbackIds
---@field command ChimeraEvent OnCommand(cmd: string): boolean|nil. Return false to block.
---@field frame ChimeraEvent OnFrame(). Each rendered frame. Draw calls go here or in preframe.
---@field map_load ChimeraEvent OnMapLoad(). A map is being loaded.
---@field map_preload ChimeraEvent OnMapPreload(). Before map loading.
---@field precamera ChimeraEvent OnPreCamera(x, y, z, fov, fx, fy, fz, ux, uy, uz).
---@field preframe ChimeraEvent OnPreFrame(). Before frame processing. Draw calls go here or in frame.
---@field pretick ChimeraEvent OnPreTick(). Before each game tick.
---@field rcon_message ChimeraEvent OnRconMessage(message: string): boolean|nil. Return false to block.
---@field tick ChimeraEvent OnTick(). Each game tick (1/30 s).
---@field unload ChimeraEvent OnScriptUnload(). The script is unloading.
--- The callback must be a global function. Priorities decide the order between scripts.
---@param event ChimeraEvent | string
---@param callback_name ChimeraCallbackName | string Name of a global function
---@param priority? ChimeraPriority
function set_callback(event, callback_name, priority) end
--- Runs the callback every interval until it returns false or the timer is stopped.
---@param interval_ms number
---@param callback_name string Name of a global function
---@param ... boolean | number | string | nil Forwarded to the callback
---@return integer timer_id
function set_timer(interval_ms, callback_name, ...) end
--- Stopping a timer that no longer exists raises an error.
---@param timer_id number
function stop_timer(timer_id) end
-- Output -----------------------------------------------------------------------------------
---@param message string
function hud_message(message) end
---@overload fun(message: string, red: number, green: number, blue: number)
---@overload fun(message: string, alpha: number, red: number, green: number, blue: number)
---@param message string
function console_out(message) end
--- Draws inside a screen rectangle. Call it from preframe or frame.
---@param text string
---@param left number
---@param top number
---@param right number
---@param bottom number
---@param font "small" | "large" | "ticker"
---@param align "left" | "center" | "right"
---@param alpha number
---@param red number
---@param green number
---@param blue number
function draw_text(text, left, top, right, bottom, font, align, alpha, red, green, blue) end
-- Console and scripting --------------------------------------------------------------------
--- Runs a Halo console command or a Halo script expression (Lisp syntax).
---@param script string
function execute_script(script) end
--- Reads a Halo scripting global.
---@param name string
---@return boolean | number
function get_global(name) end
--- The value type is validated.
---@param name string
---@param value boolean | number
function set_global(name, value) end
--- Only allow-listed Chimera commands work.
---@param command string
---@param save boolean
function execute_chimera_command(command, save) end
-- Players, objects and tags ----------------------------------------------------------------
--- Player table pointer, or nil. No index returns the local player.
---@param index? number
---@return integer | nil address
function get_player(index) end
--- Pointer to the player's current object, or nil if the player is dead. No index returns the local player.
---@param index? number
---@return integer | nil address
function get_dynamic_player(index) end
--- Dynamic object pointer for an object id, or nil.
---@param object_id number
---@return integer | nil address
function get_object(object_id) end
--- Raises an error if the id is invalid.
---@param object_id number
function delete_object(object_id) end
--- Spawned client side only in multiplayer. Other players do not see the object.
---@overload fun(tag_class: string, tag_path: string, x: number, y: number, z: number): integer
---@param tag_id number
---@param x number
---@param y number
---@param z number
---@param parent? number
---@return integer object_id
function spawn_object(tag_id, x, y, z, parent) end
--- With a class and path, returns the tag id. With a tag id, returns the tag entry pointer.
---@overload fun(tag_id: number): integer | nil
---@param tag_class string Four-letter class, e.g. "weap"
---@param tag_path string
---@return integer | nil tag
function get_tag(tag_class, tag_path) end
-- Memory -----------------------------------------------------------------------------------
--- Reads a signed 8-bit integer.
---@param address number
---@return integer
function read_i8(address) end
--- Reads an unsigned 8-bit integer.
---@param address number
---@return integer
function read_u8(address) end
--- Reads a signed 16-bit integer.
---@param address number
---@return integer
function read_i16(address) end
--- Reads an unsigned 16-bit integer.
---@param address number
---@return integer
function read_u16(address) end
--- Reads a signed 32-bit integer.
---@param address number
---@return integer
function read_i32(address) end
--- Reads an unsigned 32-bit integer.
---@param address number
---@return integer
function read_u32(address) end
--- Reads an unsigned 8-bit integer.
---@param address number
---@return integer
function read_byte(address) end
--- Reads an unsigned 16-bit integer.
---@param address number
---@return integer
function read_word(address) end
--- Reads an unsigned 32-bit integer.
---@param address number
---@return integer
function read_dword(address) end
--- Reads a signed 8-bit integer.
---@param address number
---@return integer
function read_char(address) end
--- Reads a signed 16-bit integer.
---@param address number
---@return integer
function read_short(address) end
--- Reads a signed 32-bit integer.
---@param address number
---@return integer
function read_int(address) end
--- Reads an integer.
---@param address number
---@return integer
function read_long(address) end
--- Reads a 32-bit float.
---@param address number
---@return number
function read_f32(address) end
--- Reads a 64-bit float.
---@param address number
---@return number
function read_f64(address) end
--- Reads a 32-bit float.
---@param address number
---@return number
function read_float(address) end
--- Reads a 64-bit float.
---@param address number
---@return number
function read_double(address) end
--- Writes a signed 8-bit integer.
---@param address number
---@param value number
function write_i8(address, value) end
--- Writes an unsigned 8-bit integer.
---@param address number
---@param value number
function write_u8(address, value) end
--- Writes a signed 16-bit integer.
---@param address number
---@param value number
function write_i16(address, value) end
--- Writes an unsigned 16-bit integer.
---@param address number
---@param value number
function write_u16(address, value) end
--- Writes a signed 32-bit integer.
---@param address number
---@param value number
function write_i32(address, value) end
--- Writes an unsigned 32-bit integer.
---@param address number
---@param value number
function write_u32(address, value) end
--- Writes an unsigned 8-bit integer.
---@param address number
---@param value number
function write_byte(address, value) end
--- Writes an unsigned 16-bit integer.
---@param address number
---@param value number
function write_word(address, value) end
--- Writes an unsigned 32-bit integer.
---@param address number
---@param value number
function write_dword(address, value) end
--- Writes a signed 8-bit integer.
---@param address number
---@param value number
function write_char(address, value) end
--- Writes a signed 16-bit integer.
---@param address number
---@param value number
function write_short(address, value) end
--- Writes a signed 32-bit integer.
---@param address number
---@param value number
function write_int(address, value) end
--- Writes an integer.
---@param address number
---@param value number
function write_long(address, value) end
--- Writes a 32-bit float.
---@param address number
---@param value number
function write_f32(address, value) end
--- Writes a 64-bit float.
---@param address number
---@param value number
function write_f64(address, value) end
--- Writes a 32-bit float.
---@param address number
---@param value number
function write_float(address, value) end
--- Writes a 64-bit float.
---@param address number
---@param value number
function write_double(address, value) end
---@param address number
---@param bit_index number Less than 32
---@return integer bit 0 or 1
function read_bit(address, bit_index) end
---@param address number
---@param bit_index number Less than 32
---@param value boolean | number
function write_bit(address, bit_index, value) end
--- Reads a null-terminated string.
---@param address number
---@return string
function read_string8(address) end
--- Writes a null-terminated string.
---@param address number
---@param value string
function write_string8(address, value) end
--- Alias of read_string8.
---@param address number
---@return string
function read_string(address) end
--- Alias of write_string8.
---@param address number
---@param value string
function write_string(address, value) end
-- Filesystem -------------------------------------------------------------------------------
-- Confined to chimera/lua/data/<script_type>/<script_name>/. Paths outside raise a scope error.
---@param path string
---@return boolean ok True if created or already present
function create_directory(path) end
---@param path string
---@return boolean ok
function remove_directory(path) end
--- Directory entries end in a backslash.
---@param path string
---@return string[] | false entries
function list_directory(path) end
---@param path string
---@return boolean
function directory_exists(path) end
---@param path string
---@param content string
---@param append? boolean
---@return boolean ok
function write_file(path, content, append) end
---@param path string
---@return string | nil content nil if the file cannot be opened
function read_file(path) end
---@param path string
---@return boolean ok
function delete_file(path) end
---@param path string
---@return boolean
function file_exists(path) end
Memory addresses and offsets
| Pointer | PC Address | CE Address | Description |
|---|---|---|---|
gametype |
0x6F1C88 |
0x68CC48 |
Gametype struct base (see Gametype Struct) |
race_globals |
0x87A520 |
0x64C1C0 |
Race global data base (see Race Globals) |
camera_address |
0x647498 |
0x647498 |
Camera state mode (30400 = First Person, 31952 = Third Person, 30704 = Devcam) |
camera_interpolation |
0x647494 |
0x647494 |
Camera interpolation scale when entering or leaving vehicles |
zoom_address |
read_dword(0x815918) + 204 |
read_dword(0x815918) + 204 |
Local player camera zoom level pointer |
mouse_input_address |
0x64C73C |
0x64C73C |
Mouse input state (+0x0 delta X, +0x4 delta Y, +0x8 scroll wheel, +0xC left click, +0xE right click) |
keyboard_input_address |
0x64C550 |
0x64C550 |
Keyboard key state array indexed by key code |
chat_open_address |
0x64E788 |
0x64E788 |
Chat input window state (1 = open, 0 = closed) |
announcer_address |
0x64C020 |
0x64C020 |
Client-side multiplayer announcer sound queue (+0x8 delay, +0x14 sound tag ID) |
first_person_animation_address |
0x40000EB8 |
0x40000EB8 |
First-person animation structure (+0x1E base anim ID, +0x20 base anim frame) |
game_state_address |
0x400002E8 |
0x400002E8 |
Screen render and pause state (+0x0 screen render, +0x1 game running, +0x2 game paused) |
game_paused_address |
0x004ACA79 |
0x004ACA79 |
Game pause byte (1 = paused, 0 = running) |
game_on_menus_address |
0x00622058 |
0x00622058 |
Game menu state byte (1 = in menu/console, 0 = gameplay) |
joystick_input_address |
0x0064D998 |
0x0064D998 |
Controller/joystick input structure array (0xA0 bytes per controller) |
object_table_address |
0x400506B4 |
0x4005062C |
Global object table header |
device_groups_table |
0x00816110 |
0x006E1C50 |
Scenario device groups table |
widgets_instance |
0x006B401C |
0x006B401C |
UI widget instance structure |
synced_network_objects |
0x006226F0 |
0x00598020 |
Synced network objects table pointer |
gravity_global |
0x637BE4 |
0x637BE4 |
World gravity float multiplier (default ~ 0.003565) |
ctf_flag_red |
0x40492F1C |
0x40492F1C |
Red flag world spawn coordinates structure |
ctf_flag_blue |
0x40492FB0 |
0x40492FB0 |
Blue flag world spawn coordinates structure |
koth_globals |
0x64BDF0 |
0x64BDF0 |
King of the Hill global data (+0x90 marker count, +0x94 marker coords, +0x184 center coords) |
race_locs |
0x68C848 |
0x68C848 |
Race checkpoint world coordinates array (0x20 bytes per checkpoint) |
Probing the Platform
Chimera has no halo_type and no function that reports which client you are on. Put both variants
in a candidates table, probe each one, and check the value you read against what the field should
hold.
-- Same logical pointer, different address on each platform.
local candidates = {
{ gametype = 0x6F1C88, race_globals = 0x87A520 }, -- PC
{ gametype = 0x68CC48, race_globals = 0x64C1C0 } -- CE
}
local gametype_base, race_globals
local function detectPlatform()
for _, cand in ipairs(candidates) do
-- pcall: an address valid on one platform may be unmapped on the other.
local ok, game_type = pcall(function()
return read_byte(cand.gametype + 0x30)
end)
-- 5 = Race. The value, not just a successful read, identifies the platform.
if ok and game_type == 5 then
gametype_base = cand.gametype
race_globals = cand.race_globals
return
end
end
end
The check compares against 5, so it only resolves while a race gametype is loaded. A script for
another gametype would compare against that gametype’s code instead. If both probes fail, both
pointers stay nil, so check before using them.
Object Table
local function scanObjects()
local object_table_address = read_dword(0x401192 + 2)
local object_table = read_dword(object_table_address)
if object_table == 0 then return end
local object_count = read_word(object_table + 0x2E)
local first_object = read_dword(object_table + 0x34)
for i = 0, object_count - 1 do
local object_entry = first_object + i * 0xC + 0x8
local object = read_dword(object_entry)
if object ~= 0 then
-- Valid object pointer.
end
end
end
Callbacks and priorities
set_callback("event", "GlobalFnName" [, priority]). The callback function must be global (not
local or anonymous).
Chimera loads a script only when the declared clua_version is compatible:
| Script version | Example (vs 2.056) | Loads? |
|---|---|---|
| Same | 2.056 | Yes |
| Lower, same major version | 2.04 | Yes |
| Higher | 2.06 | No |
| Lower, different major version | 1.5 | No |
Event priorities
When setting a callback with set_callback, you can specify a priority as the third argument.
Priorities determine the order in which multiple scripts’ callbacks run. If multiple scripts use the
same priority, they run in script load order.
"before": runs before default callbacks"default": normal priority (used if omitted)"after": runs after default callbacks"final": runs last; any return values are ignored (useful for monitoring)
Example:
set_callback("tick", "OnTickEarly", "before")
set_callback("tick", "OnTickMain", "default")
set_callback("tick", "OnTickLate", "after")
Events
The callback names are matched after converting underscores to spaces, so "map_load" and
"map load" are equivalent.
| Callback name | When it runs | Arguments | Return value |
|---|---|---|---|
command |
A console command is about to execute | command string |
false to block; true or nothing to allow |
frame |
Each rendered frame | none | - |
preframe |
Before frame processing | none | - |
map_load |
A map is being loaded | none | - |
map_preload |
Before map loading | none | - |
precamera |
Before the camera is applied | 10 numbers: x, y, z, fov, forward.x, forward.y, forward.z, up.x, up.y, up.z |
Up to 10 numbers to override the camera (ignored at final priority) |
rcon_message |
An RCON message is received | message string |
false to block; true or nothing to allow |
tick |
Each game tick | none | - |
pretick |
Before each game tick | none | - |
unload |
Script is being unloaded | none | - |
Return values and notes:
command: returnfalseto block the command and suppress the “unknown command” error; returntrueor nothing to allow it.rcon_message: returnfalseto block the message.precamera: must return all ten camera values (see Camera manipulation).unload: fired when the script unloads (map change or manual reload). The map may already have changed by then.
Draw calls such as draw_text belong in the per-frame callbacks (preframe or frame), not
tick.
Sandbox restrictions
Sandboxed scripts (map scripts by default) run with a stripped environment. Chimera disables
io, dofile, getfenv, load, loadfile, loadstring, require, os.execute, os.exit,
os.remove, os.rename, and os.tmpname before your code runs.
Memory read/write functions (read_int, write_float, read_string8, etc.) are also restricted to
the address range 0x40000000 to 0x41B00000. Outside that range, a sandboxed script will raise an
error. If you need file access or unrestricted memory access, place the script in the global
folder instead.
The built-in filesystem functions (write_file, read_file, etc.) are still available in sandbox
mode, but they are confined to the script’s private data directory.
Embedded scripts
Mapmakers can bake Lua scripts into their .map files with tools like Harbinger. Chimera loads
these embedded scripts when load_embedded_lua = 1 is set in chimera.ini. The script runs when
players load that map, so there is no separate file to install.
Hotkeys
Chimera can bind commands to keyboard keys in chimera.ini. You can bind:
- Chimera’s native commands
- Halo’s built-in console commands
- Your own script commands (like
wpninfoorcompass)
For example, bind F5 to compass to toggle a compass HUD without typing in the console.
Global variables
Chimera exposes several read-only global variables that provide useful information about the current game state, your script’s environment, and the Chimera version.
| Variable | Description |
|---|---|
build |
Chimera build number (Git commit count). |
full_build |
Chimera version string. |
gametype |
Current gametype: "ctf", "slayer", "oddball", "king", or "race". |
local_player_index |
Local player index (0-15). May be nil until assigned after joining a server. |
map |
Currently loaded map name. |
map_is_protected |
true if the map uses protected tag data (e.g., "h3mt-foundry"). |
sandboxed |
true if the script is running in a sandboxed environment (see Sandbox restrictions). |
script_name |
Filename for global scripts, or map name for map scripts. |
script_type |
"global" or "map". |
server_type |
"none" (single player), "local" (hosting), or "dedicated" (joined a server). |
[!NOTE] Chimera has no equivalent to SAPP’s
halo_typeglobal and no function that reports which client you are on. See Probing the Platform for details on how to do this.
Server type check
function OnTick()
if server_type == "dedicated" then
console_out("Running on dedicated server. Map: (" .. map .. ")")
if gametype == "ctf" then
-- Show flag carrier info, etc.
elseif gametype == "slayer" then
-- Show kill/death stats
end
local player_index = local_player_index
if not player_index then return end -- not ready yet
for i = 0, 15 do
if i ~= player_index and get_player(i) then
-- This player is not the local player and is connected
end
end
end
end
Output and HUD
The simplest way to talk to the player is hud_message, console_out, or draw_text:
function OnTick()
hud_message("Chimera Rocks!") -- standard hud message
console_out("Chimera Rocks!") -- standard console output
console_out("Chimera Rocks!", 1.0, 0.35, 0.35) -- w/RGB colors (red, green, blue)
console_out("Chimera Rocks!", 1, 1.0, 0.35, 0.35) -- w/ARGB colors (alpha, red, green, blue)
end
function OnPreFrame()
draw_text(
"Hello, World!",
0, 460, 640, 480, -- left, top, right, bottom
"small", -- font: small, large, ticker
"right", -- alignment: left, center, right
1.0, -- alpha
0.45, -- red
0.72, -- green
1.0 -- blue
)
end
draw_text draws text inside a rectangular screen region. Supported fonts: "small", "large",
"ticker". Supported alignments: "left", "center", "right".
draw_text(text, left, top, right, bottom, font, align, a, r, g, b)
console_out accepts one argument (message only), four arguments (message, red, green, blue), or
five arguments (message, alpha, red, green, blue).
Timers
Use set_timer and stop_timer to run code after a delay or at a fixed interval instead of
counting ticks.
set_timer
Registers a repeating timer and returns a timer ID that you can later cancel with stop_timer. The
function runs at the given interval (in milliseconds) until either:
- it returns
false, or - you manually remove it with
stop_timer.
You can pass additional arguments to the callback (strings, numbers, booleans, or nil).
Syntax: set_timer(interval_ms, "function_name", [arg1, arg2, ...])
Returns: a numeric timer ID.
function ShowMessage(text)
console_out(text)
end
local timer_id = set_timer(2000, "ShowMessage", "Hello from the timer!") -- runs every 2s
stop_timer
Stops a running timer by ID. Stopping a timer that no longer exists raises an error.
Syntax: stop_timer(timer_id)
Console and scripting functions
| Function | Signature | Notes |
|---|---|---|
execute_script |
execute_script(script) |
Runs a Halo console command or Halo script expression (Lisp syntax). |
get_global |
get_global(name) |
Reads a Halo scripting global. Returns boolean, number, or integer. |
set_global |
set_global(name, value) |
Writes a Halo scripting global. The value type is validated. |
execute_chimera_command |
execute_chimera_command(command, save) |
Executes an allow-listed Chimera command. save is a boolean. |
Allow-listed Chimera commands include chimera_show_fps, chimera_block_zoom_blur,
chimera_devmode, chimera_budget, chimera_af, chimera_block_hold_f1, chimera_fov, and
chimera_mouse_sensitivity.
Examples:
-- Show the current player list in the console
execute_script("sv_players")
-- Clear the console
execute_script("cls")
-- Change the map after 5 seconds
function ChangeMap()
execute_script("map bloodgulch ctf")
return false
end
set_timer(5000, "ChangeMap")
-- Display a message using Halo script syntax
execute_script('(print "Welcome to the server!")')
-- Execute a Halo script command directly
execute_script("(object_teleport (player0) teleport_flag)")
Player and object functions
| Function | Signature | Notes |
|---|---|---|
get_player |
get_player([index]) |
Returns a player pointer (integer) or nil. With no argument, returns the local client player. |
get_dynamic_player |
get_dynamic_player([index]) |
Returns the dynamic object pointer for the local player or the player at the given RCON index, or nil. |
get_object |
get_object(id) |
Returns a dynamic object pointer for an object ID, or nil. |
delete_object |
delete_object(object_id) |
Deletes the object. Raises an error if the ID is invalid. |
spawn_object |
spawn_object(tag_id, x, y, z [, parent]) or spawn_object(class, path, x, y, z) |
Spawns an object. Returns the new object ID. |
get_tag |
get_tag(id) or get_tag(class, path) |
Returns a tag pointer (integer) or nil. |
Player pointers
Both functions use the local player when you pass no index.
local dyn_player = get_dynamic_player(player_index)
local static_player = get_player(player_index)
get_dynamic_player returns a pointer to the player’s current object. Use this for position,
velocity, health, shields, and other object data. Returns nil if the player is dead.
get_player returns a pointer to the player’s persistent player data structure. Use this for
player-related data that exists independently of the live object (team, kills, deaths, ping, name).
Field offsets for both pointers are in Memory Offsets.
Position HUD
Chimera has no read_vector3d or write_vector3d. Read the position as three floats (or use
getObjectPosition from Position and Aim):
function OnTick()
local dyn_player = get_dynamic_player()
if not dyn_player then return end -- player is dead
local x = read_float(dyn_player + 0x5C)
local y = read_float(dyn_player + 0x60)
local z = read_float(dyn_player + 0x64)
local static_player = get_player(local_player_index)
local name = static_player and readPlayerName(static_player) -- see Player name
execute_script("cls")
hud_message(string.format("Pos: %s, %.1f, %.1f, %.1f", name or "?", x, y, z))
end
Player name
Player names are stored as a wide-character string (2 bytes per character, UTF-16). Use
readWideString and readPlayerName from Wide Strings. The
name sits at static player offset 0x4 and is at most 12 characters.
local static_player = get_player(player_index)
local name = static_player and readPlayerName(static_player)
Memory access
Chimera exposes raw memory reads and writes. These are useful for working with Halo’s structures when no higher-level API exists.
Integer reads and writes
Registered names include sized aliases:
read_i8 / write_i8 read_u8 / write_u8
read_i16 / write_i16 read_u16 / write_u16
read_i32 / write_i32 read_u32 / write_u32
read_byte / write_byte
read_word / write_word
read_dword / write_dword
read_char / write_char
read_short / write_short
read_int / write_int
read_long / write_long
Signature: read_*(address) returns an integer; write_*(address, value) writes an integer.
Float reads and writes
read_f32 / write_f32 read_f64 / write_f64
read_float / write_float
read_double / write_double
Signature: read_*(address) returns a number; write_*(address, value) writes a number.
Bits and strings
| Function | Signature | Notes |
|---|---|---|
read_bit |
read_bit(address, bit_index) |
Returns 0 or 1. bit_index must be less than 32. |
write_bit |
write_bit(address, bit_index, value) |
value may be a boolean or 0/1. |
read_string8 |
read_string8(address) |
Reads a null-terminated string. |
write_string8 |
write_string8(address, string) |
Writes a null-terminated string. |
read_string and write_string are registered as aliases for read_string8 and write_string8
respectively. They call the same underlying implementation and behave identically.
In sandbox mode, addresses outside 0x40000000-0x41B00000 are rejected.
Filesystem access
Chimera registers a set of filesystem functions that are always confined to the script’s private
data directory: chimera/lua/data/<script_type>/<script_name>/
script_type is "global" or "map", and script_name is the filename without .lua. Any path
that resolves outside this directory raises a scope error.
| Function | Signature | Returns |
|---|---|---|
create_directory |
create_directory(path) |
true if created (or already exists), false otherwise |
remove_directory |
remove_directory(path) |
true if removed, false otherwise |
list_directory |
list_directory(path) |
Table of filenames (directories have a trailing \), or false |
directory_exists |
directory_exists(path) |
true if the path exists and is a directory |
write_file |
write_file(path, content [, append]) |
true on success |
read_file |
read_file(path) |
File contents as a string, or nil if it cannot be opened |
delete_file |
delete_file(path) |
true if the file was removed |
file_exists |
file_exists(path) |
true if the path is a regular file |
Global scripts can also use the standard Lua io library. Sandboxed map scripts cannot use io,
but the functions above still work.
Persistent data example:
local SAVE_FILE = "my_data.txt" -- file will appear in the script's data directory
---@param stats table Table with kills, deaths, and credits.
local function saveStats(stats)
write_file(SAVE_FILE, string.format("%d;%d;%d", stats.kills, stats.deaths, stats.credits))
end
---@return table stats Table with kills, deaths, and credits.
local function loadStats()
local content = read_file(SAVE_FILE)
if not content then return { kills = 0, deaths = 0, credits = 0 } end
local kills, deaths, credits = content:match("(%d+);(%d+);(%d+)")
return {
kills = tonumber(kills) or 0,
deaths = tonumber(deaths) or 0,
credits = tonumber(credits) or 0
}
end
Tag paths
Chimera provides two overloads of get_tag:
get_tag(class, name): looks up a tag by its class and asset path, returning the tag ID.get_tag(tag_id): converts a tag ID into a pointer to the tag entry, so you can read the tag’s internal data.
Tag ID from class and name
local ghost_tag = get_tag("vehi", "vehicles\\ghost\\ghost_mp")
if ghost_tag then
console_out("Ghost tag ID: " .. ghost_tag)
else
console_out("Ghost tag not found!")
end
Tag path from an object
Once you have an object, read its tag ID (the first dword), convert it with get_tag(tag_id), then
read the path string at offset 0x10. readTagPath in
Tag Entry Helpers does the last step.
local ghost_id = spawn_object("vehi", "vehicles\\ghost\\ghost_mp", 0, 0, 0)
local object = get_object(ghost_id) -- raw object pointer
if object then
local tag_entry = get_tag(read_dword(object))
if tag_entry then console_out(readTagPath(tag_entry)) end -- vehicles\ghost\ghost_mp
end
Object names
Gets the display name of an object (weapon, vehicle, equipment) from its tag path, using
readTagPath and getShortName from
Tag Entry Helpers. getShortName drops the directory
part, so weapons\assault rifle\assault rifle becomes assault rifle. It returns "N/A" for an
invalid object and "???" if the tag cannot be resolved.
---@param object number|nil Object memory address.
---@return string name Short object name, "N/A", or "???".
local function getObjectName(object)
if not object then return "N/A" end
local tag_entry = get_tag(read_dword(object))
if not tag_entry then return "???" end
return getShortName(readTagPath(tag_entry))
end
function OnTick()
local dyn_player = get_dynamic_player()
if not dyn_player then return end -- dead
local weapon_object_id = read_dword(dyn_player + 0x118)
if weapon_object_id == 0xFFFFFFFF then return end -- nothing held
local weapon_object = get_object(weapon_object_id)
local weapon_name = getObjectName(weapon_object) -- e.g. "assault rifle"
console_out("Weapon: " .. weapon_name)
end
Enumerating objects
To scan for all objects in the world (projectiles, vehicles, weapons, etc.), you need to read the global object table. The address and the loop are in Object Table.
Spawning objects
Chimera’s spawn_object takes either a tag ID or a class and name.
[!NOTE] Objects spawned client-side via Chimera in multiplayer sessions are local-only and will not be visible to other players. They are primarily useful for single-player custom mods and client-side testing.
-- By tag ID
local ghost_tag = get_tag("vehi", "vehicles\\ghost\\ghost_mp")
if ghost_tag then
spawn_object(ghost_tag, x, y, z)
end
-- By class and name directly
local weapon = spawn_object("weap", "weapons\\assault rifle\\assault rifle", x, y, z)
Camera manipulation
Chimera allows you to modify the camera position, orientation, and field of view before the game
renders. The example below zooms the FOV while holding a sniper. readTagPath is in
Tag Entry Helpers.
-- x,y,z: camera position
-- fov: field of view in degrees
-- ox1,oy1,oz1: camera look-at point (forward vector)
-- ox2,oy2,oz2: up vector
function OnPreCamera(x, y, z, fov, ox1, oy1, oz1, ox2, oy2, oz2)
local dyn_player = get_dynamic_player(local_player_index)
if not dyn_player then return x, y, z, fov, ox1, oy1, oz1, ox2, oy2, oz2 end
local weapon_id = read_dword(dyn_player + 0x118)
if weapon_id ~= 0xFFFFFFFF then
local weapon = get_object(weapon_id)
if weapon then
local tag_entry = get_tag(read_dword(weapon))
local weapon_name = readTagPath(tag_entry):lower()
if weapon_name:find("sniper") then
fov = 20 -- zoom
end
end
end
return x, y, z, fov, ox1, oy1, oz1, ox2, oy2, oz2
end
Zoom control
Reads a weapon’s zoom level count from its tag data (offset 0x3DA) and writes the zoom state
directly to memory. The zoom pointer is zoom_address in Memory addresses and offsets.
local ZOOM_ADDRESS = 0x815918
---@param level number Zoom level (0 for level 1, 1 for level 2, 0xFFFF to unzoom).
local function setZoomLevel(level)
local zoom_ptr = read_dword(ZOOM_ADDRESS)
if zoom_ptr and zoom_ptr ~= 0 then
write_word(zoom_ptr + 0xCC, level)
end
end
---@param weapon_object number Memory address of the weapon.
---@return number zoom_levels Number of zoom levels available on the weapon.
local function getWeaponZoomLevels(weapon_object)
local tag_id = read_dword(weapon_object)
local tag_ptr = get_tag(tag_id)
if not tag_ptr then return 0 end
local tag_data = read_dword(tag_ptr + 0x14)
return tag_data ~= 0 and read_word(tag_data + 0x3DA) or 0
end
Toggle commands
Create console toggles to enable/disable features. Type my_command in the console (press ~) to
toggle.
clua_version = 2.056
local enabled = true
local command_name = "my_command"
set_callback("command", "OnCommand")
set_callback("tick", "OnTick")
function OnCommand(cmd)
if cmd:lower() == command_name then
enabled = not enabled
console_out("My Feature " .. (enabled and "ON" or "OFF"))
return false -- prevents Halo error message
end
return true -- allow other commands
end
function OnTick()
if not enabled then return end
-- ... do something ...
end
Ping smoothing
An exponential moving average smooths jittery ping values and makes sudden latency spikes easy to spot:
local ema_ping = nil
---@param raw_ping number Latest ping sample in milliseconds.
---@return number avg, number delta Smoothed ping in milliseconds and difference from the average.
local function updatePing(raw_ping)
ema_ping = ema_ping and (ema_ping * 0.85 + raw_ping * 0.15) or raw_ping
local avg = math.floor(ema_ping + 0.5)
local delta = math.floor(raw_ping - ema_ping + 0.5)
return avg, delta
end