-- ===================================================================== -- aether-medical — Kern -- -- Der Kern kennt keine Medizin. Er stellt bereit: -- * eine Modul-Registry (Plugin-System) -- * einen Ereignisbus zwischen den Modulen -- * ein Handler-Register, an das die Oberfläche Anfragen schickt -- -- Fachmodule registrieren sich selbst und können jederzeit ergänzt -- werden, ohne dass der Kern angefasst werden muss. -- ===================================================================== Medizin = Medizin or {} Kern = {} -- Registrierte Fachmodule: [id] = { id, name, icon, recht, reihenfolge, aktiv } local module = {} -- Handler, die die Oberfläche aufrufen darf: [modulId][aktion] = funktion local handler = {} -- Abonnenten des Ereignisbusses: [ereignis] = { funktion, ... } local abonnenten = {} -- --------------------------------------------------------------------- -- Modul-Registry -- --------------------------------------------------------------------- --- Registriert ein Fachmodul. --- @param beschreibung table { --- id eindeutige Kennung --- name Anzeigename in der Seitenleiste --- icon Symbol --- recht Recht zum Öffnen — Kennung, Liste oder nil (für alle) --- reihenfolge Sortierung in der Seitenleiste --- aktiv Standardmäßig aktiv? --- } --- @return boolean function Kern.RegistriereModul(beschreibung) if type(beschreibung) ~= 'table' or not beschreibung.id then Aether.Fehler('Modul ohne Kennung kann nicht registriert werden.') return false end if module[beschreibung.id] then Aether.Fehler(('Modul "%s" ist bereits registriert.'):format(beschreibung.id)) return false end module[beschreibung.id] = { id = beschreibung.id, name = beschreibung.name or beschreibung.id, icon = beschreibung.icon or '▪', recht = beschreibung.recht, reihenfolge = beschreibung.reihenfolge or 100, aktiv = beschreibung.aktiv ~= false, beschreibung = beschreibung.beschreibung, } handler[beschreibung.id] = handler[beschreibung.id] or {} Aether.Debug(('Medizin-Modul registriert: %s'):format(beschreibung.id)) return true end --- Registriert einen Handler, den die Oberfläche aufrufen kann. --- @param modulId string Modul --- @param aktion string Name der Aktion --- @param recht string|nil Erforderliches Recht (nil = nur Modulrecht) --- @param funktion function fun(quelle, mitarbeiter, daten): ergebnis function Kern.RegistriereAktion(modulId, aktion, recht, funktion) handler[modulId] = handler[modulId] or {} handler[modulId][aktion] = { recht = recht, funktion = funktion } end --- Prüft das Öffnungsrecht eines Moduls. --- `recht` darf eine Kennung oder eine Liste sein — bei einer Liste --- genügt EIN Recht daraus. Das brauchen Module, die mehrere Bereiche --- mit je eigenem Recht unter einem Dach zusammenfassen. --- @param mitarbeiter table Mitarbeiterdaten --- @param recht string|table|nil Erforderliches Recht --- @return boolean function Kern.DarfOeffnen(mitarbeiter, recht) if not recht then return true end if type(recht) == 'table' then for _, einzeln in ipairs(recht) do if Rechte.Hat(mitarbeiter, einzeln) then return true end end return false end return Rechte.Hat(mitarbeiter, recht) end --- Liefert alle Module, die ein Mitarbeiter sehen darf --- @param mitarbeiter table Mitarbeiterdaten (mit Rechten) --- @return table Liste function Kern.SichtbareModule(mitarbeiter) local liste = {} for _, modul in pairs(module) do if modul.aktiv and Kern.DarfOeffnen(mitarbeiter, modul.recht) then liste[#liste + 1] = { id = modul.id, name = modul.name, icon = modul.icon, reihenfolge = modul.reihenfolge, beschreibung = modul.beschreibung, } end end table.sort(liste, function(a, b) return a.reihenfolge < b.reihenfolge end) return liste end --- Liefert alle registrierten Module, auch die abgeschalteten. --- Nur für die Administration gedacht. --- @return table Liste function Kern.AlleModule() local liste = {} for _, modul in pairs(module) do -- Wie viele Aktionen hängen an diesem Modul? local aktionen = 0 for _ in pairs(handler[modul.id] or {}) do aktionen = aktionen + 1 end liste[#liste + 1] = { id = modul.id, name = modul.name, icon = modul.icon, recht = modul.recht, reihenfolge = modul.reihenfolge, beschreibung = modul.beschreibung, aktiv = modul.aktiv, aktionen = aktionen, } end table.sort(liste, function(a, b) return a.reihenfolge < b.reihenfolge end) return liste end --- Gibt es dieses Modul? --- @param modulId string Modul --- @return boolean function Kern.KenntModul(modulId) return module[modulId] ~= nil end --- Schaltet ein Modul an oder aus (Administration) --- @param modulId string Modul --- @param aktiv boolean Neuer Zustand function Kern.SetzeModulAktiv(modulId, aktiv) if module[modulId] then module[modulId].aktiv = aktiv and true or false end end --- Führt eine Aktion aus — inklusive Rechteprüfung. --- Wird von der Oberfläche über ein einziges Netz-Event aufgerufen. --- @param quelle number Server-ID --- @param mitarbeiter table Mitarbeiterdaten --- @param modulId string Modul --- @param aktion string Aktion --- @param daten table Nutzdaten --- @return any Ergebnis, string|nil Fehler function Kern.FuehreAus(quelle, mitarbeiter, modulId, aktion, daten) local modul = module[modulId] if not modul or not modul.aktiv then return nil, 'Dieses Modul ist nicht verfügbar.' end -- Recht auf das Modul selbst if not Kern.DarfOeffnen(mitarbeiter, modul.recht) then return nil, 'Dir fehlt die Berechtigung für diesen Bereich.' end local eintrag = handler[modulId] and handler[modulId][aktion] if not eintrag then return nil, 'Unbekannte Aktion.' end -- Zusätzliches Recht auf die einzelne Aktion if eintrag.recht and not Rechte.Hat(mitarbeiter, eintrag.recht) then return nil, 'Dir fehlt die Berechtigung für diese Aktion.' end local erfolg, ergebnis, fehler = pcall(eintrag.funktion, quelle, mitarbeiter, daten or {}) if not erfolg then Aether.Fehler(('Fehler in %s/%s: %s'):format(modulId, aktion, tostring(ergebnis))) return nil, 'Interner Fehler im Modul.' end return ergebnis, fehler end -- --------------------------------------------------------------------- -- Ereignisbus zwischen den Modulen -- -- Damit kann ein Modul auf Vorgänge eines anderen reagieren, ohne -- es zu kennen. Ausgelöst wird zusätzlich ein FiveM-Event, damit -- auch fremde Ressourcen mithören können. -- --------------------------------------------------------------------- --- Abonniert ein Ereignis --- @param ereignis string Name --- @param funktion function Rückruf function Kern.Abonniere(ereignis, funktion) abonnenten[ereignis] = abonnenten[ereignis] or {} abonnenten[ereignis][#abonnenten[ereignis] + 1] = funktion end --- Löst ein Ereignis aus --- @param ereignis string Name --- @param ... any Nutzdaten function Kern.Loese(ereignis, ...) for _, funktion in ipairs(abonnenten[ereignis] or {}) do local erfolg, fehler = pcall(funktion, ...) if not erfolg then Aether.Fehler(('Fehler im Abonnenten von "%s": %s'):format(ereignis, tostring(fehler))) end end -- Auch nach aussen melden, damit fremde Ressourcen reagieren können TriggerEvent('aether:medical:' .. ereignis, ...) end -- --------------------------------------------------------------------- -- Kennungen -- --------------------------------------------------------------------- --- Erzeugt eine fortlaufende Kennung im Format PRAEFIX-JAHR-NNNN --- @param praefix string z. B. 'BEH' --- @param tabelle string Tabelle --- @param spalte string Spalte mit der Kennung --- @return string function Kern.ErzeugeKennung(praefix, tabelle, spalte) local jahr = os.date('%Y') local muster = ('%s-%s-%%'):format(praefix, jahr) local anzahl = MySQL.scalar.await( ('SELECT COUNT(*) FROM %s WHERE %s LIKE ?'):format(tabelle, spalte), { muster }) or 0 -- Bei einer Kollision einfach weiterzählen for versuch = 1, 50 do local kandidat = ('%s-%s-%04d'):format(praefix, jahr, anzahl + versuch) local belegt = MySQL.scalar.await( ('SELECT 1 FROM %s WHERE %s = ?'):format(tabelle, spalte), { kandidat }) if not belegt then return kandidat end end return ('%s-%s-%s'):format(praefix, jahr, tostring(os.time()):sub(-5)) end