aether-framework/resources/[aether]/aether-inventory/server/main.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

404 lines
15 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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