-- ===================================================================== -- Aether Shop — Serverlogik -- Der Client schickt nur den Warenkorb. Preise, Bestand, Lizenz, -- Deckung und Traglast werden hier komplett neu geprueft. -- ===================================================================== local Core = exports['aether-core'] local Inventory = exports['aether-inventory'] -- Bestaende der Laeden im Speicher: [ladenId][produktId] = Menge local bestaende = {} -- Merkt sich, an welchem Laden ein Spieler gerade steht local offeneLaeden = {} -- --------------------------------------------------------------------- -- Bestandsverwaltung -- --------------------------------------------------------------------- --- Setzt alle Bestaende auf die Ausgangswerte des Katalogs local function BestaendeAufbauen() for ladenId, laden in pairs(Shop.Laeden) do bestaende[ladenId] = {} for _, produktId in ipairs(laden.produkte) do local produkt = Shop.Produkte[produktId] bestaende[ladenId][produktId] = produkt and produkt.bestand or 0 end end end --- Fuellt die Bestaende langsam wieder auf (alle 15 Minuten um 10 %) CreateThread(function() BestaendeAufbauen() while true do Wait(15 * 60 * 1000) for ladenId, laden in pairs(Shop.Laeden) do for _, produktId in ipairs(laden.produkte) do local produkt = Shop.Produkte[produktId] if produkt then local maximum = produkt.bestand or 0 local aktuell = bestaende[ladenId][produktId] or 0 if aktuell < maximum then local nachschub = math.max(1, math.floor(maximum * 0.1)) bestaende[ladenId][produktId] = math.min(maximum, aktuell + nachschub) end end end end end end) -- --------------------------------------------------------------------- -- Hilfsfunktionen -- --------------------------------------------------------------------- --- Baut den Katalog eines Ladens fuer die Anzeige (mit aktuellem Bestand) --- @param ladenId string Kennung des Ladens --- @return table|nil local function BaueKatalog(ladenId) local laden = Shop.Laeden[ladenId] if not laden then return nil end local produkte = {} for _, produktId in ipairs(laden.produkte) do local produkt = Shop.Produkte[produktId] if produkt then produkte[produktId] = { name = produkt.name, kat = produkt.kat, icon = produkt.icon, preis = produkt.preis, rabatt = produkt.rabatt, lizenz = produkt.lizenz, beschreibung = produkt.beschreibung, menge = produkt.menge, bestand = bestaende[ladenId] and bestaende[ladenId][produktId] or 0, } end end return { id = ladenId, name = laden.name, logo = laden.logo, tl = laden.tl, accent = laden.accent, ink = laden.ink, warnung = laden.warnung, produkte = produkte, reihenfolge = laden.produkte, } end --- Prueft, ob ein Spieler einen gueltigen Waffenschein besitzt. --- Sobald `aether-documents` migriert ist, greift die echte Pruefung. --- @param quelle number Server-ID --- @return boolean local function HatWaffenschein(quelle) if not Shop.LizenzPflicht then return true end -- Optionale Abfrage beim Dokumente-Modul local erfolg, ergebnis = pcall(function() return exports['aether-documents']:HatWaffenschein(quelle) end) if erfolg and type(ergebnis) == 'boolean' then return ergebnis end -- Modul noch nicht vorhanden: Rueckfallregel aus der Konfiguration return Shop.OhneDokumenteErlauben end --- Lehnt eine Aktion mit Begruendung ab local function Ablehnen(quelle, grund) TriggerClientEvent('aether:shop:fehler', quelle, grund) end -- --------------------------------------------------------------------- -- Laden oeffnen -- --------------------------------------------------------------------- RegisterNetEvent('aether:shop:oeffnen', function(ladenId) local quelle = source if not Core:HoleCharakterId(quelle) then return end if type(ladenId) ~= 'string' then return end local laden = Shop.Laeden[ladenId] if not laden then return end -- Entfernungspruefung: Der Spieler muss wirklich an einer Filiale stehen local position = GetEntityCoords(GetPlayerPed(quelle)) local inReichweite = false for _, standort in ipairs(laden.standorte) do if #(position - standort) <= (Shop.INTERAKTION_WEITE + 2.0) then inReichweite = true break end end if not inReichweite then Ablehnen(quelle, 'Du bist nicht an diesem Laden.') return end offeneLaeden[quelle] = ladenId local daten = Core:HoleSpielerInfo(quelle) TriggerClientEvent('aether:shop:zeige', quelle, BaueKatalog(ladenId), { bargeld = daten and daten.bargeld or 0, konto = daten and daten.konto or 0, }) end) -- --------------------------------------------------------------------- -- Kaufen -- --------------------------------------------------------------------- RegisterNetEvent('aether:shop:kaufen', function(warenkorb, zahlung) local quelle = source local charakterId = Core:HoleCharakterId(quelle) if not charakterId then return end local ladenId = offeneLaeden[quelle] if not ladenId then Ablehnen(quelle, 'Kein Laden geöffnet.') return end if type(warenkorb) ~= 'table' then return end zahlung = (zahlung == 'karte') and 'konto' or 'bargeld' -- ---- Warenkorb vollstaendig neu durchrechnen ---- local gesamt = 0 local posten = {} for produktId, menge in pairs(warenkorb) do if type(produktId) ~= 'string' or type(menge) ~= 'number' then return end menge = math.floor(menge) if menge < 1 or menge > Shop.MAX_MENGE then Ablehnen(quelle, 'Ungültige Menge.') return end -- Fuehrt dieser Laden das Produkt ueberhaupt? if not Shop.FuehrtProdukt(ladenId, produktId) then Ablehnen(quelle, 'Dieses Produkt gibt es hier nicht.') return end local produkt = Shop.Produkte[produktId] if not produkt then Ablehnen(quelle, 'Unbekanntes Produkt.') return end -- Bestand pruefen local vorrat = bestaende[ladenId][produktId] or 0 if menge > vorrat then Ablehnen(quelle, ('%s ist nicht so oft vorrätig.'):format(produkt.name)) return end -- Waffenschein pruefen if produkt.lizenz and not HatWaffenschein(quelle) then Ablehnen(quelle, ('Für %s brauchst du einen gültigen Waffenschein.'):format(produkt.name)) return end -- Preis kommt IMMER vom Server, nie aus dem Warenkorb des Clients local einzelpreis = Shop.Preis(produkt) gesamt = gesamt + einzelpreis * menge posten[#posten + 1] = { produktId = produktId, name = produkt.name, menge = menge, summe = einzelpreis * menge, item = produkt.item, stueck = (produkt.menge or 1) * menge, -- tatsaechliche Stueckzahl im Inventar } end if #posten == 0 then Ablehnen(quelle, 'Der Warenkorb ist leer.') return end -- ---- Passt alles ins Inventar? ---- for _, eintrag in ipairs(posten) do if not Inventory:PasstNoch(quelle, eintrag.item, eintrag.stueck) then Ablehnen(quelle, 'Du kannst nicht so viel tragen.') return end end -- ---- Bezahlen (prueft die Deckung) ---- if not Core:EntferneGeld(quelle, zahlung, gesamt, 'Einkauf ' .. Shop.Laeden[ladenId].name) then Ablehnen(quelle, zahlung == 'bargeld' and 'Nicht genug Bargeld.' or 'Konto nicht gedeckt.') return end -- ---- Waren aushaendigen ---- local fehlgeschlagen = {} for _, eintrag in ipairs(posten) do if Inventory:GebeItem(quelle, eintrag.item, eintrag.stueck) then bestaende[ladenId][eintrag.produktId] = (bestaende[ladenId][eintrag.produktId] or 0) - eintrag.menge else fehlgeschlagen[#fehlgeschlagen + 1] = eintrag end end -- Konnte etwas nicht uebergeben werden, den Anteil zurueckerstatten if #fehlgeschlagen > 0 then local erstattung = 0 for _, eintrag in ipairs(fehlgeschlagen) do erstattung = erstattung + eintrag.summe end Core:FuegeGeldHinzu(quelle, zahlung, erstattung, 'Erstattung Einkauf') Ablehnen(quelle, ('%d Artikel passten nicht — %d € wurden erstattet.') :format(#fehlgeschlagen, erstattung)) end Aether.Info(('Einkauf: Charakter #%d bei %s für %d €') :format(charakterId, Shop.Laeden[ladenId].name, gesamt)) -- Beleg und aktualisierten Katalog zurueckschicken local daten = Core:HoleSpielerInfo(quelle) TriggerClientEvent('aether:shop:gekauft', quelle, { posten = posten, gesamt = gesamt, zahlung = zahlung, katalog = BaueKatalog(ladenId), geld = { bargeld = daten and daten.bargeld or 0, konto = daten and daten.konto or 0 }, }) end) -- --------------------------------------------------------------------- -- Aufraeumen -- --------------------------------------------------------------------- RegisterNetEvent('aether:shop:schliessen', function() offeneLaeden[source] = nil end) AddEventHandler('playerDropped', function() offeneLaeden[source] = nil end)