Créer un module externe
Un module est une ressource séparée qui s'appuie sur tlr_core : il s'enregistre auprès du cœur, déclare ses paramètres et ses permissions, et utilise les exports pour l'argent, l'inventaire, les métiers. Cette page suit un exemple complet, un module de cueillette de baies nommé tlr_baies. Retour au sommaire.
Avant de commencer, lisez l'architecture et gardez sous la main la référence de l'API.
1. Le manifeste
fx_version 'cerulean'
game 'rdr3'
rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.'
lua54 'yes'
name 'tlr_baies'
version '1.0.0'
dependencies { 'tlr_core', 'oxmysql' }
shared_script '@tlr_core/lib/init.lua'
server_scripts { '@oxmysql/lib/MySQL.lua', 'server.lua' }
client_scripts { 'client.lua' }
dependenciesgarantit que tlr_core démarre avant le module.@tlr_core/lib/init.luadonne accès à l'objettlib(callbacks, zones, invites, notifications, formulaires).- Dans server.cfg, mettez
ensure tlr_baiesaprèsensure tlr_core.
2. S'enregistrer auprès du cœur
L'export RegisterModule valide la déclaration et retourne true ou false, message. Il faut l'appeler quand le cœur est prêt, et de nouveau si tlr_core redémarre : on écoute tlr:core:ready et on vérifie au chargement si le cœur est déjà prêt.
Champs de la déclaration :
id: 3 à 40 caractères, minuscules, chiffres et soulignés, commençant par une lettre ;name,version(forme 1.2.3),descriptionetauthorfacultatifs ;requires = { core = '0.1.0' }: version minimale de tlr_core ;dependencies: ressources qui doivent être démarrées ;settings: paramètres du module (voir l'étape 3) ;permissions: liste de{ node, label }(voir l'étape 4).
Le module apparaît ensuite dans la section Modules du panel. S'il s'arrête, il y reste marqué arrêté et ses paramètres sont retirés.
3. Déclarer un réglage
Les paramètres se déclarent dans settings. Chacun reçoit la clé <id du module>.<nom> et apparaît dans la section Paramètres, rangé sous le nom du module (ou sous category si vous la donnez).
- Types :
boolean,integer,number,string,enum(avecchoices = { { value, label } }),money(centimes),object,array. - Options :
min,max,default,label,description,category. - Lecture :
exports.tlr_core:GetSetting('tlr_baies.prix'). - Changement :
exports.tlr_core:OnSettingChanged('tlr_baies.prix', function(valeur) end).
La valeur en jeu prend le dessus sur la valeur par défaut ; l'administrateur peut revenir à la valeur par défaut depuis le panel.
4. Déclarer une permission
Les permissions d'un module doivent commencer par tlr.modules.<id>., sinon elles sont ignorées. Elles apparaissent dans la section Permissions, rangées sous le nom du module. Rappel : tout ce qui n'est pas accordé est refusé.
Pour vérifier un droit côté serveur, le module passe par l'objet central :
local TLR = exports.tlr_core:GetCoreObject()
local permis = TLR.Permissions.has(src, 'tlr.modules.tlr_baies.admin')
L'appel de fonctions de l'objet central depuis une autre ressource passe par les références de fonctions de Cfx : il est prévu par le cœur (la lib s'en sert pour le journal) mais son coût reste à mesurer en jeu.
5. Les actions venues de l'interface
La passerelle RPC de tlr_core (TLR.RPC.register) sert la page NUI de tlr_core : elle reçoit l'événement tlr:rpc envoyé par cette page. Aucun export n'enregistre une action RPC pour une autre ressource, et la page d'un module ne peut pas l'appeler directement.
Un module utilise donc tlib.callback, qui applique les mêmes principes : refus par défaut (un nom non enregistré ne répond pas), limite de débit par joueur, erreurs journalisées. Le gestionnaire revérifie tout : permission, distance, argent.
tlib.callback.register('tlr_baies:cueillir', function(src)
return true
end, { rate = { requests = 5, perMs = 5000 } })
Côté client : local ok, err = tlib.callback.await('tlr_baies:cueillir'). Préfixez toujours le nom par celui de la ressource.
6. Une migration
TLR.Migrations.add est réservé au code de tlr_core : les migrations du cœur s'appliquent une seule fois, au démarrage de tlr_core, avant que les modules ne s'enregistrent. Un module crée donc ses propres tables avec oxmysql, au démarrage, avec CREATE TABLE IF NOT EXISTS. Pour faire évoluer une table, ajoutez une nouvelle requête idempotente plutôt que de modifier l'ancienne, comme le fait le cœur.
7. Un objet
L'export ProvideJobItems(liste) ajoute des objets au registre de l'inventaire ; la ressource appelante en devient propriétaire, et un nouvel appel remplace tous ses objets. Les objets de config.lua et du panel restent prioritaires. Chaque objet suit le schéma des objets :
name(minuscules, chiffres et soulignés),label,descriptionfacultative ;widthde 1 à 7 cases,heightde 1 à 4 cases,weighten grammes ;stackable,maxStackfacultatif,consumable;hunger,thirst,energy,cleanfacultatifs ;perish: durée de péremption en secondes parmi les choix proposés (0 pour jamais, 3600, 7200, 10800, 86400...) ;image(fichier deweb/img/items/de tlr_core) etcategoryfacultatifs.
Pour rendre l'objet utilisable, le cœur fournit TLR.Inventory.onUse(nom, gestionnaire, libellé) dans son propre code ; depuis un module, écoutez plutôt l'événement serveur tlr:inventory:used (src, nomObjet).
8. Un écran
Deux possibilités :
- Utiliser les composants de la lib, affichés dans la page de tlr_core :
tlib.notify,tlib.progress,tlib.input,tlib.alert,tlib.context,tlib.menu,tlib.textui,tlib.skillcheck. Hors de tlr_core, ils passent par les exports client de tlr_core. C'est le plus simple et l'écran suit les thèmes. - Une page NUI propre au module (
ui_pagedans son manifeste). Elle doit suivre les mêmes règles : aucune ressource externe, fond transparent, fichiers déclarés dansfiles. Le module gère alors lui même son focus et ses rappels NUI.
9. Événements disponibles
Côté serveur, à écouter avec AddEventHandler :
tlr:core:ready: le cœur est prêt (aussi après un redémarrage de tlr_core).tlr:character:loaded (src, idPersonnage)ettlr:character:unloaded (src, idPersonnage).tlr:settings:changed (clé, valeur).tlr:jobs:changed (nom),tlr:jobs:refresh (src),tlr:jobs:duty (src, métier, enService),tlr:jobs:activity (src, activité, type).tlr:inventory:used (src, nomObjet),tlr:inventory:updated (idPersonnage),tlr:items:changed.tlr:shops:purchased (src, idBoutique, détail, total).
Côté client : tlr:lib:notify (données) affiche une notification envoyée par le serveur.
10. Exemple complet
server.lua :
local TLR = nil
local ID = 'tlr_baies'
local function enregistrer()
TLR = exports.tlr_core:GetCoreObject()
local ok, err = exports.tlr_core:RegisterModule({
id = ID, name = 'Cueillette de baies', version = '1.0.0',
description = 'Cueillir des baies et les revendre.',
requires = { core = '0.1.0' },
settings = {
actif = { type = 'boolean', default = true, label = 'Cueillette active' },
quantite = { type = 'integer', min = 1, max = 10, default = 2, label = 'Baies par cueillette' },
prix = { type = 'money', min = 0, max = 10000, default = 15, label = "Prix de vente d'une baie" },
},
permissions = {
{ node = 'tlr.modules.tlr_baies.admin', label = 'Donner des baies' },
},
})
if not ok then print('tlr_baies : ' .. err) return end
exports.tlr_core:ProvideJobItems({
{ name = 'baie_sauvage', label = 'Baies sauvages', description = 'Une poignée de baies.',
width = 1, height = 1, weight = 50, stackable = true, consumable = true,
hunger = 5, thirst = 2, perish = 86400, category = 'nourriture' },
})
end
AddEventHandler('tlr:core:ready', enregistrer)
CreateThread(function()
if GetResourceState('tlr_core') == 'started' and exports.tlr_core:IsReady() then enregistrer() end
end)
tlib.callback.register('tlr_baies:cueillir', function(src)
if not exports.tlr_core:GetSetting('tlr_baies.actif') then return false, 'INACTIF' end
if not exports.tlr_core:GetCharacterId(src) then return false, 'NO_CHARACTER' end
local qte = exports.tlr_core:GetSetting('tlr_baies.quantite')
local ok, code = exports.tlr_core:AddItem(src, 'baie_sauvage', qte)
if not ok then return false, code end
tlib.logger('economy', 'baies.cueillette', { qte = qte }, src)
return true, qte
end, { rate = { requests = 3, perMs = 5000 } })
tlib.callback.register('tlr_baies:vendre', function(src)
local n = exports.tlr_core:GetItemCount(src, 'baie_sauvage')
if n < 1 then return false, 'NOT_FOUND' end
exports.tlr_core:RemoveItem(src, 'baie_sauvage', n)
local total = n * exports.tlr_core:GetSetting('tlr_baies.prix')
local opId = ('tlr_baies:vente:%d:%d'):format(src, os.time())
exports.tlr_core:AddMoney(src, 'cash', total, 'Vente de baies', opId)
return true, exports.tlr_core:FormatMoney(total)
end)
RegisterCommand('donnerbaies', function(src, args)
if not TLR or not TLR.Permissions.has(src, 'tlr.modules.tlr_baies.admin') then return end
local cible = tonumber(args[1])
if cible then exports.tlr_core:AddItem(cible, 'baie_sauvage', 5) end
end, false)
client.lua :
local buisson = vector3(0.0, 0.0, 0.0)
CreateThread(function()
local invite = tlib.prompt.hold('E', 'Cueillir', 1500)
invite:setVisible(false)
tlib.points.new({
coords = buisson, distance = 2.0,
onEnter = function() invite:setVisible(true) end,
onExit = function() invite:setVisible(false) end,
})
while true do
Wait(0)
if invite:completed() then
invite:reset()
local ok, res = tlib.callback.await('tlr_baies:cueillir')
if ok then
tlib.notify({ type = 'ok', title = 'Cueillette', description = ('%d baies cueillies'):format(res), item = 'baie_sauvage' })
else
tlib.notify({ type = 'bad', title = 'Cueillette', description = "Rien à cueillir pour l'instant." })
end
end
end
end)
Remplacez la position du buisson par une vraie coordonnée. Le maintien des invites (faut il appeler reset après une invite réussie) est signalé comme à vérifier en jeu dans docs/API.md.
Vérifications avant de livrer
- Le module s'enregistre (message vert dans la console) et apparaît dans la section Modules.
- Ses paramètres apparaissent dans Paramètres et ses permissions dans Permissions.
- Chaque gestionnaire serveur revérifie droits, distance et argent : le client ne décide jamais.
- Montants en centimes, avec un
opIdunique pour chaque opération d'argent. - Textes et commentaires en français, sans tiret : voir les conventions.
Source : tlr_core/docs/guide/dev_module.md