Write a module
Last updated on
Add a new module to opx_infinity step by step — declaration, config, text, server and client halves, a contract, a network event, a command and the manifest lines.
A module is the normal way to add a feature. It lives inside opx_infinity under modules/<id>/, runs in the same Lua VM as every other module and can use their contracts. This guide builds tips: a player tips another player some money.
Read Modules and contracts and Server authority first.
The files
config/tips.lua settings
modules/tips/module.lua declaration and event names
modules/tips/locales.lua English and French text
modules/tips/server/main.lua the rules
modules/tips/client/main.lua the requestDeclare it
local M = OPX.Modules.Declare{
id = 'tips',
side = 'both',
fatal = false,
requires = { 'character' }, -- money lives in the character contract
}
M.Event = {
SEND = OPX.Event(OPX.Channel.NET, 'tips', 'send'), -- client -> server
SENT = OPX.Event(OPX.Channel.LOCAL, 'tips', 'sent'), -- server-side announcement
}Configure it
OPX.Config.MODULES.tips = {
enabled = true,
MAX_AMOUNT = 500,
COOLDOWN_MS = 10000,
}Read it as M.Settings.MAX_AMOUNT inside a phase, never at file scope (see Settings).
Text
OPX.Locale.Register('en', {
['tips.sent'] = 'You tipped {amount} eddies.',
['tips.badAmount'] = 'That is not a valid tip.',
})
OPX.Locale.Register('fr', {
['tips.sent'] = 'Vous avez donné un pourboire de {amount} eddies.',
['tips.badAmount'] = "Ce n'est pas un pourboire valide.",
})The server half
local M = OPX.Modules.Get('tips')
local character -- the contract, resolved in Start
local function send(source, target, amount)
amount = math.floor(tonumber(amount) or 0)
target = tonumber(target)
if amount < 1 or amount > M.Settings.MAX_AMOUNT or target == nil or target == source then
return OPX.Refuse(source, 'tips.badAmount', 'tips.send')
end
-- ... take from `source`, give to `target` through the character contract,
-- refunding if the second half fails. See the character module page for
-- the exact functions and their answers.
OPX.Publish(M.Event.SENT, source, { target = target, amount = amount })
OPX.NotifyLocale(source, 'tips.sent', { amount = amount }, 'success')
end
function M.Api()
OPX.Api.Provide('tips', 1, {
Send = send,
})
end
function M.Start()
character = OPX.Api.Require('character')
RegisterNetEvent(M.Event.SEND, function(target, amount)
local source = source -- from the connection, never the payload
if OPX.Cooling(source, 'tips.send', M.Settings.COOLDOWN_MS) then
return OPX.Refuse(source, 'error.tooFast', 'tips.send')
end
send(source, target, amount)
end)
OPX.Command.Register('opx.tip', { help = 'tips.help' }, function(source, args)
send(source, args[1], args[2])
end)
endOPX.Publishraisesopx:on:tips:sentfor every resource on the server (serverTriggerEventis host-wide). Give it a plain payload, never a live record, and list the event on Public server events.- Wiring between your own halves or other modules only: an
opx:in:name — still visible to other server resources, but not a promise. - To let other resources call your module, add a wrapper to
core/server/exports.lua(orcore/client/exports.lua) and document it under For creators. sourcecomes from the connection. Re-check every value in the payload.OPX.Command.Register, notRegisterCommand, so the chat box knows the command.{ restricted = true }for a staff command: the host then checkscommand.opx.tipbefore the handler runs.- Refuse with a code (
OPX.Refuse), not a sentence.
The client half
local M = OPX.Modules.Get('tips')
function M.Start()
-- e.g. a target row or a menu row calls this
end
function M.Ask(target, amount)
TriggerServerEvent(M.Event.SEND, target, amount)
end- Repeating client work goes through
OPX.Scheduler.Every.
The instruction budget
Every client callback runs on a coroutine resume with an instruction budget of about 10,000 VM instructions. Past it, the platform ends the coroutine silently, halfway through: a menu that never comes back, a key that needs two presses, a cursor left captured. Net events, page handlers (OPX.UI.On), event handlers, key mappings and scheduler jobs all count, and all scheduler jobs of one pass share one resume.
- Keep handlers cheap: validate, store, and hand the heavy work to a thread.
- In a loop over a list that can grow (players, slots, spots, catalogue rows), yield every few dozen items.
- Draw at most a few engine objects per pass (
OPX.Spots.Markers.Newcaps markers at 8). - Send long lists to the page in slices, and a big menu with
yield = trueor pages.
RegisterNetEvent(M.Event.SYNC, function(rows)
if type(rows) ~= 'table' then return end
M.pending = rows -- cheap: just keep it
if M.reading then return end
M.reading = true
CreateThread(function()
local list = {}
for i, row in ipairs(M.pending) do
list[#list + 1] = M.Check(row)
if i % 10 == 0 then Wait(0) end -- a fresh budget every 10 rows
end
M.rows, M.reading = list, false
end)
end)Measure with the budget meter.
List every file in the manifest
One line per file in open77.lua, after the modules you depend on and after config/<id>.lua:
shared_script "config/tips.lua"
shared_script "modules/tips/module.lua"
shared_script "modules/tips/locales.lua"
server_script "modules/tips/server/main.lua"
client_script "modules/tips/client/main.lua"- A file that is not listed never loads, and nothing tells you. The framework's test suite fails on an unlisted
.luafile. - A native that needs a new manifest permission: add it to the
permissionsblock.
Check it
From the opx_infinity folder:
lua tests/run.lua
OPX_BUDGET_METER=5000 lua tests/run.lua # lists client resumes near the budget
luac -p $(find . -name '*.lua' -not -name 'open77.lua')Deploy, restart, and look for [module] tips started in the journal.
Document it
Add content/docs/opx_infinity/tips/ to this site (index.mdx, server.mdx, client.mdx, events.mdx, meta.json) and "tips" to the pages of content/docs/opx_infinity/meta.json; see Contributing. The coverage check fails until every command, event, contract function and config key has an anchor.
Events
The public opx:on:* server events any resource can listen to, the private opx:in:* events, and the client side.
Write a separate resource
How a resource outside opx_infinity uses OPX//77 — server and client exports, public server events, opx_lib — what the player state bag no longer carries, and what it still cannot do.