🗄️ 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.

ObiettivoDettagli
Cosa imparerai- Separare configurazione dal codice con config.lua
- Connettere e interrogare un database MySQL con oxmysql
- Operazioni CRUD sicure con prepared statements
PrerequisitiFile 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
end

Database 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

  1. Scarica l’ultima release da GitHub
  2. Mettila nella cartella resources/
  3. Aggiungi ensure oxmysql al tuo server.cfg
  4. 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 }
    )
end

Prepared 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 apici

Esempio 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
end

Async 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

RegolaPerché
Sempre prepared statementsPreviene SQL injection
Chiudi connessioni solo su oxmysqlLa libreria gestisce il pool automaticamente
Cache dei dati in memoriaLeggi dal DB all’avvio, salva su disconnect
Salvataggio periodico (autosave)Previene perdita dati su crash
Non fare query nei loop strettiUsa Wait(0) o cache locale
Separare config dal codiceModifiche più semplici, riusabilità
Validare input prima di salvaretonumber(), controlli tipo
Usa TRANSACTION per operazioni multipleGarantisce 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
end

QUESTION

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