Schema Development¶
Table of Contents¶
- Creating a New Schema
- Extending Framework Systems
- Schema-Specific Hooks
- Content Organization
- Schema Configuration
Creating a New Schema¶
Directory Structure¶
your-schema/
├── gamemode/
│ ├── init.lua -- Server: DeriveGamemode("parallax")
│ ├── cl_init.lua -- Client: DeriveGamemode("parallax")
│ └── schema/
│ ├── boot.lua -- Schema definition
│ ├── config/ -- Configuration
│ ├── factions/ -- Faction definitions
│ ├── items/ -- Item definitions
│ ├── hooks/ -- Schema hooks
│ ├── meta/ -- Meta-tables
│ └── languages/ -- Localization
└── content/ -- Custom content
└── materials/ -- Custom materials
Schema Boot File¶
-- schema/boot.lua
SCHEMA.name = "My Roleplay Schema"
SCHEMA.description = "A custom roleplay schema based on Parallax."
SCHEMA.author = "YourName"
Schema Init Files¶
Server (gamemode/init.lua):
AddCSLuaFile("cl_init.lua")
DeriveGamemode("parallax")
-- Add custom content
resource.AddFile("materials/my_schema/banner.png")
-- Add workshop content
resource.AddWorkshop(123456789)
Client (gamemode/cl_init.lua):
Minimal Working Schema¶
For a minimal schema, you need:
-
gamemode/init.lua(server): -
gamemode/cl_init.lua(client): -
gamemode/schema/boot.lua:
This is enough to load the schema! You can then add content as needed.
Extending Framework Systems¶
Extending Factions¶
Add custom faction properties:
-- schema/factions/sh_custom.lua
FACTION.name = "Custom Faction"
FACTION.description = "A faction with custom logic."
FACTION.color = Color(255, 0, 0)
FACTION.customField = "custom value"
function FACTION:CustomMethod()
-- Custom faction logic
print("Custom method called!")
end
Extending Characters¶
Add custom character methods:
-- schema/meta/sh_character.lua
function ax.character.meta:GetCustomData()
return self.vars.customData or {}
end
function ax.character.meta:SetCustomData(data)
self.vars.customData = data
-- Additional logic
end
-- Example: Helper methods
function ax.character.meta:IsVIP()
return self:GetVar("vip", false)
end
function ax.character.meta:GetPlaytime()
return self:GetVar("playtime", 0)
end
Extending Items¶
Create custom item types:
-- schema/items/base/sh_consumable.lua
ITEM.name = "Consumable Base"
ITEM.isBase = true
function ITEM:CanUse(client)
return client:Alive()
end
function ITEM:Consume(client)
-- Override in derived items
return true
end
-- Derived item
-- schema/items/food/sh_apple.lua
ITEM.name = "Apple"
ITEM.base = "consumable"
ITEM.description = "A fresh apple."
function ITEM:Consume(client)
client:SetHealth(client:Health() + 5)
return true
end
Extending Inventories¶
Add custom inventory functionality:
-- schema/meta/sh_inventory.lua
function ax.inventory.meta:GetItemCount(class)
local count = 0
for _, item in pairs(self.items) do
if item.class == class then
count = count + 1
end
end
return count
end
function ax.inventory.meta:HasItem(class)
for _, item in pairs(self.items) do
if item.class == class then
return true, item
end
end
return false, nil
end
Extending Commands¶
Add custom command helpers:
-- schema/core/sh_commands.lua
-- Helper function for notifications
local function Notify(client, message, type)
type = type or "info"
ax.util:Print("[" .. string.upper(type) .. "] " .. message, Color(255, 255, 255))
end
-- Custom command with helpers
ax.command:Add("customcmd", {
description = "A custom command",
OnRun = function(this, client, arg1)
Notify(client, "You ran customcmd with: " .. tostring(arg1))
return true
end
})
Schema-Specific Hooks¶
Override framework behavior:
Entity Hooks¶
-- schema/hooks/sh_hooks.lua
function SCHEMA:PlayerCanPickupItem(client, item)
if item:GetClass() == "weapon_rifle" and !client:IsAdmin() then
return false -- Prevent pickup
end
return true
end
function SCHEMA:EntityEmitSound(data)
local ent = data.Entity
if !IsValid(ent) then return end
if ent:GetClass() == "npc_combine_camera" then
-- Reduce camera sound volume
data.SoundLevel = 60
return true
end
-- Reduce NPC sounds
if ent:IsNPC() then
data.SoundLevel = data.SoundLevel - 15
return true
end
end
Player Hooks¶
function SCHEMA:PlayerSpawn(client)
local char = client:GetCharacter()
if char:IsCombine() then
-- Combine spawn logic
client:SetModel(char:GetModel())
client:SetMaxHealth(150)
client:SetHealth(150)
client:SetArmor(50)
else
-- Citizen spawn logic
client:SetModel(char:GetModel())
client:SetMaxHealth(100)
client:SetHealth(100)
client:SetArmor(0)
end
end
function SCHEMA:PlayerGiveSWEP(client, className, info)
-- Prevent certain weapons
if className == "weapon_rpg" and !client:IsSuperAdmin() then
return false
end
return true
end
Door Hooks¶
-- Custom door detection
local combineDoorModels = {
["models/props_combine/combine_door01.mdl"] = true,
["models/combine_gate_vehicle.mdl"] = true,
["models/combine_gate_citizen.mdl"] = true,
}
function SCHEMA:IsEntityDoor(entity, class)
return class == "prop_dynamic" and combineDoorModels[string.lower(entity:GetModel())]
end
function SCHEMA:PlayerCanUseDoor(client, entity)
local isCombineDoor = self:IsEntityDoor(entity, entity:GetClass())
local char = client:GetCharacter()
if isCombineDoor and !char:IsCombine() then
client:Notify("This door is restricted to Combine personnel.")
return false
end
return true
end
Think Hooks¶
function SCHEMA:Think()
-- Run every frame on both client and server
-- Be careful with expensive operations here!
-- Example: Update scoreboard
if SERVER then
-- Server-side think logic
end
if CLIENT then
-- Client-side think logic
end
end
Post-PlayerDeath¶
function SCHEMA:PostPlayerDeath(client, inflictor, attacker)
local char = client:GetCharacter()
-- Drop items on death
if char then
local inv = ax.inventory:Get(char:GetInventoryID())
if inv then
-- Drop all items
for itemID, item in pairs(inv.items) do
ax.item:Transfer(item, inv, 0)
end
end
end
end
Content Organization¶
Custom Materials¶
content/
└── materials/
└── my_schema/
├── banners/
│ ├── faction1.png
│ └── faction2.png
└── icons/
└── item_icon.png
Reference in code:
-- In faction
FACTION.image = ax.util:GetMaterial("my_schema/banners/faction1.png")
-- In item
ITEM.icon = ax.util:GetMaterial("my_schema/icons/item_icon.png")
Add files in gamemode/init.lua:
resource.AddFile("materials/my_schema/banners/faction1.png")
resource.AddFile("materials/my_schema/icons/item_icon.png")
Custom Models¶
Add models via resource.AddFile:
-- gamemode/init.lua
resource.AddFile("models/my_schema/custom_model.mdl")
resource.AddFile("models/my_schema/custom_model.phy")
resource.AddFile("models/my_schema/custom_model.vvd")
Use in items:
Custom Sounds¶
Add sounds:
Use sounds:
util.PrecacheSound("my_schema/custom_sound.wav")
-- Play sound
client:EmitSound("my_schema/custom_sound.wav")
Directory Best Practices¶
schema/
├── boot.lua -- Schema definition
├── config/ -- Configuration files
│ └── sh_config.lua
├── core/ -- Core schema systems
│ └── sh_characters.lua
├── factions/ -- Faction definitions
│ ├── sh_citizen.lua
│ └── sh_mpf.lua
├── items/ -- Item definitions
│ ├── base/ -- Base items
│ ├── weapons/ -- Weapons
│ └── food/ -- Food
├── hooks/ -- Schema hooks
│ ├── cl_hooks.lua -- Client hooks
│ ├── sh_hooks.lua -- Shared hooks
│ └── sv_hooks.lua -- Server hooks
├── meta/ -- Meta-tables
│ └── sh_character.lua
├── languages/ -- Localization
│ └── cl_english.lua
├── libraries/ -- Custom libraries
└── interface/ -- UI systems
└── cl_derma.lua
Schema Configuration¶
Configuration File¶
-- schema/config/sh_config.lua
-- Server settings
ax.config:Set("server.name", "My Roleplay Server")
ax.config:Set("server.description", "A custom roleplay server")
ax.config:Set("server.max_players", 32)
-- Gameplay settings
ax.config:Set("playtime.enabled", true)
ax.config:Set("playtime.interval", 60) -- Update every 60 seconds
-- Economy settings
ax.config:Set("economy.starting_money", 500)
ax.config:Set("economy.max_money", 1000000)
-- Character settings
ax.config:Set("character.max_characters", 5)
ax.config:Set("character.default_health", 100)
-- Inventory settings
ax.config:Set("inventory.default_weight", 30)
ax.config:Set("inventory.weight_unit", "kg")
Map-Specific Configuration¶
-- schema/config/maps/rp_city45_2013.lua
-- Map-specific settings for City45
ax.config:Set("map.spawn_positions", {
{pos = Vector(-1234, 567, 128), angle = Angle(0, 90, 0)},
{pos = Vector(-1234, 467, 128), angle = Angle(0, 90, 0)},
-- ... more spawns
})
ax.config:Set("map.cctv_positions", {
{pos = Vector(-1000, 1000, 200), angle = Angle(0, 0, 0)},
-- ... more cameras
})
ax.config:Set("map.nexus_door", 12345)
Using Configuration¶
-- Get configuration value
local serverName = ax.config:Get("server.name")
local maxPlayers = ax.config:Get("server.max_players", 32) -- With default
-- Set configuration
ax.config:Set("server.name", "New Server Name")
Schema Localization¶
Localization File¶
-- schema/languages/cl_english.lua
ax.lang:Add("en", {
-- General
["welcome"] = "Welcome to the server!",
["disconnected"] = "You have been disconnected.",
-- Factions
["faction.citizen"] = "Citizen",
["faction.mpf"] = "Metropolitan Police Force",
["faction.ota"] = "Overwatch Transhuman Arm",
-- Commands
["cmd.charcreate"] = "Create a character",
["cmd.pm"] = "Send a private message",
-- UI
["ui.inventory"] = "Inventory",
["ui.characters"] = "Characters",
["ui.factions"] = "Factions",
})
Using Localization¶
-- Get localized string
local welcomeMsg = ax.lang:Get("welcome", "en") -- or use player's language
-- In UI
local label = vgui.Create("DLabel")
label:SetText(ax.lang:Get("ui.inventory", client:GetLanguage()))
Schema Events¶
Schema Loaded Hook¶
-- Run when schema finishes loading
function SCHEMA:OnSchemaLoaded()
ax.util:PrintSuccess(SCHEMA.name .. " loaded successfully!")
-- Post-load initialization
self:InitializeCustomSystems()
end
Custom Events¶
-- Register custom hook type
ax.hook:Register("MYSCHEMA")
-- Define custom events
MYSCHEMA:OnCustomEvent(data)
print("Custom event:", data.value)
end
-- Trigger event
hook.Run("OnCustomEvent", {value = "test"})
Schema Best Practices¶
1. Keep It Modular¶
Organize code into logical directories:
- Factions in
factions/ - Items in
items/ - Hooks in
hooks/ - Core systems in
core/
2. Use File Prefixes¶
sh_for shared code (most common)cl_for client-side onlysv_for server-side only
3. Extend, Don't Override¶
When possible, extend framework systems rather than overriding:
-- Good: Extend character meta-table
function ax.character.meta:IsCustomRole()
return self:GetFaction() == CUSTOM_FACTION
end
-- Bad: Override framework function
-- function ax.character:Get() ... -- Don't do this
4. Use Schema Hooks¶
Override framework behavior with schema hooks:
5. Document Your Code¶
Add comments to explain complex logic:
-- Check if player can become faction based on multiple conditions:
-- 1. Player must be alive
-- 2. Player must not be arrested
-- 3. Player must have required flags
function FACTION:CanBecome(client)
if !client:Alive() then
return false, "You must be alive to join this faction"
end
-- ... more checks
return true
end
6. Test Incrementally¶
Start with a minimal schema and add features one at a time:
- Create basic schema structure
- Add one faction
- Add one item
- Add one hook
- Test thoroughly
Continue to: Advanced Topics