Aller au contenu

Écrire une ressource sur bs_core

Construisons-en une toute petite : une « tour radio » utilisable une fois toutes les 10 minutes pour 50 $ en liquide et un peu d’XP. Elle est inventée, les éléments qu’elle utilise sont réels.

resources/[beyond]/bs_radiotower/
├── fxmanifest.lua
├── config/config.lua
├── client/cl_main.lua
└── server/sv_main.lua

Préfixez-la avec bs_. Les noms de ressources FiveM forment un seul espace de noms global ; une ressource appelée radio finira par entrer en collision avec le script de quelqu’un.

fx_version 'cerulean'
game 'gta5'
lua54 'yes'
name 'bs_radiotower'
description 'BeyondSurvival - radio tower example'
shared_scripts {
'@bs_core/shared/sh_bridge.lua', -- BSB callbacks + BSB.RegisterCommand
'config/config.lua',
}
client_scripts { 'client/cl_main.lua' }
server_scripts { 'server/sv_main.lua' }
dependencies { 'bs_core' }
-- live overrides from the F1 console, AFTER your config files
shared_script '@bs_config/shared/apply.lua'
-- crash breadcrumbs
client_script '@bs_core/shared/sh_crumbs.lua'

Ajoutez ensuite ensure bs_radiotower à server.cfg, après bs_core et après tout ce que vous appelez.

RadioTower = {}
RadioTower.Settings = {
reward = 50,
xp = 20,
cooldown = 600, -- seconds
pos = vector3(0.0, 0.0, 0.0),
range = 3.0,
}

Une table globale au nom de votre ressource. C’est ce que le helper apply de F1 modifie (voir Helper config apply).

local last = {} -- [charId] = os.time()
BSB.RegisterCallback('radiotower:use', function(src)
local p = exports.bs_core:GetPlayerSummary(src)
if not p then return { ok = false, error = 'Not loaded.' } end
-- never trust the client: check distance here too
local pos = GetEntityCoords(GetPlayerPed(src))
if #(pos - RadioTower.Settings.pos) > RadioTower.Settings.range + 2.0 then
return { ok = false, error = 'Too far.' }
end
local now = os.time()
if last[p.charId] and now - last[p.charId] < RadioTower.Settings.cooldown then
return { ok = false, error = 'Nothing on the air yet.' }
end
last[p.charId] = now
exports.bs_core:AddMoney(src, 'cash', RadioTower.Settings.reward, 'radiotower')
exports.bs_core:AddXP(src, RadioTower.Settings.xp, 'radiotower')
return { ok = true }
end)

Argent et XP uniquement via les exports de bs_core. N’écrivez jamais vous-même dans les colonnes de la BD.

local function use()
if not exports.bs_core:IsLoaded() then return end
Crumb('radiotower', 'use')
local res = BSB.Await('radiotower:use', 5000)
if not res then return end -- timed out, server side not running
TriggerEvent('bs:ui:notify', {
type = res.ok and 'success' or 'error',
message = res.ok and 'You picked up a signal.' or res.error,
})
end
RegisterCommand('radiotower', use, false)

Pour une vraie interaction, vous enregistreriez une option dans bs_interactions (le point ALT) plutôt qu’une commande ; regardez comment bs_recycler ou bs_mining le font.

Appelez exports.bs_guard:Allow(src, 'teleport', 3000) avant de les déplacer côté serveur, sinon bs_guard signale chaque usage.

  • Les globales d’une autre ressource sont invisibles. Items.Definitions dans bs_admin est une table vide, pas celle de bs_inventory. Utilisez les exports.
  • Les fonctions ne traversent pas les exports. Passer une fonction callback à l’export d’une autre ressource ne marche pas. C’est pourquoi BSB est un fichier que vous incluez, pas un export.
  • Ne faites pas de Wait dans un export appelé par quelqu’un d’autre. Il s’exécute sur le thread de l’appelant dans votre runtime et lève une erreur. bs_radial avait ce bug, c’est pourquoi BSB.Radial existe.
  • local a, b = x and f() or y - Lua ne garde que le premier retour de f() ici. Écrivez-le avec un if.
  • Un local utilisé avant sa déclaration est une globale nil. luac5.4 -l file.lua | grep _ENV les trouve.
  • Protégez GetEntityModel avec DoesEntityExist. Sur un handle mort, ça fait planter violemment le client du jeu.
  • Pas de messages NUI à chaque frame au repos. Envoyez au changement.
  • Chaque bs:ui:opened a besoin d’un bs:ui:closed sur chaque chemin de sortie, voir Conventions UI/NUI.