aether-framework/resources/[aether]/aether-inventory/server/inventar.lua
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- 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>
2026-07-27 18:55:32 +02:00

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