🗄️ 05 — Config e Database
INFO
Separare la configurazione dal codice è un pattern universale in FiveM che rende le risorse più flessibili e manutenibili. Il database permette di persistare dati come giocatori, inventari e posizioni.
| Obiettivo | Dettagli |
|---|---|
| Cosa imparerai | - Separare configurazione dal codice con config.lua- Connettere e interrogare un database MySQL con oxmysql- Operazioni CRUD sicure con prepared statements |
| Prerequisiti | File 02-04 completati, conoscenze base di SQL |
File di Configurazione
Un file config.lua separa i dati dalla logica. È un pattern universale in FiveM.
-- config.lua
Config = {}
-- Impostazioni generali
Config.ServerName = "Il Mio Server Roleplay"
Config.ServerSlots = 64
Config.ServerLanguage = "it"
Config.ServerLogo = "https://esempio.com/logo.png"
-- Impostazioni gameplay
Config.StartMoney = 5000
Config.StartJob = "disoccupato"
Config.MaxPlayers = 200
-- Posizioni (vettori)
Config.SpawnPositions = {
{ x = 185.0, y = -1000.0, z = 30.0, heading = 180.0, label = "Centro città" },
{ x = -1040.0, y = -2740.0, z = 20.0, heading = 90.0, label = "Porto" },
{ x = 430.0, y = -1800.0, z = 28.0, heading = 0.0, label = "Ospedale" }
}
-- Veicoli disponibili al concessionario
Config.DealerVehicles = {
{ model = "adder", price = 1000000, category = "super" },
{ model = "sultanrs", price = 250000, category = "sport" },
{ model = "brioso", price = 8000, category = "compatto" },
{ model = "sanchez", price = 5000, category = "moto" },
{ model = "guardian", price = 350000, category = "offroad" }
}
-- Armi (hash e prezzi)
Config.Weapons = {
{ hash = "WEAPON_PISTOL", name = "Pistola", price = 5000 },
{ hash = "WEAPON_COMBATPISTOL", name = "Pistola da comb.", price = 8000 },
{ hash = "WEAPON_MICROSMG", name = "Micro SMG", price = 15000 },
{ hash = "WEAPON_PUMPSHOTGUN", name = "Fucile a pompa",price = 25000 },
{ hash = "WEAPON_ASSAULTRIFLE", name = "Fucile d'assalto", price = 40000 }
}
-- Blip (icone mappa)
Config.Blips = {
{ x = 185.0, y = -1000.0, z = 30.0, sprite = 475, color = 3, scale = 1.0, label = "Municipio" },
{ x = -807.0, y = -180.0, z = 37.0, sprite = 135, color = 56, scale = 1.0, label = "Ospedale" },
{ x = 840.0, y = -850.0, z = 27.0, sprite = 84, color = 1, scale = 1.0, label = "Polizia" }
}-- server.lua — uso della config
Citizen.CreateThread(function()
print("^2[INFO] " .. Config.ServerName .. " avviato^7")
print("^2[INFO] Slot: " .. Config.ServerSlots .. " | Lingua: " .. Config.ServerLanguage .. "^7")
SetConvar("sv_maxclients", tostring(Config.ServerSlots))
end)
-- Funzione per ottenere spawn casuale
local function GetSpawnPosition()
local pos = Config.SpawnPositions[math.random(1, #Config.SpawnPositions)]
return vector3(pos.x, pos.y, pos.z), pos.heading
endDatabase in FiveM
FiveM non include un database built-in. I più usati sono:
- oxmysql — Moderno, prepared statements nativi (raccomandato)
- ghmattimysql — Legacy, ancora molto usato
Installazione oxmysql
- Scarica l’ultima release da GitHub
- Mettila nella cartella
resources/ - Aggiungi
ensure oxmysqlal tuoserver.cfg - Configura nel
server.cfg:
set mysql_connection_string "mysql://utente:password@localhost:3306/nome_database"Connessione e Query Base
-- server.lua — con oxmysql
-- INSERT
local function CreaGiocatore(identifier, nome, denaro)
local successo, righe = MySQL.insert.await(
"INSERT INTO giocatori (identifier, nome, denaro, data_creazione) VALUES (?, ?, ?, NOW())",
{ identifier, nome, denaro or Config.StartMoney }
)
if successo then
print("^2[Nuovo giocatore] " .. nome .. " (ID DB: " .. righe .. ")^7")
return righe
else
print("^1[ERRORE] Creazione giocatore fallita per " .. nome .. "^7")
return false
end
end-- SELECT
local function CaricaGiocatore(identifier)
local risultato = MySQL.query.await(
"SELECT * FROM giocatori WHERE identifier = ? LIMIT 1",
{ identifier }
)
if risultato and #risultato > 0 then
return risultato[1] -- restituisce la prima riga come tabella
end
return nil
end-- UPDATE
local function SalvaPosizione(identifier, x, y, z, heading)
MySQL.update.await(
"UPDATE giocatori SET pos_x = ?, pos_y = ?, pos_z = ?, heading = ? WHERE identifier = ?",
{ x, y, z, heading, identifier }
)
endPrepared Statements (Sicurezza)
WARNING
SQL injection è il rischio di sicurezza più grave in FiveM. Non concatenare mai input utente direttamente in query SQL. Usa SEMPRE prepared statements con
?come placeholder.
I prepared statements prevengono SQL injection. Usali SEMPRE.
-- MALE: SQL injection possibile
local nome = "Marco' OR '1'='1"
MySQL.query.await("SELECT * FROM giocatori WHERE nome = '" .. nome .. "'")
-- BENE: prepared statement
local nome = "Marco' OR '1'='1"
MySQL.query.await("SELECT * FROM giocatori WHERE nome = ?", { nome })
-- Non trova niente — cerca letteralmente il nome con gli apiciEsempio Completo: Salvare e Caricare Giocatore
Tabella MySQL
CREATE TABLE IF NOT EXISTS giocatori (
id INT AUTO_INCREMENT PRIMARY KEY,
identifier VARCHAR(64) NOT NULL UNIQUE,
nome VARCHAR(64) NOT NULL DEFAULT 'Sconosciuto',
denaro INT NOT NULL DEFAULT 5000,
banca INT NOT NULL DEFAULT 0,
pos_x FLOAT NOT NULL DEFAULT 185.0,
pos_y FLOAT NOT NULL DEFAULT -1000.0,
pos_z FLOAT NOT NULL DEFAULT 30.0,
heading FLOAT NOT NULL DEFAULT 180.0,
data_creazione DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
ultimo_accesso DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS inventario (
id INT AUTO_INCREMENT PRIMARY KEY,
giocatore_id INT NOT NULL,
item VARCHAR(64) NOT NULL,
quantita INT NOT NULL DEFAULT 1,
FOREIGN KEY (giocatore_id) REFERENCES giocatori(id) ON DELETE CASCADE
);server.lua
-- server.lua
local giocatori = {} -- cache dei giocatori online
-- Alla connessione
AddEventHandler("playerJoining", function(playerSource)
local identifier = GetPlayerIdentifier(playerSource)
if not identifier then
DropPlayer(playerSource, "Errore di autenticazione")
return
end
local dati = CaricaGiocatore(identifier)
if not dati then
-- Nuovo giocatore
local id = CreaGiocatore(identifier, GetPlayerName(playerSource), Config.StartMoney)
if id then
dati = {
id = id,
identifier = identifier,
nome = GetPlayerName(playerSource),
denaro = Config.StartMoney,
banca = 0,
pos_x = Config.SpawnPositions[1].x,
pos_y = Config.SpawnPositions[1].y,
pos_z = Config.SpawnPositions[1].z,
heading = Config.SpawnPositions[1].heading
}
end
end
if dati then
giocatori[playerSource] = dati
print("^2[Giocatore] " .. dati.nome .. " caricato (#" .. playerSource .. ")^7")
-- Spawna il giocatore alla sua posizione salvata
TriggerClientEvent("giocatore:spawna", playerSource,
dati.pos_x, dati.pos_y, dati.pos_z, dati.heading)
end
end)
-- Alla disconnessione
AddEventHandler("playerDropped", function(reason)
local source = source
local dati = giocatori[source]
if dati then
local ped = GetPlayerPed(source)
-- Salva posizione corrente
if DoesEntityExist(ped) then
local coords = GetEntityCoords(ped)
local heading = GetEntityHeading(ped)
SalvaPosizione(dati.identifier, coords.x, coords.y, coords.z, heading)
end
print("^3[Giocatore] " .. dati.nome .. " salvato e disconnesso (" .. reason .. ")^7")
giocatori[source] = nil
end
end)
-- Funzioni database
function CaricaGiocatore(identifier)
local risultato = MySQL.query.await(
"SELECT * FROM giocatori WHERE identifier = ? LIMIT 1",
{ identifier }
)
return risultato and risultato[1] or nil
end
function CreaGiocatore(identifier, nome, denaro)
local risultato = MySQL.insert.await(
"INSERT INTO giocatori (identifier, nome, denaro) VALUES (?, ?, ?)",
{ identifier, nome, denaro }
)
return risultato
end
function SalvaPosizione(identifier, x, y, z, heading)
MySQL.update.await(
"UPDATE giocatori SET pos_x = ?, pos_y = ?, pos_z = ?, heading = ? WHERE identifier = ?",
{ x, y, z, heading, identifier }
)
end
-- Esport per altre risorse
exports("GetPlayerData", function(playerSource)
return giocatori[playerSource]
end)client.lua
-- client.lua
RegisterNetEvent("giocatore:spawna")
AddEventHandler("giocatore:spawna", function(x, y, z, heading)
Citizen.CreateThread(function()
-- Aspetta che il giocatore sia completamente connesso
while not NetworkIsPlayerActive(PlayerId()) do
Citizen.Wait(100)
end
local ped = PlayerPedId()
-- Spawna il modello di default
local modello = GetHashKey("mp_m_freemode_01")
RequestModel(modello)
while not HasModelLoaded(modello) do
Citizen.Wait(10)
end
SetPlayerModel(PlayerId(), modello)
SetModelAsNoLongerNeeded(modello)
-- Posiziona il giocatore
SetEntityCoords(ped, x, y, z, false, false, false, false)
SetEntityHeading(ped, heading)
-- Freeze temporaneo per evitare cadute
FreezeEntityPosition(ped, true)
Citizen.Wait(500)
FreezeEntityPosition(ped, false)
print("^2[Spawn] Giocatore posizionato a " .. string.format("%.1f, %.1f, %.1f", x, y, z) .. "^7")
end)
end)Inventario: Query Complesse
-- server.lua — inventario
local function GetInventario(giocatoreId)
local items = MySQL.query.await(
"SELECT i.item, i.quantita, id.nome FROM inventario i " ..
"JOIN items_database id ON i.item = id.item " ..
"WHERE i.giocatore_id = ?",
{ giocatoreId }
)
return items or {}
end
local function AggiungItem(giocatoreId, item, quantita)
-- Controlla se già possiede l'item
local esistente = MySQL.query.await(
"SELECT quantita FROM inventario WHERE giocatore_id = ? AND item = ? LIMIT 1",
{ giocatoreId, item }
)
if esistente and #esistente > 0 then
-- Aggiorna quantità
MySQL.update.await(
"UPDATE inventario SET quantita = quantita + ? WHERE giocatore_id = ? AND item = ?",
{ quantita, giocatoreId, item }
)
else
-- Nuovo item
MySQL.insert.await(
"INSERT INTO inventario (giocatore_id, item, quantita) VALUES (?, ?, ?)",
{ giocatoreId, item, quantita }
)
end
end
local function RimuovItem(giocatoreId, item, quantita)
local esistente = MySQL.query.await(
"SELECT quantita FROM inventario WHERE giocatore_id = ? AND item = ? LIMIT 1",
{ giocatoreId, item }
)
if not esistente or #esistente == 0 then
return false, "Item non posseduto"
end
local nuovaQuantita = esistente[1].quantita - quantita
if nuovaQuantita <= 0 then
MySQL.query.await(
"DELETE FROM inventario WHERE giocatore_id = ? AND item = ?",
{ giocatoreId, item }
)
else
MySQL.update.await(
"UPDATE inventario SET quantita = ? WHERE giocatore_id = ? AND item = ?",
{ nuovaQuantita, giocatoreId, item }
)
end
return true
endAsync Queries (Senza await)
oxmysql supporta sia chiamate sincrone (await) che callback:
-- Con callback (stile tradizionale)
MySQL.query("SELECT * FROM giocatori WHERE id = ?", { 1 }, function(risultato)
if risultato then
print("Nome:", risultato[1].nome)
end
end)
-- Con await (moderno, richiede Citizen.CreateThread)
Citizen.CreateThread(function()
local risultato = MySQL.query.await("SELECT * FROM giocatori WHERE id = ?", { 1 })
if risultato then
print("Nome:", risultato[1].nome)
end
end)TIP
Mantieni una cache in memoria dei giocatori online. Leggi dal database all’avvio (
playerJoining) e salva alla disconnessione (playerDropped). Aggiungi un autosave periodico per prevenire perdita dati in caso di crash del server.
Best Practices
| Regola | Perché |
|---|---|
| Sempre prepared statements | Previene SQL injection |
| Chiudi connessioni solo su oxmysql | La libreria gestisce il pool automaticamente |
| Cache dei dati in memoria | Leggi dal DB all’avvio, salva su disconnect |
| Salvataggio periodico (autosave) | Previene perdita dati su crash |
| Non fare query nei loop stretti | Usa Wait(0) o cache locale |
| Separare config dal codice | Modifiche più semplici, riusabilità |
| Validare input prima di salvare | tonumber(), controlli tipo |
| Usa TRANSACTION per operazioni multiple | Garantisce atomicità |
Esempio: Transazione
-- Trasferimento denaro tra giocatori
local function TrasferisciDenaro(fromId, toId, importo)
local successo = MySQL.transaction.await({
"UPDATE giocatori SET denaro = denaro - ? WHERE id = ? AND denaro >= ?",
"UPDATE giocatori SET denaro = denaro + ? WHERE id = ?",
{ importo, fromId, importo },
{ importo, toId }
})
return successo
endQUESTION
Riflessione: Hai considerato un sistema di autosave periodico? In caso di crash del server, i dati non salvati alla disconnessione andrebbero persi. Un salvataggio ogni 5-10 minuti è una rete di sicurezza importante.
flowchart TD subgraph "Avvio Risorsa" A[server.lua] --> B[Carica config.lua] B --> C[Connessione DB] end subgraph "Connessione Giocatore" D[playerJoining] --> E[Leggi DB] E --> F{Trovato?} F -->|Sì| G[Carica dati in cache] F -->|No| H[Crea nuovo record] H --> G end subgraph "Disconnessione" I[playerDropped] --> J[Salva posizione e dati] J --> K[Cancella dalla cache] end
TIP
Prossimo passo: 06 - NUI e UI
Torna alla: Indice Generale