-- ===================================================================== -- Aether Inventory — Serverlogik -- Jede Bewegung wird hier geprueft. Der Client sagt nur, was er moechte; -- entschieden wird ausschliesslich auf dem Server. -- ===================================================================== local Core = exports['aether-core'] -- --------------------------------------------------------------------- -- Hilfsfunktionen -- --------------------------------------------------------------------- --- Liefert die Charakter-ID eines Spielers --- @param quelle number Server-ID --- @return number|nil local function CharakterVon(quelle) return Core:HoleCharakterId(quelle) end --- Prueft, ob eine Slotnummer ueberhaupt gueltig ist --- @param slot any Zu pruefender Wert --- @return boolean local function SlotGueltig(slot) if type(slot) ~= 'number' or slot ~= math.floor(slot) then return false end return Inventar.BereichVon(slot) ~= nil end --- Baut den Zustand, den die Oberflaeche zum Anzeigen braucht --- @param quelle number Server-ID --- @param charakterId number Charakter-ID --- @return table local function BaueZustand(quelle, charakterId) local koordinaten = GetEntityCoords(GetPlayerPed(quelle)) local position = { x = koordinaten.x, y = koordinaten.y, z = koordinaten.z } -- Spieler in Reichweite (anonym, solange nicht bekannt) local nahe = {} for _, andereQuelle in ipairs(GetPlayers()) do andereQuelle = tonumber(andereQuelle) if andereQuelle ~= quelle then local andererPed = GetPlayerPed(andereQuelle) local andereKoordinaten = GetEntityCoords(andererPed) if #(koordinaten - andereKoordinaten) <= Inventar.GEBEN_WEITE then nahe[#nahe + 1] = { id = andereQuelle, -- Namen bewusst nicht mitschicken: Im Rollenspiel kennt man -- eine Person erst nach Ausweis/Visitenkarte. bekannt = false, } end end end return { inventar = Lager.Hole(charakterId) or {}, gewicht = Lager.Gewicht(charakterId), maxGewicht = Inventar.MAX_GEWICHT, boden = Lager.BodenInDerNaehe(position, Inventar.BODEN_SICHT), spieler = nahe, } end --- Schickt den aktuellen Zustand an einen Spieler --- @param quelle number Server-ID --- @param hinweis string|nil Kurze Rueckmeldung local function SendeZustand(quelle, hinweis) local charakterId = CharakterVon(quelle) if not charakterId then return end local zustand = BaueZustand(quelle, charakterId) zustand.hinweis = hinweis TriggerClientEvent('aether:inventory:zustand', quelle, zustand) end --- Meldet dem Spieler, dass eine Aktion nicht moeglich war --- @param quelle number Server-ID --- @param grund string Begruendung local function Ablehnen(quelle, grund) TriggerClientEvent('aether:inventory:hinweis', quelle, grund or 'Nicht möglich') end -- --------------------------------------------------------------------- -- Fallengelassene Items beim Serverstart laden -- --------------------------------------------------------------------- CreateThread(function() Wait(1000) -- kurz warten, bis die Datenbankverbindung steht local drops = Lager.LadeBoden() local anzahl = 0 for _ in pairs(drops) do anzahl = anzahl + 1 end Aether.Info(('%d fallengelassene Items geladen.'):format(anzahl)) end) -- --------------------------------------------------------------------- -- Laden und Speichern im Spielerlebenszyklus -- --------------------------------------------------------------------- --- Spieler wurde geladen: Inventar aus der Datenbank holen AddEventHandler('aether:server:spielerGeladen', function(quelle, spieler) local charakterId = spieler.charakterId local vorhanden = MySQL.scalar.await( 'SELECT COUNT(*) FROM aether_inventory WHERE owner_type = ? AND owner_id = ?', { 'character', tostring(charakterId) }) if (vorhanden or 0) == 0 then -- Frischer Charakter: Startausruestung anlegen Lager.ErstelleStart(charakterId) Aether.Info(('Startausruestung fuer Charakter #%d angelegt.'):format(charakterId)) else Lager.Laden(charakterId) end -- Alle bekannten Boden-Items schicken, damit der Client die Kartons setzt TriggerClientEvent('aether:inventory:dropListe', quelle, Lager.AlleBodenItems()) end) --- Spieler hat den Server verlassen: Inventar sichern AddEventHandler('aether:server:spielerEntfernt', function(quelle, spieler) Lager.Speichern(spieler.charakterId) Lager.Entladen(spieler.charakterId) end) --- Beim Stoppen der Ressource alle Inventare sichern AddEventHandler('onResourceStop', function(ressource) if ressource ~= GetCurrentResourceName() then return end for _, quelle in ipairs(GetPlayers()) do local charakterId = CharakterVon(tonumber(quelle)) if charakterId then Lager.Speichern(charakterId) end end end) -- --------------------------------------------------------------------- -- Oeffnen -- --------------------------------------------------------------------- RegisterNetEvent('aether:inventory:oeffnen', function() local quelle = source local charakterId = CharakterVon(quelle) if not charakterId then return end -- Sicherstellen, dass das Inventar geladen ist if not Lager.Hole(charakterId) then Lager.Laden(charakterId) end TriggerClientEvent('aether:inventory:zeige', quelle, BaueZustand(quelle, charakterId)) end) -- --------------------------------------------------------------------- -- Verschieben / Tauschen / Stapeln -- --------------------------------------------------------------------- RegisterNetEvent('aether:inventory:verschieben', function(vonSlot, nachSlot) local quelle = source local charakterId = CharakterVon(quelle) if not charakterId then return end if not SlotGueltig(vonSlot) or not SlotGueltig(nachSlot) then return end local erfolg, grund = Lager.Verschieben(charakterId, vonSlot, nachSlot) if not erfolg and grund then Ablehnen(quelle, grund) end SendeZustand(quelle) end) -- --------------------------------------------------------------------- -- Stapel teilen -- --------------------------------------------------------------------- RegisterNetEvent('aether:inventory:splitten', function(slot, menge) local quelle = source local charakterId = CharakterVon(quelle) if not charakterId then return end if not SlotGueltig(slot) or type(menge) ~= 'number' then return end local erfolg, grund = Lager.Splitten(charakterId, slot, math.floor(menge)) if not erfolg and grund then Ablehnen(quelle, grund) end SendeZustand(quelle) end) -- --------------------------------------------------------------------- -- Fallenlassen -- --------------------------------------------------------------------- RegisterNetEvent('aether:inventory:fallenlassen', function(slot, menge) local quelle = source local charakterId = CharakterVon(quelle) if not charakterId then return end if not SlotGueltig(slot) then return end local entfernt = Lager.EntferneAusSlot(charakterId, slot, math.floor(menge or 0)) if not entfernt then Ablehnen(quelle, 'Kein Item') return end local koordinaten = GetEntityCoords(GetPlayerPed(quelle)) local datensatz = Lager.AufBodenLegen(entfernt, { x = koordinaten.x, y = koordinaten.y, z = koordinaten.z }) if not datensatz then -- Speichern fehlgeschlagen: Item zurueckgeben, damit es nicht verschwindet Lager.Hinzufuegen(charakterId, entfernt.item, entfernt.menge, entfernt.meta) Ablehnen(quelle, 'Fallenlassen fehlgeschlagen') SendeZustand(quelle) return end -- Alle Clients erzeugen den Karton an dieser Stelle TriggerClientEvent('aether:inventory:dropHinzu', -1, datensatz) local def = Inventar.Items[entfernt.item] SendeZustand(quelle, (def and def.name or entfernt.item) .. ' fallengelassen') end) -- --------------------------------------------------------------------- -- Aufheben -- --------------------------------------------------------------------- RegisterNetEvent('aether:inventory:aufheben', function(bodenId) local quelle = source local charakterId = CharakterVon(quelle) if not charakterId or type(bodenId) ~= 'number' then return end local amBoden = Lager.HoleBodenItem(bodenId) if not amBoden then Ablehnen(quelle, 'Item ist nicht mehr da') return end -- Entfernungspruefung: niemand darf quer über die Karte aufheben local koordinaten = GetEntityCoords(GetPlayerPed(quelle)) local abstand = #(koordinaten - vector3(amBoden.x, amBoden.y, amBoden.z)) if abstand > Inventar.AUFHEB_WEITE then Ablehnen(quelle, 'Zu weit entfernt') return end if not Lager.PasstNoch(charakterId, amBoden.item, amBoden.menge) then Ablehnen(quelle, 'Zu schwer') return end -- Erst jetzt vom Boden nehmen, damit es bei Ablehnung liegen bleibt local genommen = Lager.VomBodenNehmen(bodenId) if not genommen then Ablehnen(quelle, 'Item ist nicht mehr da') return end -- Karton bei allen Clients entfernen TriggerClientEvent('aether:inventory:dropWeg', -1, bodenId) local erfolg, grund = Lager.Hinzufuegen(charakterId, genommen.item, genommen.menge, genommen.meta) if not erfolg then -- Zurueck auf den Boden legen, damit nichts verloren geht local neu = Lager.AufBodenLegen(genommen, { x = amBoden.x, y = amBoden.y, z = amBoden.z }) if neu then TriggerClientEvent('aether:inventory:dropHinzu', -1, neu) end Ablehnen(quelle, grund or 'Aufheben nicht möglich') return end local def = Inventar.Items[genommen.item] SendeZustand(quelle, (def and def.name or genommen.item) .. ' aufgehoben') end) -- --------------------------------------------------------------------- -- Geben -- --------------------------------------------------------------------- RegisterNetEvent('aether:inventory:geben', function(slot, menge, zielQuelle) local quelle = source local charakterId = CharakterVon(quelle) if not charakterId then return end if not SlotGueltig(slot) or type(zielQuelle) ~= 'number' then return end if zielQuelle == quelle then return end local zielCharakter = CharakterVon(zielQuelle) if not zielCharakter then Ablehnen(quelle, 'Person nicht erreichbar') return end -- Entfernungspruefung local hier = GetEntityCoords(GetPlayerPed(quelle)) local dort = GetEntityCoords(GetPlayerPed(zielQuelle)) if #(hier - dort) > Inventar.GEBEN_WEITE then Ablehnen(quelle, 'Zu weit entfernt') return end -- Erst pruefen, ob der Empfaenger es tragen kann local inventar = Lager.Hole(charakterId) local eintrag = inventar and inventar[slot] if not eintrag then Ablehnen(quelle, 'Kein Item') return end local abgabe = math.min(math.floor(menge or eintrag.menge), eintrag.menge) if abgabe < 1 then return end if not Lager.PasstNoch(zielCharakter, eintrag.item, abgabe) then Ablehnen(quelle, 'Die Person kann das nicht tragen') return end local entfernt = Lager.EntferneAusSlot(charakterId, slot, abgabe) if not entfernt then return end local erfolg = Lager.Hinzufuegen(zielCharakter, entfernt.item, entfernt.menge, entfernt.meta) if not erfolg then -- Rueckgabe an den Absender, damit nichts verschwindet Lager.Hinzufuegen(charakterId, entfernt.item, entfernt.menge, entfernt.meta) Ablehnen(quelle, 'Übergabe fehlgeschlagen') SendeZustand(quelle) return end local def = Inventar.Items[entfernt.item] local bezeichnung = def and def.name or entfernt.item SendeZustand(quelle, bezeichnung .. ' übergeben') TriggerClientEvent('aether:inventory:hinweis', zielQuelle, ('Du hast %s ×%d erhalten'):format(bezeichnung, entfernt.menge)) SendeZustand(zielQuelle) end) -- --------------------------------------------------------------------- -- Benutzen -- --------------------------------------------------------------------- RegisterNetEvent('aether:inventory:benutzen', function(slot) local quelle = source local charakterId = CharakterVon(quelle) if not charakterId or not SlotGueltig(slot) then return end local inventar = Lager.Hole(charakterId) local eintrag = inventar and inventar[slot] if not eintrag then return end local def = Inventar.Items[eintrag.item] if not def or not def.nutzbar then Ablehnen(quelle, 'Das lässt sich nicht benutzen') return end -- Wirkung anwenden if def.wirkung then if def.wirkung.art == 'leben' then TriggerClientEvent('aether:inventory:heilen', quelle, def.wirkung.menge) else Core:AendereZustand(quelle, def.wirkung.art, def.wirkung.menge) end end -- Verbrauchbare Items aufbrauchen if def.verbrauchbar then Lager.EntferneAusSlot(charakterId, slot, 1) end -- Andere Module informieren (z. B. Handy oeffnen) TriggerEvent('aether:inventory:itemBenutzt', quelle, eintrag.item, eintrag.meta) TriggerClientEvent('aether:inventory:itemBenutzt', quelle, eintrag.item, eintrag.meta) SendeZustand(quelle, def.name .. ' benutzt') end) -- ===================================================================== -- EXPORTS — Schnittstelle fuer andere Module (Shop, Jobs, Dokumente ...) -- ===================================================================== --- Gibt einem Spieler ein Item exports('GebeItem', function(quelle, kennung, menge, meta) local charakterId = CharakterVon(quelle) if not charakterId then return false end local erfolg = Lager.Hinzufuegen(charakterId, kennung, menge or 1, meta) if erfolg then SendeZustand(quelle) end return erfolg end) --- Prueft, ob ein Spieler eine zusaetzliche Menge noch tragen kann --- @param quelle number Server-ID --- @param kennung string Item-Kennung --- @param menge number Stueckzahl --- @return boolean exports('PasstNoch', function(quelle, kennung, menge) local charakterId = CharakterVon(quelle) if not charakterId then return false end return Lager.PasstNoch(charakterId, kennung, menge or 1) end) --- Zaehlt, wie viele Stueck eines Items ein Spieler besitzt exports('ZaehleItem', function(quelle, kennung) local charakterId = CharakterVon(quelle) local inventar = charakterId and Lager.Hole(charakterId) if not inventar then return 0 end local summe = 0 for _, eintrag in pairs(inventar) do if eintrag.item == kennung then summe = summe + eintrag.menge end end return summe end) --- Entfernt eine Menge eines Items (ueber beliebige Slots verteilt) exports('EntferneItem', function(quelle, kennung, menge) local charakterId = CharakterVon(quelle) local inventar = charakterId and Lager.Hole(charakterId) if not inventar then return false end -- Erst pruefen, ob genug vorhanden ist local vorhanden = 0 for _, eintrag in pairs(inventar) do if eintrag.item == kennung then vorhanden = vorhanden + eintrag.menge end end if vorhanden < menge then return false end -- Dann abziehen local offen = menge for slot, eintrag in pairs(inventar) do if offen <= 0 then break end if eintrag.item == kennung then local abzug = math.min(offen, eintrag.menge) Lager.EntferneAusSlot(charakterId, slot, abzug) offen = offen - abzug end end SendeZustand(quelle) return true end)