- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark, Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI) - aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource, an aether-core gekoppelt (Rechte über aether_users.admin_group), mit WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU) - Prototypen admin/ vervollständigt - SQL-Schema aether_admin.sql - Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
370 lines
13 KiB
Lua
370 lines
13 KiB
Lua
-- =====================================================================
|
|
-- Aether Inventory — Datenschicht
|
|
-- Haelt die Inventare aktiver Spieler im Speicher und schreibt sie
|
|
-- in die Datenbank zurueck. Enthaelt keine Ereignisse — nur Logik.
|
|
-- =====================================================================
|
|
|
|
Lager = {}
|
|
|
|
-- Inventare aktiver Charaktere: [charakterId] = { [slot] = {item, menge, meta} }
|
|
local inventare = {}
|
|
|
|
-- Items am Boden: [bodenId] = { id, item, menge, meta, x, y, z, prop }
|
|
-- Die IDs kommen aus der Datenbank, damit Drops Neustarts ueberleben.
|
|
local bodenItems = {}
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Laden und Speichern
|
|
-- ---------------------------------------------------------------------
|
|
|
|
--- Laedt das Inventar eines Charakters aus der Datenbank
|
|
--- @param charakterId number Charakter-ID
|
|
--- @return table Inventar (slot -> Eintrag)
|
|
function Lager.Laden(charakterId)
|
|
local zeilen = MySQL.query.await(
|
|
'SELECT slot, item, amount, metadata FROM aether_inventory WHERE owner_type = ? AND owner_id = ?',
|
|
{ 'character', tostring(charakterId) }) or {}
|
|
|
|
local inventar = {}
|
|
for _, zeile in ipairs(zeilen) do
|
|
-- Unbekannte Items ueberspringen (z. B. nach Entfernen aus der Registry)
|
|
if Inventar.Items[zeile.item] then
|
|
local meta = nil
|
|
if zeile.metadata then
|
|
local erfolg, entschluesselt = pcall(json.decode, zeile.metadata)
|
|
if erfolg then meta = entschluesselt end
|
|
end
|
|
inventar[zeile.slot] = { item = zeile.item, menge = zeile.amount, meta = meta }
|
|
end
|
|
end
|
|
|
|
inventare[charakterId] = inventar
|
|
return inventar
|
|
end
|
|
|
|
--- Schreibt das Inventar eines Charakters komplett zurueck
|
|
--- @param charakterId number Charakter-ID
|
|
function Lager.Speichern(charakterId)
|
|
local inventar = inventare[charakterId]
|
|
if not inventar then return end
|
|
|
|
-- Einfach und sicher: alten Stand loeschen, aktuellen neu schreiben
|
|
MySQL.query.await('DELETE FROM aether_inventory WHERE owner_type = ? AND owner_id = ?',
|
|
{ 'character', tostring(charakterId) })
|
|
|
|
local werte = {}
|
|
for slot, eintrag in pairs(inventar) do
|
|
werte[#werte + 1] = {
|
|
'character', tostring(charakterId), slot, eintrag.item, eintrag.menge,
|
|
eintrag.meta and json.encode(eintrag.meta) or nil,
|
|
}
|
|
end
|
|
|
|
if #werte > 0 then
|
|
MySQL.prepare.await(
|
|
'INSERT INTO aether_inventory (owner_type, owner_id, slot, item, amount, metadata) VALUES (?, ?, ?, ?, ?, ?)',
|
|
werte)
|
|
end
|
|
end
|
|
|
|
--- Entfernt ein Inventar aus dem Speicher (nach dem Speichern)
|
|
--- @param charakterId number Charakter-ID
|
|
function Lager.Entladen(charakterId)
|
|
inventare[charakterId] = nil
|
|
end
|
|
|
|
--- Legt die Startausruestung fuer einen neuen Charakter an
|
|
--- @param charakterId number Charakter-ID
|
|
function Lager.ErstelleStart(charakterId)
|
|
local inventar = {}
|
|
for _, eintrag in ipairs(Inventar.StartItems) do
|
|
inventar[eintrag.slot] = { item = eintrag.item, menge = eintrag.menge, meta = nil }
|
|
end
|
|
inventare[charakterId] = inventar
|
|
Lager.Speichern(charakterId)
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Zugriff
|
|
-- ---------------------------------------------------------------------
|
|
|
|
--- Liefert das Inventar eines Charakters (oder nil)
|
|
--- @param charakterId number Charakter-ID
|
|
--- @return table|nil
|
|
function Lager.Hole(charakterId)
|
|
return inventare[charakterId]
|
|
end
|
|
|
|
--- Berechnet das getragene Gesamtgewicht
|
|
--- @param charakterId number Charakter-ID
|
|
--- @return number Gewicht in Kilogramm
|
|
function Lager.Gewicht(charakterId)
|
|
local inventar = inventare[charakterId]
|
|
if not inventar then return 0 end
|
|
|
|
local gewicht = 0
|
|
for _, eintrag in pairs(inventar) do
|
|
local def = Inventar.Items[eintrag.item]
|
|
if def then gewicht = gewicht + (def.gewicht or 0) * eintrag.menge end
|
|
end
|
|
return gewicht
|
|
end
|
|
|
|
--- Sucht den ersten freien Platz in der Tasche
|
|
--- @param charakterId number Charakter-ID
|
|
--- @return number|nil Slotnummer oder nil, wenn voll
|
|
function Lager.FreierSlot(charakterId)
|
|
local inventar = inventare[charakterId]
|
|
if not inventar then return nil end
|
|
|
|
for slot = Inventar.BEREICH.tasche.von, Inventar.BEREICH.tasche.bis do
|
|
if not inventar[slot] then return slot end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--- Prueft, ob eine zusaetzliche Menge noch getragen werden kann
|
|
--- @param charakterId number Charakter-ID
|
|
--- @param kennung string Item-Kennung
|
|
--- @param menge number Stueckzahl
|
|
--- @return boolean
|
|
function Lager.PasstNoch(charakterId, kennung, menge)
|
|
local def = Inventar.Items[kennung]
|
|
if not def then return false end
|
|
local zusatz = (def.gewicht or 0) * menge
|
|
return (Lager.Gewicht(charakterId) + zusatz) <= Inventar.MAX_GEWICHT
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Aenderungen am Inventar
|
|
-- ---------------------------------------------------------------------
|
|
|
|
--- Fuegt ein Item hinzu (stapelt, wenn moeglich)
|
|
--- @param charakterId number Charakter-ID
|
|
--- @param kennung string Item-Kennung
|
|
--- @param menge number Stueckzahl
|
|
--- @param meta table|nil Zusatzdaten
|
|
--- @return boolean Erfolg, string|nil Fehlergrund
|
|
function Lager.Hinzufuegen(charakterId, kennung, menge, meta)
|
|
local inventar = inventare[charakterId]
|
|
local def = Inventar.Items[kennung]
|
|
if not inventar or not def then return false, 'Unbekanntes Item' end
|
|
if not Lager.PasstNoch(charakterId, kennung, menge) then return false, 'Zu schwer' end
|
|
|
|
-- Stapelbare Items ohne Zusatzdaten auf vorhandene Stapel legen
|
|
if def.stapelbar and not meta then
|
|
for slot = Inventar.BEREICH.tasche.von, Inventar.BEREICH.tasche.bis do
|
|
local eintrag = inventar[slot]
|
|
if eintrag and eintrag.item == kennung and not eintrag.meta then
|
|
eintrag.menge = eintrag.menge + menge
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
local frei = Lager.FreierSlot(charakterId)
|
|
if not frei then return false, 'Kein Platz frei' end
|
|
|
|
inventar[frei] = { item = kennung, menge = menge, meta = meta }
|
|
return true
|
|
end
|
|
|
|
--- Entfernt eine Menge aus einem bestimmten Slot
|
|
--- @param charakterId number Charakter-ID
|
|
--- @param slot number Slotnummer
|
|
--- @param menge number Stueckzahl
|
|
--- @return table|nil Entfernter Eintrag {item, menge, meta} oder nil
|
|
function Lager.EntferneAusSlot(charakterId, slot, menge)
|
|
local inventar = inventare[charakterId]
|
|
if not inventar then return nil end
|
|
|
|
local eintrag = inventar[slot]
|
|
if not eintrag then return nil end
|
|
|
|
menge = math.min(menge or eintrag.menge, eintrag.menge)
|
|
if menge <= 0 then return nil end
|
|
|
|
local entfernt = { item = eintrag.item, menge = menge, meta = eintrag.meta }
|
|
|
|
if menge >= eintrag.menge then
|
|
inventar[slot] = nil
|
|
else
|
|
eintrag.menge = eintrag.menge - menge
|
|
end
|
|
|
|
return entfernt
|
|
end
|
|
|
|
--- Verschiebt oder tauscht zwei Slots (inkl. Stapeln)
|
|
--- @param charakterId number Charakter-ID
|
|
--- @param vonSlot number Quelle
|
|
--- @param nachSlot number Ziel
|
|
--- @return boolean Erfolg, string|nil Fehlergrund
|
|
function Lager.Verschieben(charakterId, vonSlot, nachSlot)
|
|
local inventar = inventare[charakterId]
|
|
if not inventar or vonSlot == nachSlot then return false end
|
|
|
|
local quelle = inventar[vonSlot]
|
|
if not quelle then return false, 'Kein Item' end
|
|
|
|
local quellDef = Inventar.Items[quelle.item]
|
|
local ziel = inventar[nachSlot]
|
|
|
|
-- Ausruestungs-Slot: nur passende Items zulassen
|
|
local zielSchluessel = Inventar.EquipSchluessel(nachSlot)
|
|
if zielSchluessel and quellDef.slot ~= zielSchluessel then
|
|
return false, 'Passt nicht in diesen Ausrüstungsplatz'
|
|
end
|
|
|
|
-- Beim Tauschen muss auch das Zielitem in die Quelle passen
|
|
local quellSchluessel = Inventar.EquipSchluessel(vonSlot)
|
|
if ziel and quellSchluessel then
|
|
local zielDef = Inventar.Items[ziel.item]
|
|
if zielDef.slot ~= quellSchluessel then
|
|
return false, 'Tauschen nicht möglich'
|
|
end
|
|
end
|
|
|
|
-- Stapeln, wenn identisches stapelbares Item ohne Zusatzdaten
|
|
if ziel and ziel.item == quelle.item and quellDef.stapelbar
|
|
and not quelle.meta and not ziel.meta then
|
|
ziel.menge = ziel.menge + quelle.menge
|
|
inventar[vonSlot] = nil
|
|
return true
|
|
end
|
|
|
|
-- Verschieben oder Tauschen
|
|
inventar[nachSlot] = quelle
|
|
inventar[vonSlot] = ziel -- nil, wenn das Ziel leer war
|
|
return true
|
|
end
|
|
|
|
--- Teilt einen Stapel auf einen freien Taschenplatz auf
|
|
--- @param charakterId number Charakter-ID
|
|
--- @param slot number Quellslot
|
|
--- @param menge number Abzuspaltende Menge
|
|
--- @return boolean Erfolg, string|nil Fehlergrund
|
|
function Lager.Splitten(charakterId, slot, menge)
|
|
local inventar = inventare[charakterId]
|
|
if not inventar then return false end
|
|
|
|
local eintrag = inventar[slot]
|
|
if not eintrag then return false, 'Kein Item' end
|
|
if menge < 1 or menge >= eintrag.menge then return false, 'Ungültige Menge' end
|
|
|
|
local frei = Lager.FreierSlot(charakterId)
|
|
if not frei then return false, 'Kein Platz frei' end
|
|
|
|
eintrag.menge = eintrag.menge - menge
|
|
inventar[frei] = { item = eintrag.item, menge = menge, meta = eintrag.meta }
|
|
return true
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Items am Boden
|
|
-- ---------------------------------------------------------------------
|
|
|
|
--- Laedt alle fallengelassenen Items aus der Datenbank (beim Serverstart)
|
|
--- und raeumt dabei zu alte Drops auf.
|
|
--- @return table Alle geladenen Drops
|
|
function Lager.LadeBoden()
|
|
-- Alte Drops entfernen, damit die Welt nicht zumuellt
|
|
if Inventar.DROP_MAX_STUNDEN and Inventar.DROP_MAX_STUNDEN > 0 then
|
|
MySQL.query.await(
|
|
'DELETE FROM aether_drops WHERE created_at < DATE_SUB(NOW(), INTERVAL ? HOUR)',
|
|
{ Inventar.DROP_MAX_STUNDEN })
|
|
end
|
|
|
|
local zeilen = MySQL.query.await(
|
|
'SELECT id, item, amount, metadata, x, y, z, prop FROM aether_drops') or {}
|
|
|
|
bodenItems = {}
|
|
for _, zeile in ipairs(zeilen) do
|
|
-- Items, die es nicht mehr gibt, werden uebersprungen
|
|
if Inventar.Items[zeile.item] then
|
|
local meta = nil
|
|
if zeile.metadata then
|
|
local erfolg, entschluesselt = pcall(json.decode, zeile.metadata)
|
|
if erfolg then meta = entschluesselt end
|
|
end
|
|
bodenItems[zeile.id] = {
|
|
id = zeile.id, item = zeile.item, menge = zeile.amount, meta = meta,
|
|
x = zeile.x, y = zeile.y, z = zeile.z, prop = zeile.prop,
|
|
}
|
|
end
|
|
end
|
|
|
|
return bodenItems
|
|
end
|
|
|
|
--- Liefert alle bekannten Boden-Items (fuer die Uebertragung an Clients)
|
|
--- @return table
|
|
function Lager.AlleBodenItems()
|
|
return bodenItems
|
|
end
|
|
|
|
--- Legt ein Item an einer Position auf den Boden und speichert es dauerhaft
|
|
--- @param eintrag table {item, menge, meta}
|
|
--- @param koordinaten table {x, y, z}
|
|
--- @return table|nil Datensatz mit id und prop
|
|
function Lager.AufBodenLegen(eintrag, koordinaten)
|
|
-- Zufaelligen Karton waehlen, damit alle Drops einheitlich aussehen
|
|
local prop = Inventar.DROP_PROPS[math.random(#Inventar.DROP_PROPS)]
|
|
|
|
local id = MySQL.insert.await([[
|
|
INSERT INTO aether_drops (item, amount, metadata, x, y, z, prop)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
]], {
|
|
eintrag.item, eintrag.menge,
|
|
eintrag.meta and json.encode(eintrag.meta) or nil,
|
|
koordinaten.x, koordinaten.y, koordinaten.z, prop,
|
|
})
|
|
|
|
if not id then return nil end
|
|
|
|
local datensatz = {
|
|
id = id, item = eintrag.item, menge = eintrag.menge, meta = eintrag.meta,
|
|
x = koordinaten.x, y = koordinaten.y, z = koordinaten.z, prop = prop,
|
|
}
|
|
bodenItems[id] = datensatz
|
|
return datensatz
|
|
end
|
|
|
|
--- Nimmt ein Item vom Boden (und loescht es dauerhaft)
|
|
--- @param bodenId number Boden-ID
|
|
--- @return table|nil Eintrag oder nil
|
|
function Lager.VomBodenNehmen(bodenId)
|
|
local eintrag = bodenItems[bodenId]
|
|
if not eintrag then return nil end
|
|
|
|
bodenItems[bodenId] = nil
|
|
MySQL.query('DELETE FROM aether_drops WHERE id = ?', { bodenId })
|
|
return eintrag
|
|
end
|
|
|
|
--- Liefert ein Item am Boden, ohne es zu entfernen
|
|
--- @param bodenId number Boden-ID
|
|
--- @return table|nil
|
|
function Lager.HoleBodenItem(bodenId)
|
|
return bodenItems[bodenId]
|
|
end
|
|
|
|
--- Liefert alle Boden-Items in Reichweite einer Position
|
|
--- @param koordinaten table {x, y, z}
|
|
--- @param weite number Sichtweite in Metern
|
|
--- @return table Liste mit {id, item, menge, meta}
|
|
function Lager.BodenInDerNaehe(koordinaten, weite)
|
|
local liste = {}
|
|
for id, eintrag in pairs(bodenItems) do
|
|
local dx = eintrag.x - koordinaten.x
|
|
local dy = eintrag.y - koordinaten.y
|
|
local dz = eintrag.z - koordinaten.z
|
|
if (dx * dx + dy * dy + dz * dz) <= (weite * weite) then
|
|
liste[#liste + 1] = {
|
|
id = id, item = eintrag.item, menge = eintrag.menge, meta = eintrag.meta,
|
|
}
|
|
end
|
|
end
|
|
return liste
|
|
end
|