-- ===================================================================== -- 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