Core Systems¶
Table of Contents¶
- Hook System (
ax.hook) - Faction System (
ax.faction) - Item System (
ax.item) - Character System (
ax.character) - Inventory System (
ax.inventory) - Command System (
ax.command) - Module System (
ax.module)
Hook System (ax.hook)¶
The hook system extends Garry's Mod's native hook system with custom hook types and proper dispatch order.
Registering Custom Hooks¶
-- Register a new hook type
ax.hook:Register("SCHEMA")
-- Now SCHEMA:HookName() functions will be called
function SCHEMA:OnPlayerSpawn(player)
-- Schema-specific spawn logic
end
Hook Dispatch Order¶
Hooks are dispatched in this order:
- Custom Hook Tables (e.g.,
SCHEMA,MODULE) - Module Methods (e.g.,
MODULE:HookName) - Gamemode (standard GMod hooks)
This allows schemas and modules to intercept hooks before they reach gamemode.
Schema Hooks¶
Schema hooks are defined in schema/hooks/:
-- schema/hooks/sh_hooks.lua
function SCHEMA:EntityEmitSound(data)
local ent = data.Entity
if !IsValid(ent) then return end
if ent:GetClass() == "npc_combine_camera" then
data.SoundLevel = 60
return true -- Override sound
end
end
Module Hooks¶
Modules can also define hooks:
-- modules/my_module/sh_hooks.lua
MODULE = MODULE or {}
function MODULE:OnPlayerSpawn(player)
-- Module-specific spawn logic
end
return MODULE
Available Hooks¶
Standard GMod hooks work as expected. Custom Parallax hooks include:
OnSchemaLoaded()- Called when schema finishes loadingOnHookRegistered(name)- Called when a new hook type is registeredCharacterDataChanged(char, name, key, value)- Character data modifiedCanBecomeFaction(faction, client)- Validate faction changesOnCharacterVarChanged(char, name, value)- Character variable changed
Hook API¶
-- Register custom hook type
ax.hook:Register("CUSTOM")
-- Define hook function
CUSTOM:OnMyEvent(data)
print("Event:", data)
end
-- Run hook
hook.Run("OnMyEvent", {value = "test"})
Faction System (ax.faction)¶
Factions represent player teams/groups in roleplay setting.
Faction Structure¶
FACTION.name = "Citizen"
FACTION.description = "Ordinary humans trying to survive..."
FACTION.color = Color(150, 150, 150)
FACTION.isDefault = true
FACTION.image = ax.util:GetMaterial("parallax/hl2rp/banners/citizen.png")
FACTION.models = {
"models/humans/group01/male_01.mdl",
"models/humans/group01/female_01.mdl",
-- ... more models
}
Creating a Faction¶
Create a file in schema/factions/:
-- schema/factions/sh_citizen.lua
FACTION.name = "Citizen"
FACTION.description = "The oppressed populace under Combine rule."
FACTION.color = Color(150, 150, 150)
FACTION.isDefault = true -- New players start with this faction
FACTION.models = {
"models/humans/group01/male_01.mdl",
"models/humans/group01/female_01.mdl",
-- ... add more models
}
-- Precache models for performance
for i = 1, #FACTION.models do
util.PrecacheModel(FACTION.models[i])
end
-- Optional: Custom validation
function FACTION:CanBecome(client)
if client:GetCharacter():GetVar("banned") then
return false, "You are banned from this faction"
end
return true
end
Faction Properties¶
| Property | Type | Description |
|---|---|---|
name |
string | Display name |
description |
string | Lore/description |
color |
Color | Team color |
isDefault |
boolean | Starting faction for new characters |
image |
ITexture | Banner/background image |
models |
table | Available player models |
CanBecome |
function | Custom join validation |
Faction API¶
-- Get a faction by ID, index, or name
local faction = ax.faction:Get("citizen")
local faction = ax.faction:Get(1)
-- Check if player can join faction
local canJoin, reason = ax.faction:CanBecome("citizen", client)
-- Get all factions
local allFactions = ax.faction:GetAll()
-- Check if faction exists
if ax.faction:IsValid("citizen") then
-- Faction exists
end
Global Faction Constants¶
After loading, factions are available as globals:
FACTION_CITIZEN = 1
FACTION_MPF = 2
FACTION_OTA = 3
-- etc.
-- Use in code
if character:GetFaction() == FACTION_CITIZEN then
-- Player is a citizen
end
Faction Methods¶
-- Get models (with default fallback)
local models = faction:GetModels()
-- Check if player can join
local canJoin, reason = faction:CanBecome(client)
Item System (ax.item)¶
The item system uses a three-tier inheritance model: Base Items → Regular Items → Instances.
Item Inheritance¶
Base Item (abstract template)
↓
Regular Item (inherits from base)
↓
Item Instance (actual game object with ID)
Creating a Base Item¶
Base items provide common functionality for related items:
-- schema/items/base/sh_weapon.lua
ITEM.name = "Weapon Base"
ITEM.description = "A base weapon template"
ITEM.model = "models/weapons/w_pistol.mdl"
ITEM.weight = 2.0
ITEM.category = "Weapons"
ITEM.isBase = true
ITEM:AddAction("drop", { ... })
-- Common weapon functionality
function ITEM:CanUse(client)
return client:Alive()
end
function ITEM:OnDrop(client, position)
-- Custom drop logic
end
Creating a Regular Item¶
Regular items can inherit from base items or stand alone:
-- schema/items/weapons/sh_pistol.lua
ITEM.name = "9mm Pistol"
ITEM.description = "A standard Combine-issued sidearm."
ITEM.model = "models/weapons/w_pistol.mdl"
ITEM.weight = 1.5
ITEM.category = "Weapons"
ITEM.base = "weapon" -- Inherit from base
-- Override base properties
ITEM.description = "A reliable 9mm sidearm."
-- Add custom action
ITEM:AddAction("inspect", {
name = "Inspect",
icon = "icon16/magnifier.png",
order = 2,
CanUse = function(this, client)
return true
end,
OnRun = function(action, item, client)
client:Notify("This weapon is in good condition.")
return false -- Don't consume the item
end
})
Item Actions¶
Actions are interactable functions attached to items:
ITEM:AddAction("drop", {
name = "Drop",
icon = "icon16/arrow_down.png",
order = 1, -- Lower numbers appear first
CanUse = function(this, client)
-- Can this action be used?
return true
end,
OnRun = function(action, item, client)
-- Action logic
return true -- Return true to consume item, false to keep it
end
})
Common Actions:
drop- Drop item to worlduse- Use/consume itemeat- Eat food itemequip- Equip weapon/clothing
Item Properties¶
| Property | Type | Description |
|---|---|---|
name |
string | Display name |
description |
string | Item description |
model |
Model | World model |
weight |
number | Weight in kg |
category |
string | UI category |
base |
string | Base item to inherit from |
isBase |
boolean | Is this a base item? |
Item API¶
-- Get item definition by class
local itemDef = ax.item:Get("pistol")
-- Get item instance by ID
local itemInstance = ax.item:Get(123)
-- Spawn item in world (server)
ax.item:Spawn("pistol", Vector(0, 0, 0), Angle(0, 0, 0), function(entity, itemObj)
-- Callback when spawned
end)
-- Transfer item between inventories (server)
local success, reason = ax.item:Transfer(item, fromInv, toInv, function(success)
if success then
print("Transfer complete")
end
end)
-- Get item actions
local actions = ax.item:GetActionsForClass("pistol")
Directory Organization¶
schema/items/
├── base/ -- Base items (templates)
│ ├── sh_weapon.lua
│ ├── sh_food.lua
│ └── sh_clothing.lua
├── weapons/ -- Inherits from base/weapon
│ ├── sh_pistol.lua
│ └── sh_rifle.lua
├── food/ -- Inherits from base/food
│ ├── sh_bread.lua
│ └── sh_water.lua
└── sh_scrap_metal.lua -- Standalone item
Items in subdirectories automatically inherit from matching base items in base/.
Item Methods¶
-- Get item weight
local weight = item:GetWeight()
-- Check if player can use
local canUse = item:CanUse(client)
-- On drop callback
function ITEM:OnDrop(client, position)
-- Custom drop logic
end
Character System (ax.character)¶
Characters represent player avatars with persistent data.
Character Variables¶
Register custom character variables:
-- Schema: schema/core/sh_character.lua
ax.character:RegisterVar("description", {
default = "",
fieldType = ax.type.text,
bNoGetter = false,
bNoSetter = false
})
ax.character:RegisterVar("health", {
default = 100,
fieldType = ax.type.number
})
ax.character:RegisterVar("data", {
fieldType = ax.type.data -- JSON object
})
-- With custom callbacks
ax.character:RegisterVar("faction", {
default = FACTION_CITIZEN,
fieldType = ax.type.number,
Get = function(char)
return char.vars.faction or FACTION_CITIZEN
end,
Set = function(char, faction)
char.vars.faction = faction
-- Additional logic
end,
changed = function(char, newValue, oldValue)
print("Faction changed from " .. tostring(oldValue) .. " to " .. tostring(newValue))
end
})
Character Meta-Table Extension¶
Add methods to all characters:
-- schema/meta/sh_character.lua
function ax.character.meta:IsCombine()
return self:GetFaction() == FACTION_MPF or self:GetFaction() == FACTION_OTA
end
function ax.character.meta:IsMetrocop()
return self:GetFaction() == FACTION_MPF
end
function ax.character.meta:IsCitizen()
return self:GetFaction() == FACTION_CITIZEN
end
-- Usage
local char = client:GetCharacter()
if char:IsCombine() then
print("Player is Combine")
end
Character API¶
-- Get character by ID
local character = ax.character:Get(123)
-- Get character variable (with fallback)
local description = character:GetDescription("No description")
local faction = character:GetFaction()
-- Set character variable
character:SetDescription("A brave resistance fighter")
character:SetFaction(FACTION_CITIZEN)
-- With options
character:SetDescription("Updated", {
bNoNetworking = true, -- Don't network this change
bNoDBUpdate = true, -- Don't update database
recipients = {client} -- Only send to specific player
})
-- Access character data (JSON object)
local data = character:GetData()
data.customField = "value"
character:SetData("customField", "value")
Character Data Fields¶
Built-in character variables include:
name- Character namedescription- Character descriptionfaction- Faction IDclass- Class IDrank- Rank IDinventory- Inventory IDhealth- Health valuedata- Custom JSON data object
Character Methods¶
-- Get character ID
local id = character:GetID()
-- Get character name
local name = character:GetName()
-- Get inventory ID
local invID = character:GetInventoryID()
-- Get player who owns character
local owner = character:GetOwner()
Inventory System (ax.inventory)¶
Inventories store items for characters, containers, item-owned bags, or the world
(inventory ID 0). Every inventory instance has a type (typeID) that decides
how items are addressed and how capacity is checked, and an owner (ownerKind +
ownerID) that decides who's allowed to modify it.
Inventory Structure¶
inventory = {
id = 1,
typeID = "weight", -- registry key; decides addressing + capacity rules
ownerKind = "character", -- "character", "item", "entity", or nil (legacy/ownerless)
ownerID = 4,
data = {}, -- per-instance type parameters (e.g. grid size, slot set)
items = {
[123] = itemInstance1,
[124] = itemInstance2,
},
maxWeight = 30.0, -- only meaningful for the "weight" type
receivers = {player1, player2}
}
Type Registry¶
Every inventory is created against a registered type. The default weight type
reproduces the framework's original behavior (a flat list capped by total weight,
no addressing) so untouched schemas keep working unmodified. Other types (grid,
slot, ...) add coordinate/slot addressing and their own capacity rule on top:
ax.inventory:RegisterType("weight", {
CanReceiveItem = function(inventory, item, placement) ... end,
CanRemoveItem = function(inventory, item) ... end,
-- Addressed types (grid/slot) additionally implement:
-- CanItemFit(...), FindEmptySlot(...), ResolvePlacement(self, w, h, explicitPlacement, ignoreItem)
})
-- Get the type definition backing an inventory instance
local typeDef = ax.inventory:GetType(inventory)
A type with no addressing (the default) skips placement validation entirely in
Transfer and keeps writing an empty placement blob - this is what makes the
weight type back-compatible.
Ownership and Access¶
ownerKind/ownerID are never hand-typed strings above the database layer -
they're resolved through registered owner resolvers:
ax.inventory:RegisterOwnerResolver({
kind = "character",
checkOwner = function(owner) return istable(owner) and owner.GetID != nil end,
getOwner = function(owner) return owner:GetID() end,
})
The core enforces exactly one built-in access rule - an owner-character may
modify their own inventories - and otherwise defers to the type's own
CanAccess rule (distance, locks, faction/rank, ...):
if ax.inventory:CanAccess(inventory, client) then
-- client may Transfer items into/out of this inventory
end
"Can access" (may modify) and "is a receiver" (gets synced state) are separate concepts - a player can see an inventory without being allowed to change it.
Inventory API¶
-- Get inventory by ID
local inventory = ax.inventory:Get(1)
-- Get inventory weight
local weight = inventory:GetWeight()
-- Get max weight
local maxWeight = inventory:GetMaxWeight()
-- Add item to inventory
local success, reason = inventory:AddItem("pistol", {dataField = "value"})
-- Remove item from inventory
local success, reason = inventory:RemoveItem(itemID)
-- Check if can receive player
if inventory:HasReceiver(client) then
-- Client can see this inventory
end
-- Get all players who can see this inventory
local receivers = inventory:GetReceivers()
Transferring Items¶
ax.item:Transfer is the single transaction that moves or repositions an item -
drag-and-drop, equip, world drop/pickup, and reward grants all go through it.
There is no other way to change where an item lives.
-- Transfer(item, fromInventory, toInventory, placement, client, callback)
-- fromInventory/toInventory: instance, id, or 0/nil for the world
-- placement: nil to auto-place (addressed types only), or { gridX, gridY } / { slotID }
-- client: the player initiating the transfer, or nil for a trusted/system call
-- (reward grants, admin commands) - nil skips the access gate
ax.item:Transfer(item, fromInventory, toInventory, nil, client, function(success, reasonCode)
if !success then
-- reasonCode is a phrase key, e.g. "inventory.reason.no_space" - map it
-- through ax.localization:GetPhrase for player-facing text, don't show
-- reasonCode raw
return
end
end)
Checks run in a fixed order: client access to both endpoints -> anti-dupe lock on
the item -> item-level CanTransferItem hook -> source type's CanRemoveItem ->
destination type's CanReceiveItem (plus weight capacity as a universal
baseline) -> placement resolution (skipped for non-addressed types) -> depth-1
nesting (an item that owns an inventory can never end up inside another
item-owned inventory). The database write happens before any in-memory state
changes, so a DB failure never leaves memory and DB diverged.
World Inventory¶
Inventory ID 0 represents the world (dropped items):
-- Transfer to world (drop item)
ax.item:Transfer(item, playerInv, 0, nil, client, function(success)
if success then
print("Item dropped")
end
end)
-- Transfer from world (pickup item)
ax.item:Transfer(item, 0, playerInv, nil, client, function(success)
if success then
print("Item picked up")
end
end)
Reason Codes¶
A failed access check or Transfer never returns a raw English string - it
returns a phrase key from a small, growing set (inventory.reason.no_space,
wrong_slot, no_access, locked, nesting, too_far, invalid, ...).
Schemas map these through ax.localization:GetPhrase for player-facing text;
treat the key itself as an enum, not display copy.
Inventory Synchronization¶
A full snapshot of the inventory is sent on every change - there is no delta
protocol. The payload includes the type and owner fields as additive tail
arguments, so a receiver that predates the type registry still gets a working
default (weight, {}) sync:
-- Sync inventory to all receivers (server)
ax.inventory:Sync(inventoryID)
-- Sync to specific recipients
ax.inventory:Sync(inventoryID, {client1, client2})
Creating Inventories (Server)¶
ax.inventory:Create({
owner = character, -- resolved to (ownerKind, ownerID) via the registered resolvers
typeID = "weight", -- defaults to "weight" if omitted (back-compat)
maxWeight = 50.0
}, function(inventory)
print("Created inventory:", inventory.id)
end)
Inventory Methods¶
-- Get inventory owner
local owner = inventory:GetOwner()
-- Add receiver (player who can see inventory)
inventory:AddReceiver(client)
-- Remove receiver
inventory:RemoveReceiver(client)
-- Check if full
local isFull = inventory:IsFull()
Command System (ax.command)¶
The command system provides validated chat and console commands.
Creating a Command¶
ax.command:Add("charcreate", {
description = "Create a new character",
arguments = {
{
name = "name",
type = ax.type.string,
required = true
},
{
name = "description",
type = ax.type.text,
required = true
}
},
adminOnly = false,
OnRun = function(this, client, name, description)
-- Command logic
ax.character:Create(client, {
name = name,
description = description
})
return true
end
})
Command Arguments¶
Supported argument types:
-- String
{
name = "message",
type = ax.type.string,
optional = true
}
-- Text (consumes rest of line)
{
name = "description",
type = ax.type.text
}
-- Number with validation
{
name = "amount",
type = ax.type.number,
min = 1,
max = 100,
decimals = 2
}
-- Boolean
{
name = "enabled",
type = ax.type.bool
}
-- Player lookup
{
name = "target",
type = ax.type.player
}
-- Character lookup
{
name = "character",
type = ax.type.character
}
-- String with choices
{
name = "faction",
type = ax.type.string,
choices = {
citizen = true,
mpf = true,
ota = true
}
}
Command Permissions¶
-- Admin only
ax.command:Add("ban", {
adminOnly = true,
OnRun = function(this, client, ...)
-- Admin command
end
})
-- Superadmin only
ax.command:Add("rcon", {
superAdminOnly = true,
OnRun = function(this, client, ...)
-- Superadmin command
end
})
-- Custom validation
ax.command:Add("custom", {
CanRun = function(this, caller)
if caller:GetFaction() != FACTION_MPF then
return false, "Only MPF can use this command"
end
return true
end,
OnRun = function(this, client, ...)
-- Custom validation logic
end
})
-- Console commands
ax.command:Add("serverinfo", {
bAllowConsole = true,
OnConsole = function(this, ...)
-- Console-specific handler
end
})
Command Aliases¶
ax.command:Add("privatemessage", {
alias = {"pm", "whisper", "w"},
description = "Send a private message",
OnRun = function(this, client, target, message)
-- Can be called with /pm, /privatemessage, /whisper, or /w
end
})
Command API¶
-- Find command
local command = ax.command:Find("pm")
-- Find all matching commands
local matches = ax.command:FindAll("p")
-- Get closest match
local closest = ax.command:FindClosest("p")
-- Check access
local canRun, reason = ax.command:HasAccess(client, commandDef)
-- Get all commands
local allCommands = ax.command:GetAll()
-- Generate help string
local help = ax.command:Help("pm")
-- Returns: "privatemessage <target> <message>"
Client-Side Usage¶
-- Send command from client (chat)
ax.command:Send("/pm player1 Hello there")
-- Console usage
ax_command "charcreate John Doe A brave soldier"
Module System (ax.module)¶
Modules allow creating reusable add-ons that work across schemas.
Module Structure¶
modules/my_module/
├── boot.lua -- Module definition
├── sh_hooks.lua -- Module hooks
├── factions/ -- Module factions
│ └── sh_custom.lua
├── items/ -- Module items
│ └── sh_item.lua
└── ... -- Other module content
Creating a Module¶
-- modules/my_module/boot.lua
MODULE = MODULE or {}
MODULE.name = "My Module"
MODULE.description = "A helpful module"
MODULE.author = "YourName"
-- Module initialization
function MODULE:Initialize()
-- Setup code
ax.util:PrintSuccess("Module '" .. MODULE.name .. "' loaded!")
end
-- Module hooks
function MODULE:OnSchemaLoaded()
-- Schema loaded
end
function MODULE:OnPlayerSpawn(player)
-- Player spawned
end
return MODULE
Module Capabilities¶
Modules can add:
- Factions (
factions/directory) - Items (
items/directory) - Hooks (
sh_hooks.lua,cl_hooks.lua,sv_hooks.lua) - Libraries (
libraries/directory) - Meta-tables (
meta/directory) - Custom systems
Module Loading Order¶
Modules are loaded after schema initialization:
- Framework loads
- Schema loads
- Schema modules load
- Framework modules load
Module Hooks¶
-- modules/my_module/sh_hooks.lua
MODULE = MODULE or {}
function MODULE:OnSchemaLoaded()
print("Schema loaded, module is active")
end
function MODULE:PlayerSpawn(client)
print(client:Nick() .. " spawned")
end
return MODULE
Adding Module Content¶
-- modules/my_module/factions/sh_custom.lua
FACTION.name = "Custom Faction"
FACTION.description = "Added by module"
FACTION.color = Color(255, 0, 0)
-- modules/my_module/items/sh_module_item.lua
ITEM.name = "Module Item"
ITEM.description = "Added by module"
ITEM.model = "models/props_junk/wood_crate001a.mdl"
ITEM.weight = 1.0
Continue to: Schema Development