Hooks¶
The hook systems in Helix and Parallax both override hook.Call so that schema-level and plugin-level hook methods are dispatched before the gamemode. The dispatch order is slightly different, but the registration story — "just name a method after a hook and it runs" — is the same. Most hook names port without renaming.
Table of Contents¶
- Dispatch Order
- Registering a Hook Type
- The Hook Name Migration Table
- Schema and Module Hook Methods
- Hook Return Semantics
- Safe Run and Error Handling
- Custom Hook Families
- Pitfalls
Dispatch Order¶
Helix (inside its hook.Call override):
- Plugin hooks from
HOOKS_CACHE[name]— every function namednameon every loaded plugin's table, in iteration order. Schema[name]if defined.hook.ixCall(name, gm, ...)— the original gamemode hook chain (hook.Add listeners, gamemode methods).
Parallax (inside its hook.Call override):
- Custom hook tables registered via
ax.hook:Register(name)— iterated by table name inax.hook.stored, each table'snamemethod called. - Module hooks: for each module in
ax.module.stored, any method matchingnameis called. hook.axCall(name, gm, ...)— the original gamemode hook chain.
The meaningful difference: Parallax has an explicit registration step for custom hook families (ax.hook:Register("SCHEMA") makes SCHEMA:HookName methods eligible for dispatch), while Helix implicitly supports Schema as a hard-coded second-priority table. SCHEMA is registered automatically by Parallax's framework boot, so you only need to call ax.hook:Register yourself if you want to introduce a new family (e.g. INVENTORY, FACTION) with its own dispatch slot.
The practical dispatch equivalence:
-- Helix
function PLUGIN:PlayerSay(client, text)
-- dispatched from HOOKS_CACHE["PlayerSay"]
end
function Schema:PlayerSay(client, text)
-- dispatched from the Schema table
end
-- Parallax
function MODULE:PlayerSay(client, text)
-- dispatched from ax.module.stored iteration
end
function SCHEMA:PlayerSay(client, text)
-- dispatched from ax.hook.stored.SCHEMA
end
Both are functionally equivalent to hook.Add("PlayerSay", uniqueName, fn) with framework-managed lifecycle.
Registering a Hook Type¶
Helix has no concept of custom hook types — the Schema global is hardcoded as the second dispatch priority. To add your own named family, you would need to edit the framework.
Parallax exposes ax.hook:Register(name) for this:
-- Create a new hook family
ax.hook:Register("INVENTORY")
-- Now any global table named INVENTORY gets dispatched
INVENTORY = INVENTORY or {}
function INVENTORY:OnPlayerSpawn(client)
print("INVENTORY hook fired")
end
The framework automatically registers SCHEMA. Modules do not get their own family — their hooks are dispatched via the module iteration path.
The Hook Name Migration Table¶
Most Helix hook names exist unchanged in Parallax. The following table covers everything that's either renamed or has a meaningful signature change. If a hook is not listed here, assume the Helix name carries over verbatim.
Character lifecycle¶
| Helix | Parallax | Notes |
|---|---|---|
CharacterLoaded(char) |
OnCharacterLoaded(char) |
Name prefix added. |
PlayerLoadedCharacter(client, new, old) |
PostPlayerLoadedCharacter(client, new, old) |
Rename to Post prefix. |
OnCharacterCreated(client, char) |
OnCharacterCreated(client, char) |
Same. |
OnCharacterDelete(client, id) |
PreCharacterDeleted(client, char) |
Receives character object, not ID. Fired before deletion. |
OnCharacterDisconnect(client, char) |
OnCharacterDisconnected(char) |
No client arg; pull from char:GetPlayer() (which may be invalid). |
CanPlayerCreateCharacter(client, payload) |
CanPlayerCreateCharacter(client, payload) |
Same. |
CanPlayerUseCharacter(client, char) |
CanPlayerUseCharacter(client, char) |
Same. |
ix.char.HookVar(name, ...) per-var callback |
OnCharacterVarChanged(char, name, value) |
Single generic hook; filter on name. |
Player and loadout¶
| Helix | Parallax | Notes |
|---|---|---|
PlayerLoadout(client) |
PlayerLoadout(client) |
Same. |
PostPlayerLoadout(client) |
PostPlayerLoadout(client) |
Same. |
ShouldSpawnClientRagdoll(client) |
ShouldSpawnClientRagdoll(client) |
Same. |
ShouldRemoveRagdollOnDeath(client) |
(no direct equivalent — customize via OnRagdollCreated) |
|
GetPlayerDeathSound(client) |
GetPlayerDeathSound(client) |
Same. |
GetPlayerPainSound(client) |
GetPlayerPainSound(client, attacker, hp, dmg) |
Parallax passes extra context args. |
| (no equivalent) | GetPlayerRespawnSound(client, attacker, dmg) |
New. |
ShouldPlayerDrowned(client) |
(implement via standard GetMaxHealth/damage logic) |
|
PlayerWeaponChanged(client, weapon) |
PlayerWeaponChanged(client, weapon) |
Same. |
Factions and classes¶
| Helix | Parallax | Notes |
|---|---|---|
(no dedicated hook; FACTION:OnTransferred) |
OnPlayerBecameFaction(client, faction, oldFaction) |
Fires after a successful faction change. |
CanPlayerJoinClass(client, class, info) |
CanPlayerBecomeClass(classTable, client) |
Argument order inverted; first arg is class table. |
| (no equivalent) | CanPlayerBecomeFaction(factionTable, client) |
Central validation point. |
GetSalaryAmount(client, faction) |
(no equivalent — salary is module-level) | |
CanPlayerEarnSalary(client, faction) |
(same) |
Items and inventory¶
| Helix | Parallax | Notes |
|---|---|---|
CanPlayerInteractItem(client, action, item, data) |
CanPlayerInteractItem(client, item, action, context) |
Argument order differs. |
CanPlayerDropItem(client, item) |
CanPlayerDropItem(client, item) |
Same — use CanPlayerInteractItem with action == "drop" in Parallax as the preferred path. |
CanPlayerTakeItem(client, item) |
CanPlayerTakeItem(client, item) |
Same — likewise prefer the unified interact hook. |
CanPlayerCombineItem(client, item, other) |
(no equivalent — model as interact-with-context) | |
CanPlayerEquipItem(client, item) |
CanPlayerEquipItem(client, item) |
Same. |
CanPlayerUnequipItem(client, item) |
CanPlayerUnequipItem(client, item) |
Same. |
InventoryItemAdded(inventory, item) |
OnInventoryItemAdded(inventory, item) |
Prefix added. |
InventoryItemRemoved(inventory, item) |
OnInventoryItemRemoved(inventory, item) |
Prefix added. |
World and gameplay¶
| Helix | Parallax | Notes |
|---|---|---|
CanPlayerUseDoor(client, door) |
CanPlayerUseDoor(client, door) |
Same. |
CanPlayerUseBusiness(client, uid) |
(no first-class business system; implement as a module) | |
PlayerUseDoor(client, door) |
PlayerUseDoor(client, door) |
Same. |
GetDefaultCharacterName(client, faction) |
GetDefaultCharacterName(client, faction) |
Same. |
CanPlayerSpray(client) |
(standard PlayerSpray) |
Framework lifecycle¶
| Helix | Parallax | Notes |
|---|---|---|
InitializedSchema() |
OnSchemaLoaded() |
Rename. |
InitializedPlugins() |
OnModuleLoaded(module) fires per-module |
No "all modules loaded" hook; listen to each or use the OnSchemaLoaded hook as a "everything is up" marker. |
PluginLoaded(uid, plugin) |
OnModuleLoaded(module) |
Same purpose. |
PluginUnloaded(uid) |
(no equivalent) | |
PluginShouldLoad(uid) |
Return false from module's boot.lua. |
Different mechanism. |
DoPluginIncludes(path, plugin) |
(no equivalent — module loader walks predefined dirs) | |
SaveData() |
(no equivalent) | Use explicit ax.data:Set calls or ax.database. |
LoadData() |
Use MODULE:OnLoaded() |
Load state during OnLoaded. |
PostLoadData() |
Use the tail of OnLoaded |
|
PersistenceSave() |
(no equivalent — schema-specific) |
Chat¶
| Helix | Parallax | Notes |
|---|---|---|
PrePlayerMessageSend(chatType, client, text, anonymous) |
CanPlayerSendMessage(speaker, chatType, text, data) |
Rename and restructure. |
PostPlayerSay(client, type, message, anon) |
PostPlayerSay(client, type, message, anon) |
Same. |
OnChatReceived(chatType, speaker, listener, text) |
CanPlayerReceiveMessage(listener, speaker, chatType, text, data) |
Similar but permission-shaped. |
UI / inventory views¶
| Helix | Parallax | Notes |
|---|---|---|
CanPlayerViewInventory() |
CanPlayerViewInventory() |
Same, client-side. |
OnLocalPlayerCreated() |
Use InitPostEntity or similar |
Schema and Module Hook Methods¶
Declare hooks on SCHEMA or MODULE by naming methods after the hook:
-- In <your-schema>/gamemode/schema/hooks/sv_hooks.lua
function SCHEMA:CanPlayerUseDoor(client, door)
if ( door:GetNWBool("locked") ) then
return false
end
end
-- In <your-schema>/gamemode/modules/example/boot.lua
function MODULE:CanPlayerUseDoor(client, door)
if ( door:GetClass() == "func_door_rotating" ) then
return false
end
end
Both fire on dispatch, in the order described above. Dispatch short-circuits on any non-nil return — if SCHEMA:CanPlayerUseDoor returns false, MODULE:CanPlayerUseDoor does not run. If you need guaranteed ordering across schema and modules, design around one controlling authority.
Hook Return Semantics¶
The convention in both frameworks:
- Return
nil(or nothing) to allow the hook chain to continue. - Return
falseto deny / block. - Return
true(or any truthy value) to allow / succeed / short-circuit.
Action-gating hooks (CanPlayer...) expect:
nilor no return → no opinion, let the next handler decide.false→ deny. May optionally return a reason string as a second value.true→ force-allow, overriding other handlers.
Action-reporting hooks (On..., Post...) return nothing; their return values are ignored.
The one place these semantics can bite: Helix's CanPlayerInteractItem could return false with a reason as a second value that was shown to the user. Parallax preserves this — always return the reason string when blocking, or callers have to guess.
Safe Run and Error Handling¶
Helix ships hook.SafeRun(name, ...) — a pcall-wrapped variant that collects per-plugin errors and returns them. It's used internally for LoadData and PostLoadData so that one misbehaving plugin doesn't bring down persistence.
Parallax does not expose a safe-run variant. If you need error-tolerant dispatch, wrap your own:
-- In your schema or module
local function SafeRun(name, ...)
local success, result = pcall(hook.Run, name, ...)
if ( !success ) then
ax.util:PrintError("Hook " .. name .. " errored: " .. tostring(result))
return nil
end
return result
end
In practice, most Helix SafeRun call sites were for SaveData / LoadData, which don't exist in Parallax; when porting plugins that had bespoke safe-run logic for state persistence, the replacement is usually a direct ax.data or ax.database call wrapped in pcall.
Custom Hook Families¶
If your schema builds large subsystems that want their own named hook family — this was rarely done in Helix plugins because there was no registration API, but it's straightforward in Parallax — create a global table and register it:
-- In <your-schema>/gamemode/schema/libraries/sh_quests.lua
QUEST = QUEST or {}
ax.hook:Register("QUEST")
-- Elsewhere:
function QUEST:OnPlayerSpawn(client)
-- Quest system reacts to every PlayerSpawn
end
function QUEST:OnPlayerKilled(victim, killer)
-- Quest system reacts to every death
end
Any standard GMod hook will dispatch to QUEST:HookName before reaching the gamemode. Parallax inserts the call in hook.Call's override loop.
Pitfalls¶
- Assuming hook ordering. Both frameworks iterate plugin/module hooks in
pairsorder — not insertion order. If your schema depends on one plugin running before another, make that explicit by having the later plugin listen to a specific earlier event, not by assuming alphabetical order. - Short-circuit by accident. Returning
falsefrom a logging hook will block subsequent listeners. Double-check your return values — hooks likePlayerSaysometimes accumulate listeners across many modules and one early-return breaks everything. - Schema vs module dispatch priority. Parallax dispatches registered hook tables (
SCHEMA, or anything you registered) before modules. A Helix schema-level guard could be overridden by a plugin returning a different value; in Parallax the schema wins unless the module runs first by being in a differently-prefixed family. - Hook-name typos. Neither framework validates hook names at registration time. Look at the main framework docs (
parallax/manuals/05-API_REFERENCE.md) and cross-reference the Helix source to confirm you've got the right name. - Mixing
hook.Addand method-style. Both work and both get dispatched. Usehook.Addfor truly ad-hoc listeners (utility code in a one-off file); useSCHEMA:Name/MODULE:Namefor anything belonging to a coherent subsystem. Don't use both for the same logical listener.
Next: 08-inventory.md