Version 3.19.0
Einbinden#
In der fxmanifest.lua der nutzenden Ressource:
luashared_scripts {
'@dv_core/import.lua',
'config.lua'
}
dependency 'dv_core'
Danach steht überall das globale DV zur Verfügung. Ohne import.lua geht auch alles über Exports (exports['dv_core']:Notify(...)), dann fehlen aber die vier blockierenden Funktionen — siehe Blockierende Funktionen.
dv_core muss vor der nutzenden Ressource starten.
Warum import.lua und nicht nur Exports#
Nicht nur der Bequemlichkeit wegen. Jeder Aufruf über die Ressourcengrenze — ob per exports oder per Funcref aus GetLib — serialisiert die Argumente, führt die Funktion in dv_cores Runtime aus und serialisiert das Ergebnis zurück. Diese Zeit erscheint in resmon auf der Zeile von dv_core, nicht auf der des Aufrufers.
Für alles mit gemeinsamem Zustand ist das der Preis der Sache: es gibt nur eine Punkteschleife, eine Hinweisleiste, eine Blip-Buchführung.
Für reines Rechnen wäre es Verschwendung — DV.Math.Distance ist eine Subtraktion und eine Wurzel, der Transport dorthin kostet ein Vielfaches. Deshalb baut import.lua diese Funktionen im Runtime der aufrufenden Ressource neu auf:
| lokal (kein Grenzübertritt) | über die Grenze (gemeinsamer Zustand) |
|---|
DV.Math.*, DV.Table.*, DV.String.* | DV.Points, DV.Interact, DV.TextUI |
DV.ToVector3, DV.Marker, DV.Draw3DText | DV.Notify, DV.Callback, DV.Blip |
DV.GetPed, DV.GetCoords, DV.GetServerId | DV.Cache('vehicle'), DV.Cache('seat') |
DV.Cache('ped'/'coords'/'heading') | DV.GetJob, DV.HasJob, DV.Framework |
Merksatz: Rechnen ist lokal, Zustand geht über die Grenze.
Praktische Folge: DV.Math.Distance in einer Schleife ist unbedenklich, DV.Points.Add in einer Schleife nicht. Wer dv_core nur über exports einbindet, zahlt den Übertritt auch für die Rechenfunktionen — ein weiterer Grund für import.lua.
Framework#
luaDV.Framework.Get() --> 'esx' | 'qb' | 'qbox' | 'standalone'
DV.Framework.Is('esx') --> boolean
DV.Framework.Exists() --> boolean, false bei standalone
DV.Framework.GetObject() --> Shared Object, oder nil
GetObject() gibt das rohe ESX-/QBCore-Objekt. Möglichst meiden — wer es benutzt, bindet sein Script wieder an ein Framework. Für alles Übliche gibt es die Funktionen unten.
Es ist eine Kopie, keine Verbindung. Der Export des Frameworks reicht seine Tabelle über die Ressourcengrenze, und dabei wird sie serialisiert. Felder, die sich beim Framework danach noch füllen — ESX.Jobs und ESX.Items kommen asynchron aus der Datenbank —, bleiben in der gemerkten Kopie für immer leer.
Funktionen überstehen den Weg dagegen als Funcref und laufen im Zustand des Frameworks. Wo es einen Getter gibt (ESX.GetJobs()), ist der die richtige Quelle, nicht das gleichnamige Feld. DV.Framework.GetObject(true) verwirft die Kopie und holt eine frische, wenn es keinen Getter gibt.
Spieler (Server)#
lualocal player = DV.GetPlayer(src)
-- { source, identifier, name, group, job = { name, label, grade, gradeLabel, onDuty } }
DV.GetPlayers() --> Liste von Server-Ids
DV.GetPlayersInRadius(coords, radius)
--> { { source, coords, distance }, ... } sortiert
DV.GetIdentifier(src) --> ESX-Identifier bzw. QB-citizenid, sonst license:
DV.GetName(src) --> string, Vor- und Nachname zusammengesetzt
DV.GetNameParts(src) --> firstname, lastname (zwei Rueckgabewerte)
DV.GetJob(src) --> { name, label, grade, gradeLabel, onDuty }
DV.HasJob(src, 'police', 3) --> boolean, dritter Parameter ist der Mindestgrad
DV.GetPlayersWithJob('police') --> { 3, 17, 42 }
DV.GetPlayersWithJob('ems', 2, true) --> ab Grad 2, nur im Dienst
GetPlayersWithJob gibt Server-Ids zurück. Der dritte Parameter ist auf ESX wirkungslos — dort kennt das Framework kein Dienst-Flag, onDuty ist immer true.
Das Ergebnis ist ein Schnappschuss, kein lebendes Objekt. Es hat keine Methoden — Metatabellen überstehen die Ressourcengrenze nicht.
Job setzen#
luaDV.SetJob(src, 'police', 3) --> boolean, false ohne Framework
DV.SetJobDuty(src, true) --> boolean, siehe unten
Einstellen, Befördern und Kündigen (Kündigen = auf den Standardjob des Servers setzen). Damit braucht ein Boss-Menü keine einzige ESX- oder QB-Zeile.
Auf ESX wird der Jobname vorher gegen DV.GetJobs() geprüft: xPlayer.setJob gibt nichts zurück und meldet einen Tippfehler nur in der Serverkonsole — der Spieler landet sonst kommentarlos als unemployed. Ist die Jobliste noch leer (kurz nach dem Start), wird gesetzt statt abgelehnt.
DV.SetJobDuty gibt auf ESX false zurück, und das ist kein Fehler. ESX kennt kein Dienst-Flag. Der Rückgabewert sagt „ins Framework gespiegelt", nicht „geklappt". Ein Script, das Dienst anbietet, führt den Zustand selbst und ruft dies nur, damit framework-eigene Scripts auf QB/QBox dasselbe sehen. Wer den Rückgabewert als Erfolgsmeldung liest, baut auf ESX einen Dienst, der sich nie anmelden lässt.
Namen setzen#
luaDV.SetName(src, 'Max', 'Mustermann') --> boolean, false ohne Framework
local first, last = DV.GetNameParts(src)
Setzt Vor- und Nachnamen im Framework und in der Datenbank. Auf ESX werden beide Wege bedient — die Spielervariablen firstName/lastName und der Anzeigename über setName. Nur eines von beiden zu füllen hinterlässt einen Spieler, der je nach Script zwei verschiedene Namen hat.
Das UPDATE ist kein Beiwerk. ESX' setName ändert nur den Speicher, und savePlayer sichert firstname/lastname nicht mit — die schreibt sonst nur esx_identity beim Anlegen des Charakters. Ohne den Datenbankschritt hieße der Spieler nach dem nächsten Serverneustart wieder wie vorher.
Das Wegschreiben läuft im Hintergrund, weil ein Export nicht warten darf. Der Rückgabewert sagt „im Speicher gesetzt"; scheitert die Datenbank, steht es in der Konsole. Läuft oxmysql nicht, gilt der Name nur bis zum nächsten Neustart — auch das wird gemeldet.
DV.GetNameParts liefert die Teile einzeln, für Eingabefelder und Protokolleinträge. Auf ESX fällt es auf eine Teilung am ersten Leerzeichen zurück, wenn der Charakter ohne esx_identity angelegt wurde.
Job-Katalog#
luaDV.GetJobs()
--> { { name = 'police', label = 'Polizei', grades = {
--- { grade = 0, label = 'Anwärter', salary = 200 },
--- { grade = 1, label = 'Officer', salary = 400 },
--- } }, ... }
Alle Jobs, die der Server kennt — für eine Job-Auswahl in einer eigenen Oberfläche. Nach Namen sortiert, Grade nach Grad. Gelesen wird aus ESX.Jobs bzw. Shared.Jobs (QBox über GetJobs()); ohne Framework kommt eine leere Liste zurück.
Die Grade werden normalisiert, weil ESX sie numerisch indiziert und QB mit Strings ('0', '1') — der Tabellenschlüssel taugt deshalb nicht als Gradzahl.
Wie bei DV.GetItems wird das Ergebnis gemerkt und beim Neustart des Frameworks verworfen; eine leere Liste wird nicht gemerkt, weil ESX.Jobs beim Serverstart einen Moment lang leer ist. DV.ForgetJobs() verwirft den Merker von Hand — nötig, wenn sich die Jobliste ändert, ohne dass eine Ressource neu startet.
Jobs anlegen und löschen#
luaDV.CreateJob('taxi', 'Taxiunternehmen', {
{ grade = 0, label = 'Fahrer', salary = 200 },
{ grade = 1, label = 'Disponent', salary = 400 },
{ grade = 2, label = 'Chef', salary = 800 },
}, function(ok, message)
print(ok, message)
end)
DV.DeleteJob('taxi', 'unemployed', function(ok, message) ... end)
DV.RefreshJobs() --> boolean, Framework liest seine Jobliste neu ein
Mit Callback und nicht mit Rückgabewert, weil der Datenbankzugriff wartet — und eine wartende Funktion lässt sich nicht über die Ressourcengrenze reichen. Dieselbe Grenze wie bei den blockierenden Funktionen, nur andersherum gelöst.
CreateJob ersetzt die Ränge, es ergänzt sie nicht: die übergebene Liste ist danach die vollständige. Sonst blieben beim Bearbeiten entfernte Ränge stehen.
Ein Job braucht mindestens einen Rang. ESX verwirft einen Job ohne Ränge beim nächsten Laden kommentarlos. Doppelte Rangnummern werden ebenfalls abgelehnt — sie überschrieben sich in ESX gegenseitig, und der erste wäre lautlos weg.
DeleteJob setzt alle Mitglieder auf den Auffangjob (Standard unemployed): verbundene Spieler über DV.SetJob, alle übrigen direkt in der users-Tabelle. Das zweite ist keine Kür — ESX greift ungeprüft auf ESX.Jobs[job] zu und wirft beim nächsten Login jedes Betroffenen einen Fehler.
oxmysql ist optional. dv_core bindet es nicht ein; gebraucht wird es nur von diesen beiden Funktionen, und der Zugriff läuft über den rohen Export. Läuft oxmysql nicht, sagen sie das im Klartext.
Auf QBCore läuft das Anlegen über AddJob und wirkt damit nur bis zum Neustart — QBCore liest seine Jobs aus qb-core/shared/jobs.lua, und die schreibt niemand. Die Meldung sagt das ausdrücklich, statt einen Erfolg zu behaupten.
Kaputte Jobs finden#
luaDV.GetJobsWithoutGrades(function(names)
-- { 'off_police', 'off_mechanic' }
end)
Jobs, die in der Ablage stehen, aber keinen Rang haben. Das ist die Ursache hinter
[es_extended] [WARNING] Ignoring job "off_police" due to no job grades found
ESX verwirft solche Jobs bei jedem Laden: sie tauchen in DV.GetJobs nicht auf, lassen sich nicht vergeben, und ein Charakter, dessen users.job darauf zeigt, läuft beim Login in einen nil-Zugriff. Meist sind es Reste eines deinstallierten Job-Scripts, das seine Ränge mitgenommen und den Job stehengelassen hat.
Abhilfe: Ränge ergänzen (DV.CreateJob mit demselben Namen) oder den Job löschen (DV.DeleteJob, das fängt auch die betroffenen Charaktere auf).
Rechte#
luaDV.HasGroup(src, 'admin') --> boolean
DV.HasGroup(src, { 'mod', 'admin' }) --> boolean
Prüft drei Wege nacheinander: ESX-Gruppe, QBCore-Permission und zuletzt die FiveM-eigenen ACE-Rechte (dv.admin oder admin). Damit funktioniert die Abfrage auch ganz ohne Framework.
Geld#
luaDV.GetMoney(src, 'cash') --> number
DV.AddMoney(src, 500, 'bank') --> boolean
DV.RemoveMoney(src, 500, 'cash') --> boolean, false bei zu wenig Guthaben
Konten heißen immer cash, bank, black (Standard cash). Intern wird auf money/bank/black_money (ESX) bzw. cash/bank/crypto (QB) übersetzt.
Fraktionskassen#
luaDV.GetSocietyMoney('police') --> number|nil
DV.AddSocietyMoney('police', 500) --> boolean
DV.RemoveSocietyMoney('police', 500) --> boolean, false bei zu wenig Guthaben
DV.GetSocietySystem() --> 'jobs_creator'|'esx'|'qb'|'none'
Der Parameter ist immer der blanke Jobname ('police'), nie ein Kontoname. Wie daraus ein Konto wird, entscheidet der Adapter — und genau daran vertut man sich sonst:
| System | Kontoname |
|---|
jobs_creator (Jaksam) | blanker Jobname |
esx_addonaccount | society_ + Jobname |
qb-management | blanker Jobname |
Wer den Namen im Aufrufer selbst zusammensetzt, bucht auf einem Server mit dem anderen System auf ein Konto, das es nicht gibt — ohne Fehlermeldung.
Erkannt wird automatisch, in der Reihenfolge oben. Einstellbar über Config.Society: mode (auto | jobs_creator | esx | qb | none), resource für einen abweichenden Ressourcennamen — jobs_creator lässt sich umbenennen, und unter einem eigenen Namen findet die Suche ihn nie — und prefix, falls der Kontoname anders aufgebaut ist.
Ohne Kassensystem liefern die Funktionen nil/false und melden das einmal im Log. Der Aufrufer weicht dann sinnvollerweise auf ein privates Konto aus, statt zu scheitern. RemoveSocietyMoney gibt false zurück, wenn zu wenig in der Kasse ist — wer den Rückgabewert nicht prüft, gibt Ware heraus, die niemand bezahlt hat.
Items#
luaDV.HasItem(src, 'wasser', 2) --> boolean
DV.GetItemCount(src, 'wasser') --> number
DV.AddItem(src, 'wasser', 1, meta) --> boolean
DV.RemoveItem(src, 'wasser', 1) --> boolean
Läuft ox_inventory, wird es bevorzugt — sonst das Inventar des Frameworks.
Waffen und Geld: Gegenstand oder nicht?#
Die dv-Scripts geben beides als Gegenstand aus — eine Pistole als weapon_pistol, Schwarzgeld als black_money. Mit ox_inventory stimmt das. Auf einem Inventar mit Items in der Datenbank stimmt es nicht: dort steckt die Waffe im Loadout und das Geld auf einem Konto, einen Gegenstand dieses Namens gibt es gar nicht.
Config.Items sagt einmal, wie es auf diesem Server läuft. dv_core legt danach die Weiche — für alle dv-Scripts zugleich, ohne dass eines davon etwas wissen muss.
luaConfig.Items = {
weapons = 'auto', -- 'auto' | 'item' | 'native'
money = 'auto', -- 'auto' | 'item' | 'account'
moneyItems = { cash = 'money', black = 'black_money' },
}
Was 'auto' daraus macht:
| Lage | weapons | money |
|---|
ox_inventory läuft | Gegenstand | Konto |
| ESX ohne ox | Loadout | Konto |
| QB/QBox ohne ox | Gegenstand | Konto |
| ohne Framework | Loadout (über den Client) | Konto |
Bei ox trotzdem „Konto": ox bucht das Konto und schreibt den Gegenstand selbst mit fort. Über das Konto zu gehen ist dort also richtig, nicht bloß geduldet.
moneyItems gilt in beide Richtungen. DV.AddItem(src, 'black_money', 2000) landet auf dem Konto, wenn Geld hier keines ist — und DV.AddMoney(src, 2000, 'black') gibt den Gegenstand aus, wenn es einer ist.
weapons = 'native' und ox_inventory vertragen sich nicht. Läuft ox, legt ESX seine gesamte Waffen-Schnittstelle still (addWeapon wird zur leeren Funktion, hasWeapon sagt immer nein). Alles Ausgegebene wäre lautlos verloren. dv_core meldet diese Kombination beim Start.
Zwei Dinge sind bei einer nativen Waffe anders als bei einem Gegenstand:
- Die Stückzahl zählt nicht. Ein Ped kann keine drei Pistolen tragen; aus
count = 3 wird eine Waffe. - Wer sie schon hat, bekommt
false. ESX' addWeapon tut in dem Fall nichts — ein true darauf wäre der stille Fehlschlag, gegen den die Rückgabewert-Regel steht. Der Aufrufer erstattet dann den Kauf bzw. nimmt die Belohnung zurück.
Munition und Aufsätze reisen als metadata mit, in beide Richtungen:
luaDV.AddItem(src, 'weapon_pistol', 1, { ammo = 50, components = { 'clip_extended' } })
Ohne Loadout-Schnittstelle (QB ohne Waffen-Items, Betrieb ganz ohne Framework) gibt dv_core die Waffe über den Client aus und merkt sich serverseitig, was draußen ist. Das überlebt keinen Reconnect — native Waffen ohne Framework tun das aber ohnehin nicht.
luaDV.ItemKind('weapon_pistol') --> 'weapon', 'WEAPON_PISTOL'
DV.ItemKind('black_money') --> 'money', 'black_money', 'black'
DV.ItemKind('wasser') --> 'item', 'wasser'
Für Aufrufer, die es wissen müssen, statt zu raten: ein Laden darf eine Waffe nicht stückweise verkaufen, wenn sie keine ist, und eine Beschriftung sieht anders aus, wenn ein Betrag dahintersteht.
Eigenes Inventar oder Geldsystem#
Für ESX, QBCore, QBox und ox_inventory ist nichts einzutragen. Wer eine Eigenentwicklung fährt, hinterlegt sie einmal in Config.Inventory; alle dv-Scripts folgen dann.
luaConfig.Inventory = {
addItem = function(src, item, count, metadata)
return exports['mein_inventar']:AddItem(src, item, count) == true
end,
-- removeItem, addMoney, removeMoney, items — alle optional
}
Dasselbe Muster wie Config.Output: nicht gesetzt = dv_core macht es selbst, ein Handler im pcall, und wer einmal wirft, wird gemeldet und danach übergangen. Eine aktive Übersteuerung steht beim Start als Warnzeile in der Konsole.
Der Rückgabewert ist keine Formsache. true nur, wenn es wirklich geklappt hat — die Scripts nehmen bei false eine Vergabe wieder zurück oder brechen einen Kauf ab. Wer immer true liefert, verschenkt Ware.
Die Handler bekommen die Kontonamen in der abstrakten Form (cash, bank, black), nicht unter dem Namen des Frameworks — die Übersetzung entfällt, sobald sie übernehmen.
luaDV.GetInventory(src)
--> { { name = 'wasser', count = 3, label = 'Wasser', metadata = ... }, ... }
Was ein Spieler gerade trägt, nach Namen sortiert — für alles, was ein zweites Behältnis anbietet: Lager, Kofferraum, Schließfach. Ohne diese Funktion müsste der Aufrufer über DV.GetItems laufen und für jedes Item des Servers einzeln DV.GetItemCount fragen; auf einem großen Server sind das vierstellig viele Abfragen bei jedem Öffnen.
Posten mit count = 0 kommen nicht vor. Das ist kein Detail: xPlayer.getInventory() führt auf ESX jedes bekannte Item mit 0 mit, und ungefiltert bekäme man den gesamten Katalog als „Bestand".
Sind Waffen auf diesem Server keine Gegenstände, stehen sie trotzdem mit in der Liste — mit count = 1 und Munition, Aufsätzen und Lackierung in metadata. Ohne das zöge ein Gefängnis-Script dem Häftling die Gegenstände aus und ließe ihm die Pistole.
luaDV.GetItems() --> { { name = 'wasser', label = 'Wasser', weight = 200 }, ... }
Alle Items, die der Server kennt — nicht, was ein Spieler trägt. Nach Namen sortiert, für eine Item-Auswahl in einer eigenen Oberfläche. Gelesen wird aus ox_inventory (dessen data/items.lua über den Items()-Export, ohne die Datei anzufassen), sonst aus ESX.Items (Datenbanktabelle items) bzw. Shared.Items. Ohne Quelle kommt eine leere Liste zurück.
Das Ergebnis wird gemerkt und erst verworfen, wenn das Inventar oder das Framework neu startet. Beim Serverstart ist ESX.Items einen Moment lang leer — eine leere Liste wird deshalb nicht gemerkt, der nächste Aufruf liest neu.
weight fehlt, wenn das Inventar keines führt. Die Einheit ist die des Inventars: ox_inventory und QBCore rechnen in Gramm, ein ESX-Inventar in seiner eigenen. dv_core rechnet nichts um — wer eine Zahl anzeigt, weiß, welches Inventar auf seinem Server läuft.
luaDV.RegisterUsableItem('verbandskasten', function(src, item)
if DV.RemoveItem(src, item) then ... end
end)
Registriert ein benutzbares Item, egal ob ESX, QBCore oder QBox dahinter liegt. Im Standalone-Betrieb kommt false zurück — ohne Inventar gibt es nichts zu benutzen.
Kofferraum und Handschuhfach#
Was in der Fahrzeugablage liegt — nur lesend, und ohne dass das fragende Script wissen muss, welches Inventar der Server fährt.
luaDV.GetVehicleStorage('AB 123 CD', 'trunk', function(storage)
if not storage then return end -- dieser Server führt keine Fahrzeugablagen
for _, item in ipairs(storage.items) do
print(item.label, item.count, item.weight)
end
end)
Zweiter Parameter: 'trunk' (Standard) oder 'glovebox'; er darf entfallen, dann ist der Callback das zweite Argument. Die Antwort:
| Feld | |
|---|
items | { name, label, count, slot, weight, metadata }, nach Slot sortiert |
count | Stückzahl über alle Posten |
slots / maxSlots | belegte Plätze, und wie viele es gibt (wenn bekannt) |
weight / maxWeight | Gesamtgewicht und Grenze (wenn bekannt) |
source | 'live' | 'db' | 'custom' |
nil heißt „dieser Server kann das nicht", eine Tabelle mit leerem items heißt „der Kofferraum ist leer". Das ist ein Unterschied: eine Oberfläche blendet die Funktion im ersten Fall aus, statt einen immer leeren Kofferraum zu zeigen.
luaDV.HasVehicleStorage(function(possible) ... end)
Woher die Antwort kommt — und warum die Reihenfolge zählt#
- Das laufende Inventar, solange das Fahrzeug in der Welt steht. Das ist der Stand von jetzt.
- Die Datenbank, wenn es das nicht tut. Für ein eingeparktes Fahrzeug ist das die einzige Quelle.
Der Umweg ist keine Bequemlichkeit. Ein Inventar hält eine geöffnete Ablage im Speicher und schreibt sie erst im Takt weg — wer nur die Datenbank liest, sieht den Stand von vorhin, bei einem gerade gefüllten Kofferraum also einen leeren.
Gefunden wird die Ablage selbst: ox_inventory über seinen Export, sonst eine Spalte trunk/glovebox in der Fahrzeugtabelle (owned_vehicles, player_vehicles, vehicles, nd_vehicles), sonst die Tabellen trunkitems/gloveboxitems älterer QB-Inventare. Ein Inventar, das keinen dieser Wege geht, wird einmal in Config.Inventory.vehicleStorage hinterlegt.
Vor dem Verschwinden sichern#
luaDV.SaveVehicleStorage(plate, 'all', function(saved) ... end)
Zu rufen, bevor ein Fahrzeug aus der Welt verschwindet — beim Einparken, beim Abschleppen, beim Löschen. Das Inventar schreibt seine Ablage dann weg und gibt sie frei.
Danach ist es zu spät. Eine Fahrzeugablage hängt am Fahrzeug; ist das Entity gelöscht, kann kein Inventar sie mehr aufschließen. In der Datenbank stünde dann der Stand des letzten Speichertakts — und der ist bei einem gerade gefüllten Kofferraum leer.
Geschrieben wird von dv_core nichts. Es liest, und es bittet das Inventar, seinen eigenen Stand zu sichern. Ein fremder Schreibzugriff auf eine Inventarablage wäre beim nächsten Speichern des Inventars spurlos weg — mitsamt dem Gegenstand, den jemand hineingelegt hat.
Player-Events#
Statt esx:playerLoaded und QBCore:Server:PlayerLoaded zu behandeln, reicht eines:
lua-- Server
AddEventHandler('dv_core:playerLoaded', function(src) ... end)
AddEventHandler('dv_core:playerDropped', function(src, reason) ... end)
Spieler (Client)#
luaDV.GetPlayerData() --> { source, identifier, name, job = {...} }
DV.GetJob() --> { name, label, grade, gradeLabel, onDuty }
DV.HasJob('police', 3) --> boolean, zweiter Parameter ist der Mindestgrad
DV.IsPlayerLoaded() --> boolean, Charakter ausgewählt?
DV.OnPlayerLoaded(function() ... end)
OnPlayerLoaded läuft sofort, wenn der Spieler schon geladen ist, sonst beim Laden — und bei jedem weiteren (Charakterwechsel). Dazu die normalisierten Events:
luaAddEventHandler('dv_core:playerLoaded', function() ... end)
AddEventHandler('dv_core:playerUnloaded', function() ... end)
AddEventHandler('dv_core:jobChanged', function(job) ... end) -- Format wie DV.GetJob()
Callbacks#
Ersetzt ESX.RegisterServerCallback und QBCore.Functions.CreateCallback.
lua-- Server
DV.Callback.Register('shop:getStock', function(src, shopId)
return Stock[shopId], os.time()
end)
-- Client, blockierend (nur mit import.lua)
local stock, zeit = DV.Callback.Await('shop:getStock', 'ammunation')
-- Client, ohne import.lua
DV.Callback.Send('shop:getStock', function(stock, zeit)
print(stock, zeit)
end, 'ammunation')
Serverseitige Callbacks bekommen als ersten Parameter immer die Server-Id des Anfragenden. Der Rückweg ist symmetrisch: DV.Callback.Register gibt es auch auf dem Client, DV.Callback.Send(name, playerId, cb, ...) auf dem Server.
Ohne Antwort läuft die Anfrage nach 10 Sekunden in eine Zeitüberschreitung und der Callback wird ohne Argumente gerufen — der aufrufende Thread hängt also nie fest.
Meldungen#
lua-- Client
DV.Notify('success', 'Titel', 'Text', 4500)
DV.Announce('info', 'Titel', 'Text', 8000)
DV.HelpNotify('Drücke E', 'info') -- blendet sich selbst aus
DV.HelpNotifyHide()
-- Server
DV.Notify(src, 'error', 'Titel', 'Text', 4000)
DV.NotifyAll('info', 'Titel', 'Text')
DV.Announce(src, ...) / DV.AnnounceAll(...)
src = 0 ist die Serverkonsole — dort gibt es keinen Client. DV.Notify und DV.Announce schreiben die Meldung dann auf die Konsole, statt in einen Native-Fehler zu laufen. Ein Admin-Befehl, der auch aus der Konsole aufgerufen werden kann, braucht deshalb kein if src > 0. -1 (alle Verbundenen) bleibt davon unberührt.
Typen: success, error, info, warning (bei HelpNotify danger statt error). Farben und Dauer sind im Spiel über /dvcoresettings einstellbar (nur Admins).
Der Meldungstext darf \n enthalten — das ergibt einen echten Zeilenumbruch. Praktisch für einen Hinweis, der am Zeilenende sonst untergeht:
luaDV.Notify(src, 'info', 'Türklingel',
'Max klingelt an der Tür.\n➜ Drücke [ Y ] zum Antworten')
Gilt für Notify und Announce. Titel und TextUI bleiben einzeilig.
Fortschrittsbalken#
lua-- blockierend (nur mit import.lua)
if DV.Progress('default', 'Schloss knacken', '🔓', 5000, true) then
print('durchgelaufen')
else
print('abgebrochen')
end
-- ohne import.lua
DV.ProgressStart('default', 'Schloss knacken', '🔓', 5000, true, function(fertig)
print(fertig)
end)
DV.ProgressStop() -- bricht ab, onDone bekommt false
DV.ProgressActive() --> boolean
Bei cancelable = true bricht E den Balken ab. Typen: default, success, warning, error.
Eigenes Notify- oder Progressbar-System einhängen#
Wer bereits ein anderes System betreibt, hinterlegt es einmal in Config.Output der config.lua — ab dann laufen alle Meldungen aller dv-Scripts dorthin. Kein Script muss angefasst werden, auch Meldungen vom Server nicht: die gehen zur Anzeige ohnehin zum Client, und dort greift die Weiche.
luaConfig.Output = {
notify = function(data)
-- data = { type, title, message, duration }
TriggerEvent('mein_notify:zeigen', data.type, data.message, data.duration)
end,
progress = function(data, done)
-- data = { type, label, icon, duration, cancelable }
done(exports['mein_progress']:Start(data.label, data.duration))
end,
}
Es ist bewusst eine Funktion und kein bloßer Eventname: die Systeme da draußen erwarten völlig verschiedene Parameter — mal (text, typ), mal eine Tabelle, mal (titel, text, dauer, typ). Eine feste Reihenfolge in der Config passte auf keines davon.
| Schlüssel | Signatur | Daten |
|---|
notify | (data) | type, title, message, duration |
announce | (data) | type, title, message, duration |
help / helpHide | (data) / () | text, type |
textui / textuiHide | (data) / () | text, type, key |
progress | (data, done) | type, label, icon, duration, cancelable |
progressStop | () | — |
Nicht gesetzte Schlüssel behalten die Anzeige von dv_core; man kann also auch nur das Notify tauschen und die Progressbar behalten.
Beim Fortschrittsbalken gilt zusätzlich:
- Der Handler läuft in einem eigenen Thread und darf blockieren — eine Progressbar, die erst am Ende zurückkehrt, ist hier also erlaubt.
done(true) bei durchgelaufen, done(false) bei abgebrochen. Erst dieser Aufruf weckt ein wartendes DV.Progress. Bleibt er aus, greift nach duration + 15 s eine Notbremse.- Solange ein Handler gesetzt ist, misst dv_core die Zeit nicht selbst und wertet auch die Abbruchtaste nicht aus — beides gehört dann dem fremden System.
Wirft ein Handler einen Fehler, meldet dv_core das einmal in der Konsole und schaltet für diesen Ausgang zurück auf die eigene Anzeige. Eine Meldung geht also nie verloren, nur weil ein fremdes System klemmt.
Töne#
lua-- Client
local id = DV.PlaySound({ file = 'dv_doorbell/sounds/ding.ogg', volume = 0.7 })
DV.StopSound(id)
DV.StopSound() -- alle Töne der eigenen Ressource
-- Server, wie DV.Notify mit der Ziel-Id davor
DV.PlaySound(src, { file = 'dv_doorbell/sounds/ding.ogg' })
DV.PlaySound(-1, { tones = {...}, id = 'sirene', loop = true })
DV.StopSound(-1, 'sirene')
Der Dateipfad trägt den Ressourcennamen vorne (dv_doorbell/sounds/ding.ogg) und die Datei muss dort in files{} stehen. Netzadressen sind bewusst nicht erlaubt.
Statt einer Datei geht auch eine Tonfolge — dann braucht das Script überhaupt keine Audiodatei, und der Ton steht in seiner config.lua:
luaDV.PlaySound({ volume = 0.7, tones = {
{ freq = 659.25, duration = 0.45, decay = 'bell' }, -- Ding
{ freq = 523.25, duration = 0.90, decay = 'bell' }, -- Dong
} })
Ein Tonschritt: freq (Pflicht, 20–20000 Hz), freq2 (leiserer zweiter Oszillator — macht aus einem Piepton eine Glocke), duration und gap in Sekunden, volume (0–1), wave (sine (Standard) | square | sawtooth | triangle) und decay: bell klingt über die ganze Dauer aus, flat hält den Ton und bricht am Ende ab. Höchstens 64 Schritte je Aufruf.
loop wiederholt, bis DV.StopSound kommt; dafür lohnt ein eigener id statt der laufenden Nummer — vor allem serverseitig, wo die vom Client vergebene Nummer unbekannt ist. Läuft derselbe id schon, wird er ersetzt statt übereinandergelegt. Beim Stoppen der Ressource verstummen ihre Töne von selbst.
TextUI#
luaDV.TextUI('Drücke E zum Öffnen', 'info', 'E')
DV.HideTextUI()
DV.IsTextUIOpen() --> boolean
DV.TextUIOwner() --> string|nil welche Ressource die Leiste gerade hält
Unterschied zu HelpNotify: TextUI bleibt stehen, bis es ausgeblendet wird. Richtig für „solange du in der Zone stehst"-Hinweise. Mehrfachaufrufe mit gleichem Inhalt kosten nichts, der Aufruf darf also in einer Schleife stehen.
Es gibt nur eine Leiste, und sie hat einen Besitzer. Wer sie anfordert, bekommt sie, solange sie frei ist. Ist sie belegt, wird die Anfrage nur gemerkt — es blinkt nichts, und sobald der bisherige Besitzer freigibt, rückt der nächste Anwärter sofort nach, ohne dass er erneut melden muss. DV.HideTextUI blendet immer nur die eigene Leiste aus, nie die eines fremden Scripts; DV.IsTextUIOpen beantwortet entsprechend „habe ich gerade eine Leiste angemeldet".
Der Besitzer ist die aufrufende Ressource, ermittelt über @dv_core/import.lua. Ein Script braucht dafür nichts zu tun. Beim Stoppen einer Ressource wird ihre Leiste freigegeben.
Zwei Punkte desselben Scripts teilen sich damit einen Besitzer — wer dicht beieinander liegende eigene Punkte hat, führt weiterhin selbst Buch darüber, welcher davon gerade dran ist. DV.Interact macht das intern bereits.
Eingabedialog#
lualocal values = DV.InputAwait('Charakter anlegen', {
{ type = 'text', label = 'Vorname', required = true, maxLength = 24 },
{ type = 'number', label = 'Alter', min = 18, max = 99 },
{ type = 'select', label = 'Beruf', options = { 'Polizei', 'Sanitäter' } },
{ type = 'checkbox', label = 'AGB gelesen' },
{ type = 'textarea', label = 'Notiz', placeholder = 'optional' },
})
if not values then return end -- abgebrochen
print(values[1], values[2]) -- Reihenfolge der Felder
Feldtypen: text, number, password, textarea, select, checkbox. Gemeinsame Schlüssel: label, default, required, placeholder.
Ohne import.lua: DV.Input(titel, felder, cb) — cb(values) bzw. cb(nil) bei Abbruch.
lualocal item = DV.MenuAwait({
title = 'Werkstatt',
items = {
{ label = 'Reparieren', icon = '🔧', description = 'Kostet 500$', value = 'repair' },
{ label = 'Lackieren', icon = '🎨', submenu = {
title = 'Farbe wählen',
items = {
{ label = 'Rot', value = 'red' },
{ label = 'Blau', value = 'blue' },
},
} },
{ label = 'Tunen', disabled = true },
},
})
if not item then return end -- abgebrochen
print(item.label, item.value)
Bedienung: ↑↓ und Enter, ESC/Backspace geht eine Ebene zurück bzw. schließt. Maus funktioniert genauso.
Das Menü steht rechts. position = 'left' schiebt es auf die andere Seite — mitsamt der Einblend-Richtung, denn ein Menü, das links steht und von rechts hereinfährt, wirkt wie ein Fehler:
luaDV.MenuAwait({ title = 'Werkstatt', position = 'left', items = { ... } })
Einträge: label (Pflicht), description, icon (Emoji), disabled, value, data, submenu, onSelect. Ein onSelect am Eintrag läuft zuerst; der Menü-Callback (bzw. MenuAwait) bekommt die Auswahl danach ebenfalls.
Ohne import.lua: DV.Menu(daten, cb) — cb(item, path) bzw. cb(nil) bei Abbruch. Dazu DV.CloseMenu() und DV.IsMenuOpen().
Punkte und Zonen#
Der größte Performance-Gewinn. Eine einzige Schleife bedient alle Punkte aller Scripts.
lualocal id = DV.Points.Add({
coords = vector3(-1035.7, -2731.5, 12.8),
distance = 5.0,
onEnter = function(point) DV.TextUI('Drücke E', 'info', 'E') end,
onExit = function(point) DV.HideTextUI() end,
nearby = function(point) DV.Marker(point.coords) end, -- jeden Frame
})
DV.Points.Remove(id)
DV.Points.IsInside(id) --> boolean
Für eine Box statt einer Kugel:
luaDV.Points.Add({
coords = vector3(...),
shape = 'box',
size = vector3(6.0, 4.0, 3.0),
rotation = 45.0, -- Grad um die Z-Achse
onEnter = ...,
})
nearby läuft jeden Frame und ist nur für das Zeichnen gedacht. Wer es weglässt, kostet fast nichts: dann läuft nur noch der gemeinsame Scan, und der taktet sich nach dem Abstand zur nächsten Punktgrenze zwischen 100 ms und 500 ms ein. Wer mitten in einem 400-Meter-Gebiet steht, ist von jedem Zustandswechsel weit entfernt und wird entsprechend selten geprüft; zwei Meter vor einem Punktrand entsprechend oft.
Was die Callbacks bekommen#
onEnter und onExit bekommen den vollständigen Punkt. nearby bekommt eine Sicht darauf — dieselben Feldnamen mit denselben Bedeutungen, aber ohne die Callbacks und ohne die Interna:
| Feld | Bedeutung |
|---|
id | die ID aus DV.Points.Add |
coords | Mittelpunkt |
distance | der konfigurierte Radius |
currentDistance | die aktuelle Entfernung des Spielers |
shape, size, rotation | Form wie angelegt |
data | die eigenen Nutzdaten |
inside | ob der Punkt gerade betreten ist |
Der Grund ist Rechenzeit: nearby geht in jedem Bild über die Ressourcengrenze, und alles, was als Argument mitgeht, wird dabei serialisiert. Der volle Punkt trägt drei Funktionsreferenzen mit sich — sechzigmal pro Sekunde, für Felder, die kein Callback je liest.
Wer bisher point.coords, point.data, point.id oder point.currentDistance gelesen hat, liest unverändert weiter.
nearby ist teuer, auch wenn es leer ist#
Der Sprung über die Ressourcengrenze kostet unabhängig davon, was im Callback passiert — ein leerer nearby kostet dasselbe wie ein voller. Eine Drosselung im Callback ändert daran nichts, der Sprung ist vorher schon passiert.
Bei großen Punkten (Tankstellenvorplatz, Zone) lohnt deshalb das andere Muster: onEnter und onExit setzen ein Flag, und die eigentliche Arbeit läuft in einem eigenen Thread im Runtime der eigenen Ressource, wo ein Durchgang nichts kostet. Zwei Übertritte je Besuch statt sechzig je Sekunde.
Punkte werden beim Stoppen der anlegenden Ressource automatisch entfernt.
Interaktionen#
Marker + Hinweis + Taste in einem Aufruf — der Block, der vorher in jedem Gameplay-Script von Hand stand:
lualocal id = DV.Interact.Add({
coords = vector3(-1035.7, -2731.5, 12.8),
radius = 2.0,
text = 'Tresor öffnen',
job = 'police', minGrade = 2, -- optional
canInteract = function(info) return not busy end, -- optional
onInteract = function(info)
if DV.Progress('default', 'Öffnen...', '🔓', 4000, true) then ... end
end,
})
DV.Interact.Remove(id)
DV.Interact.RemoveAll() -- alle der eigenen Ressource
Statt einer Position geht auch ein Entity-Ziel — damit ist der übliche Target-Anwendungsfall ohne Fremdressource abgedeckt:
luaDV.Interact.Add({
models = { 'prop_atm_01', 'prop_atm_02' },
entityType = 'object', -- 'object' (Standard) | 'ped' | 'vehicle'
text = 'Konto abfragen',
onInteract = function(info) print(info.entity) end,
})
DV.Interact.Add({
entity = pedHandle, -- konkretes Entity statt Modellsuche
text = 'Reden',
key = 'G',
onInteract = function(info) ... end,
})
Weitere Schlüssel: key ('E' Standard, auch F/G/H/X/SPACE/ENTER oder eine Control-Id als Zahl plus keyLabel), type (TextUI-Stil), drawDistance (Marker sichtbar ab, Standard 12.0), marker (Optionen wie bei DV.Marker, false = keiner; Standard: Marker bei Positionen, keiner bei Entities), cooldown (ms, Standard 800), data (landet in info.data).
onInteract/canInteract bekommen info = { id, entity, coords, distance, data }. Positionsziele laufen über die Points-Schleife mit, Entity-Ziele über eine eigene Suche im 250/500-ms-Takt. Beim Stoppen der Ressource räumt sich alles selbst auf.
Welt#
luaDV.Marker(coords, { -- pro Frame aufrufen
type = 1,
size = vector3(1.0, 1.0, 0.6),
colour = { r = 168, g = 156, b = 255, a = 120 },
})
local blip = DV.Blip(coords, {
sprite = 108, colour = 2, scale = 0.8,
label = 'Werkstatt', shortRange = true,
})
DV.BlipArea(coords, 50.0, { colour = 1, alpha = 128 })
DV.RemoveBlip(blip)
DV.Draw3DText(coords, 'Text', 1.0, 0) -- pro Frame aufrufen
DV.Draw3DText(coords, 'Text', 1.0, 0, 25.0) -- eigene Sichtweite
DV.Draw3DText zeichnet ab 50 m Entfernung gar nicht erst (fünfter Parameter überschreibt das).
Blips verschwinden beim Stoppen der anlegenden Ressource von selbst.
Entitäten#
luaDV.GetNearbyPlayers(radius) --> { { serverId, ped, coords, distance }, ... }
DV.GetClosestPlayer(radius) --> Einzeleintrag oder nil
DV.GetNearbyPeds(radius, includePlayers, origin, models)
DV.GetNearbyVehicles(radius, origin, models)
DV.GetNearbyObjects(radius, { 'prop_barrel_02a' })
DV.GetClosestPed(radius, includePlayers, origin, models)
DV.GetClosestVehicle(radius, origin, models)
DV.GetClosestObject(radius, models)
Alle Listen sind nach Entfernung sortiert. Format (außer bei Spielern): { entity, coords, distance }. models (Namen oder Hashes) schränkt auch Peds und Fahrzeuge auf bestimmte Modelle ein; origin verlegt den Suchmittelpunkt (Standard: eigene Position).
Fahrzeuge und NPCs#
lua-- Spawnen wartet auf das Modell → nur mit import.lua, im CreateThread
local vehicle = DV.SpawnVehicle('adder', coords, 90.0, {
plate = 'DV 001',
properties = gespeicherteProps, -- Format von GetVehicleProperties
warp = true, -- Spieler auf den Fahrersitz
})
local props = DV.GetVehicleProperties(vehicle) -- speicherbar per DV.JSON
DV.SetVehicleProperties(vehicle, props)
DV.DeleteVehicle(vehicle)
local ped = DV.SpawnPed('a_m_m_business_01', coords, 180.0, {
scenario = 'WORLD_HUMAN_CLIPBOARD',
-- oder: anim = { dict = ..., name = ..., flag = 1 }
})
DV.ConfigurePed(ped, { frozen = true, invincible = true }) -- macht SpawnPed schon selbst
DV.DeletePed(ped)
SpawnVehicle ist vernetzt (networked = false schaltet ab), SpawnPed ist bewusst lokal — Kulissen-NPCs brauchen kein Netzwerk. Beide räumen sich beim Stoppen der eigenen Ressource selbst auf; persistent = true schaltet das ab.
Eigenschaften decken den gängigen Satz ab: Kennzeichen, Farben, Perlmutt/Felgen, Fenstertönung, Neon, Turbo, Extras, Livery und alle Mod-Slots 0–48.
Raycast#
lualocal hit = DV.RaycastFromCamera(10.0) -- worauf zeigt der Spieler?
-- { hit, coords, normal, entity, entityType = 'ped'|'vehicle'|'object'|nil }
DV.Raycast(von, nach, flags, ignoriertesEntity)
Synchron (kein Warten über Frames) — gedacht für einzelne Abfragen wie Platzierung, nicht für Dutzende pro Frame.
Cache#
luaDV.Cache('ped') --> Ped-Handle
DV.Cache('vehicle') --> Fahrzeug oder nil
DV.Cache('seat') --> Sitzindex oder nil
DV.Cache('serverId')
DV.Cache('coords') --> live gelesen
DV.Cache() --> alle Werte als Tabelle
-- Kurzformen
DV.GetPed() DV.GetCoords() DV.GetVehicle() DV.GetServerId()
Funktion, kein Feld. DV.Cache.ped gäbe es zwar syntaktisch, wäre in der nutzenden Ressource aber ein eingefrorener Wert vom Ladezeitpunkt.
Wie aktuell die Werte sind. coords und heading werden live gelesen, ped notfalls sofort nachgeholt. vehicle und seat werden alle 250 ms nachgezogen — nach dem Einsteigen können sie also bis zu 250 ms hinterherhinken. Wer den Moment auf den Frame genau braucht, fragt GetVehiclePedIsIn selbst ab. serverId und playerId stehen für die ganze Session fest.
Streaming und Animationen#
lualocal hash = DV.RequestModel('prop_barrel_02a') --> Hash oder nil
DV.RequestAnimDict('amb@world_human_hang_out_street@male_c@base')
DV.RequestTextureDict('commonmenu')
DV.RequestPtfxAsset('core')
DV.PlayAnim(dict, name, {
duration = 3000,
flag = 49,
ped = PlayerPedId(),
})
DV.StopAnim(dict, name)
Alle Request*-Funktionen brechen nach 5 Sekunden ab und melden das — anders als die übliche while not HasAnimDictLoaded(dict) do Wait(0) end-Schleife, die bei einem Tippfehler stumm für immer hängt.
Befehle und Tasten#
lua-- Server
DV.AddCommand('gebgeld', {
help = 'Gibt einem Spieler Geld',
groups = { 'admin' },
params = {
{ name = 'id', help = 'Server-Id', type = 'number' },
{ name = 'betrag', help = 'Wie viel?', type = 'number' },
},
}, function(src, args)
DV.AddMoney(args.id, args.betrag)
end)
-- Client
DV.AddKeybind({
name = 'inventar',
description = 'Inventar öffnen',
key = 'F2',
onPressed = function() ... end,
onReleased = function() ... end,
})
DV.DisableKeybind('inventar', true)
DV.AddCommand übernimmt Rechteprüfung, chat:addSuggestion und das Umwandeln der Argumente. Fehlt ein Parameter, bekommt der Spieler automatisch die Verwendung angezeigt. Fehlt groups, darf jeder. Wird die Ressource gestoppt, verschwindet der Befehl aus den Vorschlägen und meldet sich beim Aufruf als nicht verfügbar.
Keybind-Namen gelten je Ressource: zwei Scripts dürfen beide einen Bind inventar anlegen, ohne sich zu behindern. DV.DisableKeybind wirkt entsprechend nur auf die eigenen Bindungen.
Alle Tasten auf einmal sperren#
luaDV.LockKeybinds('bewusstlos', true) -- jede Belegung aus DV.AddKeybind ist zu
DV.LockKeybinds('bewusstlos', false) -- wieder frei
DV.KeybindsLocked() -- boolean
Für Zustände, in denen der Spieler gar nichts tun soll — bewusstlos, gefesselt, in einer Zwischensequenz.
Warum das hier steht und nicht im aufrufenden Script.
DisableControlAction kommt an eine Belegung aus RegisterKeyMapping nicht heran: FiveM führt dort einen Befehl aus, und Befehle laufen am Controlsystem vorbei. Ein Script kann die F-Tasten anderer Ressourcen deshalb grundsätzlich nicht abschalten — es sei denn, alle gehen durch dieselbe Tür. Genau das tun die dv-Scripts.
Gezählt wird nach ressource:grund: zwei Gründe dürfen gleichzeitig gelten, und der eine hebt den anderen nicht auf. Das Loslassen einer Taste kommt trotz Sperre durch — wer gedrückt hielt, als sie kam, behielte sonst einen offenen Zustand. Sperren einer gestoppten Ressource verfallen von selbst.
Fremde Ressourcen erreicht auch das nicht. Für die bleibt nur, dass sie selbst nachsehen — zum Beispiel im Statebag des Scripts, das den Zustand führt.
Wann DV.AddKeybind — und wann nicht.
RegisterKeyMapping speichert die Belegung lokal beim Spieler. Der key aus dem Code ist nur der Standard bei der allerersten Registrierung; wird er später geändert, erreicht das niemanden mehr, der das Script schon benutzt hat — jeder einzelne Spieler müsste von Hand umbinden.
Richtig also für Tasten, die dem Spieler gehören sollen (Inventar, Menü, Handy) — er kann sie in den FiveM-Einstellungen umlegen, und genau das ist der Gewinn.
Falsch für Tasten, die der Serverbetreiber in einer config.lua einstellen soll. Dafür eine GTA-Control-Id nehmen und sie im passenden Moment mit IsControlJustReleased(0, id) abfragen — das liest bei jedem Mal neu, eine Config-Änderung gilt damit sofort für alle. dv_doorbell macht beides vor: Config.Key (klingeln) und Config.Reply.control (antworten).
Zweite Falle, unabhängig davon: key benennt die physische Taste nach US-Layout. Auf deutschen QWERTZ-Tastaturen sind Y und Z vertauscht — key = 'Y' trifft dort die mit „Z" beschriftete Taste. Alle anderen Buchstaben sitzen gleich.
Werkzeuge#
Tabellen#
luaDV.Table.Contains(t, wert) DV.Table.IndexOf(t, wert)
DV.Table.Count(t) DV.Table.IsEmpty(t)
DV.Table.Copy(t) DV.Table.DeepCopy(t)
DV.Table.Merge(defaults, user) DV.Table.Filter(t, fn)
DV.Table.Map(t, fn) DV.Table.Keys(t) / .Values(t)
DV.Table.Random(t) DV.Table.Shuffle(t)
Merge ist für Config-Defaults gedacht und führt verschachtelte Tabellen zusammen.
Zeichenketten#
luaDV.String.Trim(s) DV.String.Split(s, ',')
DV.String.StartsWith(s, 'prefix') DV.String.EndsWith(s, 'suffix')
DV.String.Capitalize(s) DV.String.FormatNumber(1234567) --> '1.234.567'
DV.String.Random(8) DV.String.Truncate(s, 30)
DV.String.TruncateChars(s, 60) DV.String.Sanitize(eingabe, 100)
DV.String.Token() --> 'a3f2c1-K7X9QM2P4Z1B'
Sanitize bereinigt Benutzereingaben (Steuerzeichen raus, Leerraum zusammengefaltet, gekürzt) — für alles, was Spieler tippen und andere sehen. Gekürzt wird in Zeichen (TruncateChars, UTF-8-sicher), also derselben Einheit wie maxLength in der NUI; Truncate zählt dagegen Bytes und hängt ein Auslassungszeichen an. Token liefert eine praktisch kollisionsfreie Kennung für Vorgänge; für Sicherheitszwecke ist sie nicht gedacht.
Rechnen#
luaDV.Math.Round(3.14159, 2) --> 3.14
DV.Math.Clamp(150, 0, 100) --> 100
DV.Math.Lerp(0, 100, 0.5) --> 50
DV.Math.Distance(a, b) DV.Math.Distance2D(a, b)
DV.Math.DistSquared(a, b) -- Quadratabstand, spart die Wurzel in Schleifen
DV.Math.Percent(30, 200) --> 15.0
DV.Math.RandomFloat(1.0, 5.0, 2)
DV.ToVector3(wert) -- vector3/vector4/{x,y,z}/{1,2,3} --> vector3
Distance2D ignoriert den Höhenunterschied — für Zonen meist das Richtige.
Timeouts, JSON, Logger#
lualocal id = DV.AddTimeout(5000, function(data) ... end, data)
DV.DelTimeout(id)
local daten = DV.JSON.Load('data/spieler.json', {}, 'meine_ressource')
DV.JSON.Save('data/spieler.json', daten, 'meine_ressource')
DV.JSON.Save('data/log.json', daten, nil, false) -- kompakt statt eingerückt
local log = DV.Logger('meinscript', Config.Debug)
log.info('Wert: %s', wert) log.ok(...) log.warn(...)
log.err('kommt immer durch') log.dump('payload', tabelle)
DV.JSON schreibt in die aufrufende Ressource. Über '@dv_core/import.lua' ist der Name fest eingesetzt und stimmt immer. Nur wer dv_core ausschließlich per exports: einbindet, sollte ihn als dritten Parameter mitgeben — dort läuft die Ermittlung über GetInvokingResource(), und die ist nicht in jedem Aufrufpfad verlässlich.
DV.JSON.Save schreibt standardmäßig eingerückt, damit die Datei von Hand lesbar bleibt. Der vierte Parameter pretty = false schaltet auf kompakt — lohnt bei Dateien, die häufig und groß geschrieben werden und die ohnehin niemand von Hand liest.
Für Spielerdaten ist DV.JSON der falsche Weg. SaveResourceFile schreibt die ganze Datei bei jeder Änderung, blockierend im Servertick — der Aufwand wächst mit dem Bestand, nicht mit der Änderung. Dazu kann es weder atomar schreiben noch einen Fehlergrund liefern, nur einen Bool. Für alles, was mitwächst, gehört DV.DB genommen. dv_friendbook ist genau daran aufgelaufen und mit v1.2.0 umgezogen.
Datenbank (Server, ab 3.26.0)#
luaif not DV.DB.Available() then return end -- läuft oxmysql? sofort, ohne Query
DV.DB.Query('SELECT * FROM meine_tabelle WHERE besitzer = ?', { ident }, function(rows, err)
if err then return print(err) end
for _, row in ipairs(rows) do ... end
end)
DV.DB.Single('SELECT name FROM t WHERE id = ?', { id }, function(row, err) ... end)
DV.DB.Scalar('SELECT COUNT(*) FROM t', {}, function(anzahl, err) ... end)
DV.DB.Insert('INSERT INTO t (a, b) VALUES (?, ?)', { a, b }, function(neueId, err) ... end)
-- Schreiben ohne Rückruf: der Fehler landet trotzdem in der Konsole
DV.DB.Execute('DELETE FROM t WHERE id = ?', { id })
-- Massenschreibvorgänge: alles oder nichts, ein Roundtrip statt 500
DV.DB.Transaction({
{ query = 'INSERT INTO t (a) VALUES (?)', values = { 1 } },
{ query = 'INSERT INTO t (a) VALUES (?)', values = { 2 } },
}, function(ok, err) ... end)
-- Beim Start: warten, bis oxmysql läuft (Startreihenfolge steht nicht fest)
DV.DB.Ready(function(bereit)
if not bereit then return end
DV.DB.Query('CREATE TABLE IF NOT EXISTS …', {}, function() ... end)
end)
Jede Funktion ruft zurück, keine gibt etwas zurück. Aufrufer aus anderen Ressourcen erreichen DV.DB als Funcref über die Ressourcengrenze; ein Citizen.Await darin wäre ein Yield im fremden Aufrufrahmen und bricht mit attempt to yield across a C-call boundary ab.
oxmysql ist optional. dv_core bindet es nicht ein (kein '@oxmysql/lib/MySQL.lua' in der fxmanifest) — eine Bibliothek für jeden Server darf keine Datenbank voraussetzen. Läuft oxmysql nicht, sagen die Funktionen das im Klartext, statt still nichts zu tun. Deshalb gehört DV.DB.Available() an den Anfang jedes Scripts, das ohne Datenbank nicht arbeiten kann.
Wer kein cb übergibt, bekommt einen Fehler trotzdem zu sehen — dann in der Serverkonsole, mit dem Namen der aufrufenden Ressource davor.
Übersetzungen#
lua-- locales/de.json in der eigenen Ressource:
-- { "shop.welcome": "Willkommen, %s!" }
DV.LoadLocale('de')
DV.Locale('shop.welcome', name) --> 'Willkommen, Max!'
-- Nur nötig, wenn dv_core rein über exports: eingebunden ist:
DV.LocaleFor('meine_ressource', 'shop.welcome', name)
Fehlt ein Schlüssel, kommt er selbst zurück — so fällt im Spiel sofort auf, was noch übersetzt werden muss.
Wie bei DV.JSON gilt: über '@dv_core/import.lua' ist der Ressourcenbezug fest eingesetzt. DV.LocaleFor gibt es für den Export-Weg, wo GetInvokingResource() sonst auf dv_core zurückfallen und stumm den Schlüssel statt der Übersetzung liefern könnte.
Spielersprache (Server): Standard ist die Convar dv:locale (set dv:locale "de" in der server.cfg). Eine Ressource lädt beim Start alle Sprachen, die sie anbietet, und übersetzt dann je Spieler:
luaDV.LoadLocale('de')
DV.LoadLocale('en')
DV.SetPlayerLocale(src, 'en') -- z.B. aus einem Einstellungs-Menü
DV.GetPlayerLocale(src) --> 'en'
DV.Notify(src, 'info', nil, DV.LocaleForPlayer(src, 'shop.welcome', name))
Der Wert lebt im Speicher und fällt beim Verlassen auf den Standard zurück — wer ihn dauerhaft will, speichert ihn selbst und setzt ihn bei dv_core:playerLoaded erneut.
Cron#
lualocal id = DV.Cron.Every(60000, function() ... end) -- alle 60 s, Minimum 1000 ms
DV.Cron.At('03:00', function() ... end) -- täglich zur SERVER-Uhrzeit
DV.Cron.Remove(id)
Ein Takt-Thread für alle Aufgaben aller Ressourcen; Fehler einer Aufgabe reißen die übrigen nicht mit. Beim Stoppen der anlegenden Ressource verschwinden ihre Aufgaben.
Splashscreen und Version#
luaDV.Splash() -- reicht: Name, Version und Autor kommen aus der fxmanifest
▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
📦 mein_script v1.0.0
✔ Erfolgreich geladen
🔗 discord.gg/hqBQt8SnwA
💜 Entwickelt von dv_scripts
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
Alle Angaben sind optional:
luaDV.Splash(nil, nil, {
icon = '🚗', -- Standard '📦'
description = 'Tut Dinge',
framework = true, -- Zeile "Framework: esx"
discord = false, -- blendet die Discord-Zeile aus
author = 'jemand', -- Standard: author aus der fxmanifest
update = true, -- Update-Hinweis, siehe unten
})
Am besten in einem CreateThread am Ende des letzten server_script, damit der Splash unter den übrigen Startmeldungen steht.
Update-Hinweis#
update = true schlägt die Ressource in der gemeinsamen Versionsliste nach — einer Textdatei für alle dv-Scripts. Wo sie liegt, steht fest in dv_core und nicht in der config.lua; abschalten lässt die Prüfung sich nicht.
Die Datei selbst, durch Leerzeichen getrennt:
# Name Version Link (optional)
dv_core 3.7.0 https://example.com/dv_core
dv_pets 1.16.5
Im Script bleibt es bei einer Zeile:
luaDV.Splash(nil, nil, { update = true })
Ein neues Script kostet damit eine Zeile in der Liste, kein eigenes Repository und keine Releases. Geholt wird die Datei einmal pro Serverstart: alle Scripts teilen sich Abruf und Ergebnis, wer währenddessen dazukommt, wartet mit — fünf Ressourcen erzeugen also eine Anfrage, nicht fünf.
Kein JSON, und das mit Absicht: dort legte ein vergessenes Komma die Prüfung aller Scripts lahm. In der Textform zerschießt ein Tippfehler nur seine eigene Zeile.
Ist eine neuere Version veröffentlicht, steht sie im Kasten — eine einzelne Zeile zwischen den Startmeldungen fremder Ressourcen liest niemand:
▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
📦 mein_script v1.0.0
✔ Erfolgreich geladen
⬆ Update verfügbar: v1.2.0 (installiert: v1.0.0)
🔗 https://example.com/mein_script
🔗 discord.gg/hqBQt8SnwA
💜 Entwickelt von dv_scripts
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
Die 🔗-Zeile erscheint nur, wenn in der Liste hinter der Version ein Link steht.
Ist alles aktuell, steht dort ✔ Version ist aktuell. Ist die lokale Version neuer als die veröffentlichte, ▪ Entwicklungsstand.
Verglichen wird nach Semver, nicht als Text: v1.2.0 und 1.2.0 sind dieselbe Version. Ist eine der beiden Angaben keine Versionsnummer, wird lieber gar nichts behauptet.
Schlägt die Prüfung fehl, bleibt sie still. Ein Server ohne Internetzugang erreicht die Liste nie und bekäme sonst bei jedem Start eine Warnung für ein Problem, das keines ist. Dasselbe gilt für ein Script, das noch gar nicht gelistet ist. Der Grund steht mit Config.Debug = true im Kasten.
Der Startkasten wartet höchstens vier Sekunden auf die Antwort und wird danach ohne die Zeile gedruckt — der Serverstart hängt daran nie. Warten tut ein eigener Thread, DV.Splash selbst kehrt sofort zurück (Exports dürfen nicht yielden).
Ein Server ohne Internetzugang erreicht die Liste nie und wartet deshalb bei jedem Start diese vier Sekunden. Es ist eine Wartezeit für alle dv-Scripts zusammen, nicht eine je Ressource: die Liste wird einmal pro Serverstart geholt, und wer währenddessen dazukommt, wartet mit.
Ein Repository statt der Liste#
Wer ausdrücklich gegen GitHub-Releases prüfen will, gibt das Ziel selbst an — statt der Liste, nicht als Rückfall:
luaDV.CheckVersion('mein_script', 'owner/mein_script')
Verglichen wird dann gegen den Tag des letzten Releases; das führende v stört nicht.
Das Repository muss öffentlich sein. dv_core fragt ohne Token, ein privates antwortet ihm deshalb 404 — und die Prüfung bleibt still, ohne dass es auffällt. Genau das ist der Grund für die Liste: sie verlangt nur eine öffentliche Datei mit Versionsnummern, nicht ein öffentliches Repository mit dem Quelltext darin.
Eintrag in der Serverliste#
dv_core trägt sich in den DETAILS-Block des Servereintrags ein — dort, wo auch Discord, Startgeld und Fraktionen stehen:
💜 - dv_scripts: dv_core 3.25.0 · 7 Scripts
Dafür ist nichts in der server.cfg einzutragen. Gesetzt wird der Wert zur Laufzeit über SetConvarServerInfo, das Laufzeit-Gegenstück zu sets. Es gibt dafür keinen Schalter — die Zeile gehört fest zu dv_core, wie die Startmeldung und die Update-Prüfung. Angefasst wird genau dieses eine Feld.
Gezählt wird über das author-Feld der fxmanifest.lua, nicht über das Präfix dv_ — das benutzen andere Anbieter auch. Startet oder stoppt später eine Ressource nach, wird die Zeile neu geschrieben; pro Welle einmal, nicht je Ressource. Die Serverliste übernimmt den Wert beim nächsten Heartbeat, nicht sofort.
Der Server muss gelistet sein. DETAILS und TAGS im Serverbrowser kommen aus dem Eintrag der Cfx-Masterliste. Steht in der server.cfg ein sv_master1 "", registriert sich der Server dort nicht — dann fehlen beide Blöcke, auch die Tags aus der eigenen cfg. Was tatsächlich gesetzt ist, zeigt http://<server>:30120/info.json unter vars.
Der tags-Convar wird nicht angefasst. Dort stehen die Suchbegriffe, die der Serverbetreiber selbst gewählt hat.
Version einzeln prüfen#
Ohne Splash, wenn nur das Ergebnis gebraucht wird:
luaDV.CheckVersion('mein_script')
Ohne zweites Argument kommt die Angabe aus der Liste. Das Ergebnis landet asynchron in der Konsole. Mit einem vierten Argument bekommt man es stattdessen selbst — dann wird nichts gedruckt:
luaDV.CheckVersion('mein_script', nil, nil, function(result)
-- result.state 'current' | 'outdated' | 'ahead' | 'failed'
-- result.current installierte Version
-- result.latest veröffentlichte Version
-- result.url Link aus der Liste, sofern einer eingetragen ist
-- result.status HTTP-Status, nur bei 'failed'
end)
Als zweites Argument geht weiterhin owner/repo für die GitHub-Releases oder eine URL auf eine Textdatei, die nur die Versionsnummer enthält (1.2.0).
Discord-Logs#
Für neuen Code gedacht. Einmal einen Kanal anlegen, danach nur noch schreiben:
lua-- beim Start
DV.Log.Channel('doorbell', {
url = Config.Webhook, -- leer oder nil = Kanal bleibt stumm
name = 'Türklingel', -- Anzeigename des Bots
footer = 'dv_doorbell',
})
-- überall danach
DV.Log.Write('doorbell', {
type = 'warning', -- info (Standard) | success | warning | error
title = 'Spamsperre',
text = 'Limit an der Klingel erreicht.',
player = src, -- hängt Name, Server-Id und Identifier an
fields = { { name = 'Klingel', value = 'police_mrpd', inline = true } },
})
DV.Log.Enabled('doorbell') --> boolean
Was das abnimmt:
- Farbe je Typ statt einer Dezimalzahl im Aufruf (
colours überschreibt sie je Kanal). - Spielerangaben über
player = src — der Block, den sonst jedes Script selbst schreibt und der bei einem inzwischen getrennten Spieler gern einen Fehler wirft. - Rate-Limit. Discord beantwortet ab der sechsten Anfrage in zwei Sekunden mit
429. DV.Log schickt eine alle 600 ms aus einer Warteschlange, und bei einem 429 wandert die Meldung zurück an den Anfang statt verloren zu gehen — Discords eigenes retry_after bestimmt die Pause. - Stumm ohne Abfrage. Fehlt der Kanal oder seine URL, passiert schlicht nichts. Ein Script muss seine Aufrufe nicht in
if Config.Webhook ~= '' then einpacken.
Läuft eine Ressource Amok, werden ab 100 wartenden Meldungen weitere verworfen und beim Leerlaufen der Schlange gemeldet. Kanäle einer gestoppten Ressource verschwinden.
DV.Log.Write und nicht DV.Log(...): der direkte Aufruf bräuchte eine __call-Metatabelle, und die überlebt die Ressourcengrenze nicht.
Discord-Webhook (roh)#
luaDV.AddWebhook(url, farbe, botName, avatar, titel, text, fields, footer, zeitformat)
Die ursprüngliche Fassung, Signatur unverändert — mehrere Ressourcen rufen sie so auf. Kein Rate-Limit-Schutz. Für neuen Code besser DV.Log.
Blockierende Funktionen#
Diese gibt es nur über @dv_core/import.lua, nicht als Export:
| Funktion | |
|---|
DV.Callback.Await(name, ...) | Callback mit Rückgabewert |
DV.Progress(...) → boolean | Fortschrittsbalken |
DV.InputAwait(titel, felder) | Eingabedialog |
DV.MenuAwait(daten) → item, path | Menü |
DV.SpawnVehicle(model, coords, heading, opts) | Fahrzeug spawnen |
DV.SpawnPed(model, coords, heading, opts) | NPC spawnen |
DV.WaitFor(check, timeout, interval) | Wartet, bis check() einen Wert liefert (interval ms, Standard 0 = pro Frame) |
Warum: Über exports: gerufener Code läuft in einem C-Call. Ein Wait darin bricht mit attempt to yield across a C-call boundary ab. import.lua wird dagegen in die aufrufende Ressource geladen und läuft in deren Runtime — dort ist Wait erlaubt.
Aus demselben Grund baut import.lua auch DV.RequestModel, DV.RequestAnimDict, DV.RequestTextureDict, DV.RequestPtfxAsset und DV.PlayAnim lokal neu auf — sie warten intern auf das Streaming. Signaturen und Verhalten sind identisch zur Beschreibung oben; ohne import.lua sind sie über die Ressourcengrenze nicht verlässlich nutzbar.
Alle müssen aus einem Thread heraus aufgerufen werden (CreateThread).