- 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>
404 lines
15 KiB
Lua
404 lines
15 KiB
Lua
-- =====================================================================
|
||
-- 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)
|