forked from Simnation/Main
1322 lines
42 KiB
Lua
1322 lines
42 KiB
Lua
local QBCore = exports['qb-core']:GetCoreObject()
|
|
|
|
-- Lokale Variablen
|
|
local isMenuOpen = false
|
|
local currentTarget = nil
|
|
local nearbyPlayers = {}
|
|
local isLicenseShowing = false
|
|
local pendingRequests = {}
|
|
|
|
-- Hilfsfunktionen
|
|
local function debugPrint(message)
|
|
if Config.Debug then
|
|
print("^3[License-System Client] " .. message .. "^7")
|
|
end
|
|
end
|
|
|
|
local function showNotification(message, type)
|
|
QBCore.Functions.Notify(message, type or 'primary')
|
|
end
|
|
|
|
-- Nearby Players abrufen
|
|
local function getNearbyPlayers(radius)
|
|
radius = radius or 5.0
|
|
local players = {}
|
|
local playerPed = PlayerPedId()
|
|
local playerCoords = GetEntityCoords(playerPed)
|
|
|
|
for _, playerId in ipairs(GetActivePlayers()) do
|
|
local targetPed = GetPlayerPed(playerId)
|
|
if targetPed ~= playerPed then
|
|
local targetCoords = GetEntityCoords(targetPed)
|
|
local distance = #(playerCoords - targetCoords)
|
|
|
|
if distance <= radius then
|
|
local serverId = GetPlayerServerId(playerId)
|
|
local playerName = GetPlayerName(playerId)
|
|
|
|
table.insert(players, {
|
|
id = serverId,
|
|
name = playerName,
|
|
distance = math.floor(distance * 100) / 100,
|
|
ped = targetPed
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Nach Entfernung sortieren
|
|
table.sort(players, function(a, b)
|
|
return a.distance < b.distance
|
|
end)
|
|
|
|
debugPrint("Gefundene Spieler in der Nähe: " .. #players)
|
|
return players
|
|
end
|
|
|
|
-- Berechtigung prüfen
|
|
local function hasPermission()
|
|
local PlayerData = QBCore.Functions.GetPlayerData()
|
|
if not PlayerData or not PlayerData.job then return false end
|
|
|
|
local hasAuth = Config.AuthorizedJobs[PlayerData.job.name] or false
|
|
debugPrint("Berechtigung für Job " .. PlayerData.job.name .. ": " .. tostring(hasAuth))
|
|
return hasAuth
|
|
end
|
|
|
|
-- Lizenz anzeigen
|
|
local function showLicense(licenseData)
|
|
if not licenseData then
|
|
showNotification(Config.Notifications.license_not_found.message, Config.Notifications.license_not_found.type)
|
|
return
|
|
end
|
|
|
|
debugPrint("Zeige Lizenz: " .. licenseData.license.license_type)
|
|
|
|
SendNUIMessage({
|
|
action = 'showLicense',
|
|
data = licenseData
|
|
})
|
|
|
|
SetNuiFocus(true, true)
|
|
isLicenseShowing = true
|
|
end
|
|
|
|
-- Lizenz schließen
|
|
local function closeLicense()
|
|
SendNUIMessage({
|
|
action = 'hideLicense'
|
|
})
|
|
|
|
SetNuiFocus(false, false)
|
|
isLicenseShowing = false
|
|
debugPrint("Lizenz geschlossen")
|
|
end
|
|
|
|
-- Spieler-Lizenz anzeigen
|
|
local function showPlayerLicense(targetId)
|
|
debugPrint("=== showPlayerLicense START ===")
|
|
debugPrint("Sende Event: requestLicense für Spieler: " .. tostring(targetId))
|
|
|
|
TriggerServerEvent('license-system:server:requestLicense', targetId)
|
|
end
|
|
|
|
-- Eigene Lizenz anzeigen
|
|
local function showMyLicense(licenseType)
|
|
debugPrint("=== showMyLicense START ===")
|
|
debugPrint("Sende Event: requestMyLicense für Typ: " .. tostring(licenseType))
|
|
|
|
TriggerServerEvent('license-system:server:requestMyLicense', licenseType)
|
|
end
|
|
|
|
-- FORWARD DECLARATIONS (Funktionen die später definiert werden)
|
|
local confirmIssueLicense
|
|
local openIssueLicenseMenu
|
|
local openDriversLicenseClassMenu
|
|
local openRevokeLicenseMenu
|
|
local openPlayerLicenseMenu
|
|
local openLicenseMenu
|
|
|
|
-- Lizenz-Ausstellung bestätigen (FRÜH DEFINIERT)
|
|
confirmIssueLicense = function(targetId, targetName, licenseType, classes)
|
|
local config = Config.LicenseTypes[licenseType]
|
|
if not config then
|
|
showNotification('Unbekannter Lizenztyp!', 'error')
|
|
return
|
|
end
|
|
|
|
local classText = classes and table.concat(classes, ', ') or 'Keine'
|
|
local priceText = config.price and (config.price .. ' $') or 'Kostenlos'
|
|
|
|
debugPrint("Bestätige Lizenz-Ausstellung: " .. licenseType .. " für " .. targetName)
|
|
|
|
lib.registerContext({
|
|
id = 'confirm_issue_license',
|
|
title = 'Lizenz ausstellen bestätigen',
|
|
options = {
|
|
{
|
|
title = 'Spieler: ' .. targetName,
|
|
disabled = true,
|
|
icon = 'fas fa-user'
|
|
},
|
|
{
|
|
title = 'Lizenztyp: ' .. config.label,
|
|
disabled = true,
|
|
icon = config.icon
|
|
},
|
|
{
|
|
title = 'Klassen: ' .. classText,
|
|
disabled = true,
|
|
icon = 'fas fa-list'
|
|
},
|
|
{
|
|
title = 'Kosten: ' .. priceText,
|
|
disabled = true,
|
|
icon = 'fas fa-dollar-sign'
|
|
},
|
|
{
|
|
title = '─────────────────',
|
|
disabled = true
|
|
},
|
|
{
|
|
title = '✅ Bestätigen',
|
|
description = 'Lizenz jetzt ausstellen',
|
|
icon = 'fas fa-check',
|
|
onSelect = function()
|
|
debugPrint("Sende Lizenz-Ausstellung an Server...")
|
|
TriggerServerEvent('license-system:server:issueLicense', targetId, licenseType, classes)
|
|
lib.hideContext()
|
|
end
|
|
},
|
|
{
|
|
title = '❌ Abbrechen',
|
|
description = 'Vorgang abbrechen',
|
|
icon = 'fas fa-times',
|
|
onSelect = function()
|
|
openIssueLicenseMenu(targetId, targetName)
|
|
end
|
|
}
|
|
}
|
|
})
|
|
|
|
lib.showContext('confirm_issue_license')
|
|
end
|
|
|
|
-- Führerschein-Klassen Menü
|
|
openDriversLicenseClassMenu = function(targetId, targetName, licenseType)
|
|
local config = Config.LicenseTypes[licenseType]
|
|
if not config then
|
|
showNotification('Lizenz-Konfiguration nicht gefunden!', 'error')
|
|
return
|
|
end
|
|
|
|
local selectedClasses = {}
|
|
|
|
local function updateMenu()
|
|
local menuOptions = {}
|
|
|
|
if config.classes then
|
|
for _, class in ipairs(config.classes) do
|
|
local isSelected = false
|
|
for _, selected in ipairs(selectedClasses) do
|
|
if selected == class then
|
|
isSelected = true
|
|
break
|
|
end
|
|
end
|
|
|
|
local classDescriptions = {
|
|
['A'] = 'Motorräder',
|
|
['A1'] = 'Leichte Motorräder (bis 125ccm)',
|
|
['A2'] = 'Mittlere Motorräder (bis 35kW)',
|
|
['B'] = 'PKW (bis 3,5t)',
|
|
['BE'] = 'PKW mit Anhänger',
|
|
['C'] = 'LKW (über 3,5t)',
|
|
['CE'] = 'LKW mit Anhänger',
|
|
['D'] = 'Bus (über 8 Personen)',
|
|
['DE'] = 'Bus mit Anhänger'
|
|
}
|
|
|
|
table.insert(menuOptions, {
|
|
title = 'Klasse ' .. class .. (isSelected and ' ✅' or ''),
|
|
description = classDescriptions[class] or 'Keine Beschreibung',
|
|
icon = isSelected and 'fas fa-check-square' or 'far fa-square',
|
|
onSelect = function()
|
|
if isSelected then
|
|
-- Klasse entfernen
|
|
for i, selected in ipairs(selectedClasses) do
|
|
if selected == class then
|
|
table.remove(selectedClasses, i)
|
|
break
|
|
end
|
|
end
|
|
else
|
|
-- Klasse hinzufügen
|
|
table.insert(selectedClasses, class)
|
|
end
|
|
updateMenu()
|
|
end
|
|
})
|
|
end
|
|
end
|
|
|
|
table.insert(menuOptions, {
|
|
title = '─────────────────',
|
|
disabled = true
|
|
})
|
|
|
|
table.insert(menuOptions, {
|
|
title = 'Bestätigen (' .. #selectedClasses .. ' Klassen)',
|
|
description = 'Führerschein mit ausgewählten Klassen ausstellen',
|
|
icon = 'fas fa-check',
|
|
disabled = #selectedClasses == 0,
|
|
onSelect = function()
|
|
confirmIssueLicense(targetId, targetName, licenseType, selectedClasses)
|
|
end
|
|
})
|
|
|
|
table.insert(menuOptions, {
|
|
title = '← Zurück',
|
|
icon = 'fas fa-arrow-left',
|
|
onSelect = function()
|
|
openIssueLicenseMenu(targetId, targetName)
|
|
end
|
|
})
|
|
|
|
lib.registerContext({
|
|
id = 'drivers_license_classes',
|
|
title = 'Führerschein-Klassen: ' .. targetName,
|
|
options = menuOptions
|
|
})
|
|
|
|
lib.showContext('drivers_license_classes')
|
|
end
|
|
|
|
updateMenu()
|
|
end
|
|
|
|
-- Lizenz ausstellen Menü
|
|
openIssueLicenseMenu = function(targetId, targetName)
|
|
debugPrint("Öffne Lizenz-Ausstellungs-Menü für: " .. targetName)
|
|
|
|
local menuOptions = {}
|
|
|
|
for licenseType, config in pairs(Config.LicenseTypes) do
|
|
local priceText = config.price and (config.price .. ' $') or 'Kostenlos'
|
|
local validityText = config.validity_days and (config.validity_days .. ' Tage') or 'Unbegrenzt'
|
|
|
|
table.insert(menuOptions, {
|
|
title = config.label,
|
|
description = config.description or 'Keine Beschreibung verfügbar',
|
|
icon = config.icon,
|
|
onSelect = function()
|
|
if licenseType == 'drivers_license' and config.classes then
|
|
openDriversLicenseClassMenu(targetId, targetName, licenseType)
|
|
else
|
|
confirmIssueLicense(targetId, targetName, licenseType, nil)
|
|
end
|
|
end,
|
|
metadata = {
|
|
{label = 'Preis', value = priceText},
|
|
{label = 'Gültigkeitsdauer', value = validityText}
|
|
}
|
|
})
|
|
end
|
|
|
|
table.insert(menuOptions, {
|
|
title = '← Zurück',
|
|
icon = 'fas fa-arrow-left',
|
|
onSelect = function()
|
|
openPlayerLicenseMenu(targetId, targetName)
|
|
end
|
|
})
|
|
|
|
lib.registerContext({
|
|
id = 'issue_license',
|
|
title = 'Lizenz ausstellen: ' .. targetName,
|
|
options = menuOptions
|
|
})
|
|
|
|
lib.showContext('issue_license')
|
|
end
|
|
|
|
-- Lizenz entziehen Menü
|
|
openRevokeLicenseMenu = function(targetId, targetName, licenses)
|
|
debugPrint("Öffne Lizenz-Entziehungs-Menü für: " .. targetName)
|
|
|
|
local menuOptions = {}
|
|
|
|
for _, license in ipairs(licenses) do
|
|
if license.is_active == 1 then
|
|
local config = Config.LicenseTypes[license.license_type] or {
|
|
label = license.license_type,
|
|
icon = 'fas fa-id-card'
|
|
}
|
|
|
|
table.insert(menuOptions, {
|
|
title = config.label,
|
|
description = 'Diese Lizenz entziehen',
|
|
icon = config.icon,
|
|
onSelect = function()
|
|
lib.registerContext({
|
|
id = 'confirm_revoke_license',
|
|
title = 'Lizenz entziehen bestätigen',
|
|
options = {
|
|
{
|
|
title = 'Spieler: ' .. targetName,
|
|
disabled = true,
|
|
icon = 'fas fa-user'
|
|
},
|
|
{
|
|
title = 'Lizenztyp: ' .. config.label,
|
|
disabled = true,
|
|
icon = config.icon
|
|
},
|
|
{
|
|
title = '─────────────────',
|
|
disabled = true
|
|
},
|
|
{
|
|
title = '✅ Bestätigen',
|
|
description = 'Lizenz jetzt entziehen',
|
|
icon = 'fas fa-check',
|
|
onSelect = function()
|
|
debugPrint("Sende Lizenz-Entziehung an Server...")
|
|
TriggerServerEvent('license-system:server:revokeLicense', targetId, license.license_type)
|
|
lib.hideContext()
|
|
end
|
|
},
|
|
{
|
|
title = '❌ Abbrechen',
|
|
description = 'Vorgang abbrechen',
|
|
icon = 'fas fa-times',
|
|
onSelect = function()
|
|
openRevokeLicenseMenu(targetId, targetName, licenses)
|
|
end
|
|
}
|
|
}
|
|
})
|
|
|
|
lib.showContext('confirm_revoke_license')
|
|
end
|
|
})
|
|
end
|
|
end
|
|
|
|
if #menuOptions == 0 then
|
|
table.insert(menuOptions, {
|
|
title = 'Keine aktiven Lizenzen',
|
|
description = 'Dieser Spieler hat keine aktiven Lizenzen',
|
|
icon = 'fas fa-exclamation-triangle',
|
|
disabled = true
|
|
})
|
|
end
|
|
|
|
table.insert(menuOptions, {
|
|
title = '← Zurück',
|
|
icon = 'fas fa-arrow-left',
|
|
onSelect = function()
|
|
openPlayerLicenseMenu(targetId, targetName)
|
|
end
|
|
})
|
|
|
|
lib.registerContext({
|
|
id = 'revoke_license',
|
|
title = 'Lizenz entziehen: ' .. targetName,
|
|
options = menuOptions
|
|
})
|
|
|
|
lib.showContext('revoke_license')
|
|
end
|
|
|
|
-- Spieler-Lizenz-Menü
|
|
openPlayerLicenseMenu = function(targetId, targetName)
|
|
debugPrint("=== openPlayerLicenseMenu START ===")
|
|
debugPrint("Sende Event: requestPlayerLicenses für: " .. targetName .. " (ID: " .. targetId .. ")")
|
|
|
|
TriggerServerEvent('license-system:server:requestPlayerLicenses', targetId)
|
|
end
|
|
|
|
-- Hauptmenü für Lizenz-System
|
|
openLicenseMenu = function()
|
|
debugPrint("Öffne Hauptmenü für Lizenz-System")
|
|
|
|
if not hasPermission() then
|
|
showNotification(Config.Notifications.no_permission.message, Config.Notifications.no_permission.type)
|
|
return
|
|
end
|
|
|
|
nearbyPlayers = getNearbyPlayers(5.0)
|
|
|
|
if #nearbyPlayers == 0 then
|
|
showNotification(Config.Notifications.no_players_nearby.message, Config.Notifications.no_players_nearby.type)
|
|
return
|
|
end
|
|
|
|
local menuOptions = {}
|
|
|
|
for _, player in ipairs(nearbyPlayers) do
|
|
table.insert(menuOptions, {
|
|
title = player.name,
|
|
description = 'Entfernung: ' .. player.distance .. 'm',
|
|
icon = 'fas fa-user',
|
|
onSelect = function()
|
|
openPlayerLicenseMenu(player.id, player.name)
|
|
end
|
|
})
|
|
end
|
|
|
|
lib.registerContext({
|
|
id = 'license_nearby_players',
|
|
title = 'Spieler in der Nähe (' .. #nearbyPlayers .. ')',
|
|
options = menuOptions
|
|
})
|
|
|
|
lib.showContext('license_nearby_players')
|
|
end
|
|
|
|
-- Eigene Lizenzen anzeigen
|
|
local function showMyLicenses()
|
|
debugPrint("Öffne Menü für eigene Lizenzen")
|
|
|
|
local menuOptions = {}
|
|
|
|
for licenseType, config in pairs(Config.LicenseTypes) do
|
|
table.insert(menuOptions, {
|
|
title = config.label,
|
|
description = 'Deine ' .. config.label .. ' anzeigen',
|
|
icon = config.icon,
|
|
onSelect = function()
|
|
showMyLicense(licenseType)
|
|
end
|
|
})
|
|
end
|
|
|
|
lib.registerContext({
|
|
id = 'my_licenses',
|
|
title = 'Meine Lizenzen',
|
|
options = menuOptions
|
|
})
|
|
|
|
lib.showContext('my_licenses')
|
|
end
|
|
|
|
-- EVENT HANDLER: Einzelne Lizenz erhalten
|
|
RegisterNetEvent('license-system:client:receiveLicense', function(licenseData)
|
|
debugPrint("=== Event: receiveLicense ===")
|
|
debugPrint("LicenseData-Typ: " .. type(licenseData))
|
|
|
|
if licenseData then
|
|
debugPrint("Lizenz-Daten erhalten: " .. licenseData.license.license_type)
|
|
showLicense(licenseData)
|
|
else
|
|
debugPrint("Keine Lizenz-Daten erhalten")
|
|
showNotification(Config.Notifications.license_not_found.message, Config.Notifications.license_not_found.type)
|
|
end
|
|
end)
|
|
|
|
-- EVENT HANDLER: Eigene Lizenz erhalten
|
|
RegisterNetEvent('license-system:client:receiveMyLicense', function(licenseData, licenseType)
|
|
debugPrint("=== Event: receiveMyLicense ===")
|
|
debugPrint("LicenseType: " .. tostring(licenseType))
|
|
debugPrint("LicenseData-Typ: " .. type(licenseData))
|
|
|
|
if licenseData then
|
|
debugPrint("Eigene Lizenz-Daten erhalten: " .. licenseData.license.license_type)
|
|
showLicense(licenseData)
|
|
else
|
|
debugPrint("Keine eigene Lizenz gefunden")
|
|
local config = Config.LicenseTypes[licenseType]
|
|
local licenseName = config and config.label or licenseType
|
|
showNotification('Du hast keine ' .. licenseName .. '!', 'error')
|
|
end
|
|
end)
|
|
|
|
-- EVENT HANDLER: Alle Spieler-Lizenzen erhalten
|
|
RegisterNetEvent('license-system:client:receivePlayerLicenses', function(licenses, targetId, targetName)
|
|
debugPrint("=== Event: receivePlayerLicenses ===")
|
|
debugPrint("Erhaltene Lizenzen: " .. #licenses)
|
|
debugPrint("TargetName: " .. tostring(targetName))
|
|
|
|
local menuOptions = {}
|
|
|
|
if licenses and #licenses > 0 then
|
|
for _, license in ipairs(licenses) do
|
|
local licenseConfig = Config.LicenseTypes[license.license_type] or {
|
|
label = license.license_type,
|
|
icon = 'fas fa-id-card',
|
|
color = '#667eea'
|
|
}
|
|
|
|
local statusIcon = (license.is_active == 1) and '✅' or '❌'
|
|
local statusText = (license.is_active == 1) and 'Gültig' or 'Ungültig'
|
|
local expireText = license.expire_date or 'Unbegrenzt'
|
|
|
|
table.insert(menuOptions, {
|
|
title = licenseConfig.label .. ' ' .. statusIcon,
|
|
description = 'Status: ' .. statusText .. ' | Gültig bis: ' .. expireText,
|
|
icon = licenseConfig.icon,
|
|
onSelect = function()
|
|
local licenseData = {
|
|
license = license,
|
|
config = licenseConfig
|
|
}
|
|
showLicense(licenseData)
|
|
end,
|
|
metadata = {
|
|
{label = 'Status', value = statusText},
|
|
{label = 'Ausgestellt', value = license.issue_date or 'Unbekannt'},
|
|
{label = 'Gültig bis', value = expireText},
|
|
{label = 'Aussteller', value = license.issued_by_name or 'System'}
|
|
}
|
|
})
|
|
end
|
|
else
|
|
table.insert(menuOptions, {
|
|
title = 'Keine Lizenzen gefunden',
|
|
description = 'Dieser Spieler hat keine Lizenzen',
|
|
icon = 'fas fa-exclamation-triangle',
|
|
disabled = true
|
|
})
|
|
end
|
|
|
|
-- Aktionen hinzufügen
|
|
table.insert(menuOptions, {
|
|
title = '─────────────────',
|
|
disabled = true
|
|
})
|
|
|
|
table.insert(menuOptions, {
|
|
title = 'Neue Lizenz ausstellen',
|
|
description = 'Eine neue Lizenz für diesen Spieler ausstellen',
|
|
icon = 'fas fa-plus',
|
|
onSelect = function()
|
|
openIssueLicenseMenu(targetId, targetName)
|
|
end
|
|
})
|
|
|
|
if licenses and #licenses > 0 then
|
|
table.insert(menuOptions, {
|
|
title = 'Lizenz entziehen',
|
|
description = 'Eine bestehende Lizenz entziehen',
|
|
icon = 'fas fa-minus',
|
|
onSelect = function()
|
|
openRevokeLicenseMenu(targetId, targetName, licenses)
|
|
end
|
|
})
|
|
end
|
|
|
|
table.insert(menuOptions, {
|
|
title = '← Zurück',
|
|
icon = 'fas fa-arrow-left',
|
|
onSelect = function()
|
|
openLicenseMenu()
|
|
end
|
|
})
|
|
|
|
lib.registerContext({
|
|
id = 'player_licenses',
|
|
title = 'Lizenzen: ' .. targetName,
|
|
options = menuOptions
|
|
})
|
|
|
|
lib.showContext('player_licenses')
|
|
end)
|
|
|
|
-- EVENT HANDLER: Berechtigung erhalten
|
|
RegisterNetEvent('license-system:client:receivePermission', function(hasAuth, licenseType)
|
|
debugPrint("=== Event: receivePermission ===")
|
|
debugPrint("Berechtigung für " .. licenseType .. ": " .. tostring(hasAuth))
|
|
|
|
if not hasAuth then
|
|
showNotification(Config.Notifications.no_permission.message, Config.Notifications.no_permission.type)
|
|
end
|
|
end)
|
|
|
|
-- EVENT HANDLER: Lizenz erfolgreich ausgestellt
|
|
RegisterNetEvent('license-system:client:licenseIssued', function(targetId, licenseType)
|
|
debugPrint("=== Event: licenseIssued ===")
|
|
debugPrint("Lizenz " .. licenseType .. " für Spieler " .. targetId .. " ausgestellt")
|
|
|
|
-- Menü aktualisieren
|
|
if lib.getOpenContextMenu() then
|
|
lib.hideContext()
|
|
Wait(100)
|
|
openLicenseMenu()
|
|
end
|
|
end)
|
|
|
|
-- EVENT HANDLER: Lizenz erfolgreich entzogen
|
|
RegisterNetEvent('license-system:client:licenseRevoked', function(targetId, licenseType)
|
|
debugPrint("=== Event: licenseRevoked ===")
|
|
debugPrint("Lizenz " .. licenseType .. " für Spieler " .. targetId .. " entzogen")
|
|
|
|
-- Menü aktualisieren
|
|
if lib.getOpenContextMenu() then
|
|
lib.hideContext()
|
|
Wait(100)
|
|
openLicenseMenu()
|
|
end
|
|
end)
|
|
|
|
-- EVENT HANDLER: Lizenz anzeigen (von anderen Spielern)
|
|
RegisterNetEvent('license-system:client:showLicense', function(targetId)
|
|
debugPrint("Event erhalten: showLicense für ID " .. tostring(targetId))
|
|
showPlayerLicense(targetId)
|
|
end)
|
|
|
|
-- EVENT HANDLER: Eigene Lizenz anzeigen
|
|
RegisterNetEvent('license-system:client:showMyLicense', function(licenseType)
|
|
debugPrint("Event erhalten: showMyLicense für Typ " .. tostring(licenseType))
|
|
showMyLicense(licenseType)
|
|
end)
|
|
|
|
-- EVENT HANDLER: Kamera öffnen
|
|
RegisterNetEvent('license-system:client:openCamera', function()
|
|
debugPrint("Event erhalten: openCamera")
|
|
SendNUIMessage({
|
|
action = 'openCamera'
|
|
})
|
|
end)
|
|
|
|
-- EVENT HANDLER: Menü aktualisieren
|
|
RegisterNetEvent('license-system:client:refreshMenu', function()
|
|
debugPrint("Event erhalten: refreshMenu")
|
|
if lib.getOpenContextMenu() then
|
|
lib.hideContext()
|
|
Wait(100)
|
|
openLicenseMenu()
|
|
end
|
|
end)
|
|
|
|
-- NUI CALLBACKS
|
|
RegisterNUICallback('closeLicense', function(data, cb)
|
|
debugPrint("NUI Callback: closeLicense")
|
|
closeLicense()
|
|
|
|
if cb and type(cb) == "function" then
|
|
cb('ok')
|
|
end
|
|
end)
|
|
|
|
RegisterNUICallback('savePhoto', function(data, cb)
|
|
debugPrint("NUI Callback: savePhoto")
|
|
|
|
if data.photo and data.citizenid then
|
|
TriggerServerEvent('license-system:server:savePhoto', data.citizenid, data.photo)
|
|
|
|
if cb and type(cb) == "function" then
|
|
cb('ok')
|
|
end
|
|
else
|
|
debugPrint("^1Fehler: Foto-Daten unvollständig^7")
|
|
|
|
if cb and type(cb) == "function" then
|
|
cb('error')
|
|
end
|
|
end
|
|
end)
|
|
|
|
RegisterNUICallback('takePicture', function(data, cb)
|
|
debugPrint("NUI Callback: takePicture")
|
|
|
|
-- Hier könnte eine Kamera-Funktion implementiert werden
|
|
if cb and type(cb) == "function" then
|
|
cb('ok')
|
|
end
|
|
end)
|
|
|
|
-- COMMANDS
|
|
RegisterCommand(Config.Commands.license.name, function()
|
|
debugPrint("Command ausgeführt: " .. Config.Commands.license.name)
|
|
openLicenseMenu()
|
|
end, Config.Commands.license.restricted)
|
|
|
|
RegisterCommand(Config.Commands.mylicense.name, function()
|
|
debugPrint("Command ausgeführt: " .. Config.Commands.mylicense.name)
|
|
showMyLicenses()
|
|
end, Config.Commands.mylicense.restricted)
|
|
|
|
-- Zusätzliche Commands für schnellen Zugriff
|
|
RegisterCommand('ausweis', function()
|
|
debugPrint("Command ausgeführt: ausweis")
|
|
showMyLicense('id_card')
|
|
end, false)
|
|
|
|
RegisterCommand('führerschein', function()
|
|
debugPrint("Command ausgeführt: führerschein")
|
|
showMyLicense('drivers_license')
|
|
end, false)
|
|
|
|
RegisterCommand('waffenschein', function()
|
|
debugPrint("Command ausgeführt: waffenschein")
|
|
showMyLicense('weapon_license')
|
|
end, false)
|
|
|
|
RegisterCommand('pass', function()
|
|
debugPrint("Command ausgeführt: pass")
|
|
showMyLicense('passport')
|
|
end, false)
|
|
|
|
-- KEYBINDS
|
|
if Config.Keybinds and Config.Keybinds.open_license_menu then
|
|
RegisterKeyMapping(Config.Commands.license.name, Config.Keybinds.open_license_menu.description, 'keyboard', Config.Keybinds.open_license_menu.key)
|
|
end
|
|
|
|
if Config.Keybinds and Config.Keybinds.show_my_licenses then
|
|
RegisterKeyMapping(Config.Commands.mylicense.name, Config.Keybinds.show_my_licenses.description, 'keyboard', Config.Keybinds.show_my_licenses.key)
|
|
end
|
|
|
|
-- ESC-Taste zum Schließen der Lizenz
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(0)
|
|
|
|
if isLicenseShowing then
|
|
if IsControlJustPressed(0, 322) then -- ESC-Taste
|
|
closeLicense()
|
|
end
|
|
else
|
|
Wait(500)
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- CLEANUP UND INITIALISIERUNG
|
|
AddEventHandler('onResourceStop', function(resourceName)
|
|
if GetCurrentResourceName() == resourceName then
|
|
if isLicenseShowing then
|
|
closeLicense()
|
|
end
|
|
|
|
if lib.getOpenContextMenu() then
|
|
lib.hideContext()
|
|
end
|
|
|
|
debugPrint("License-System Client gestoppt")
|
|
end
|
|
end)
|
|
|
|
AddEventHandler('onResourceStart', function(resourceName)
|
|
if GetCurrentResourceName() == resourceName then
|
|
debugPrint("License-System Client gestartet (Event-basiert)")
|
|
|
|
-- Warten bis QBCore geladen ist
|
|
while not QBCore do
|
|
Wait(100)
|
|
end
|
|
|
|
debugPrint("QBCore erfolgreich geladen")
|
|
end
|
|
end)
|
|
|
|
-- Player laden Event
|
|
RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function()
|
|
debugPrint("Spieler geladen - License-System bereit")
|
|
end)
|
|
|
|
-- Job Update Event
|
|
RegisterNetEvent('QBCore:Client:OnJobUpdate', function(JobInfo)
|
|
debugPrint("Job aktualisiert: " .. JobInfo.name)
|
|
end)
|
|
|
|
-- Initialisierung
|
|
CreateThread(function()
|
|
debugPrint("License-System Client Thread gestartet")
|
|
|
|
-- Warten bis Spieler gespawnt ist
|
|
while not NetworkIsPlayerActive(PlayerId()) do
|
|
Wait(100)
|
|
end
|
|
|
|
debugPrint("Spieler ist aktiv - System bereit")
|
|
end)
|
|
|
|
-- Zusätzliche Utility-Funktionen
|
|
local function requestLicenseWithTimeout(eventName, targetId, timeout)
|
|
timeout = timeout or 5000
|
|
local requestId = math.random(1000, 9999)
|
|
|
|
pendingRequests[requestId] = {
|
|
timestamp = GetGameTimer(),
|
|
timeout = timeout
|
|
}
|
|
|
|
debugPrint("Sende Request mit Timeout: " .. eventName .. " (ID: " .. requestId .. ")")
|
|
TriggerServerEvent(eventName, targetId, requestId)
|
|
|
|
-- Timeout-Handler
|
|
CreateThread(function()
|
|
Wait(timeout)
|
|
if pendingRequests[requestId] then
|
|
pendingRequests[requestId] = nil
|
|
debugPrint("^1Request Timeout: " .. eventName .. " (ID: " .. requestId .. ")^7")
|
|
showNotification('Anfrage-Timeout! Versuche es erneut.', 'error')
|
|
end
|
|
end)
|
|
|
|
return requestId
|
|
end
|
|
|
|
-- Erweiterte Error-Handling
|
|
local function safeExecute(func, errorMessage)
|
|
local success, error = pcall(func)
|
|
if not success then
|
|
debugPrint("^1Fehler: " .. (errorMessage or "Unbekannter Fehler") .. "^7")
|
|
debugPrint("^1Details: " .. tostring(error) .. "^7")
|
|
showNotification('Ein Fehler ist aufgetreten!', 'error')
|
|
end
|
|
return success
|
|
end
|
|
|
|
-- Performance-Monitoring
|
|
local performanceStats = {
|
|
menuOpens = 0,
|
|
licenseShows = 0,
|
|
errors = 0
|
|
}
|
|
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(60000) -- Jede Minute
|
|
|
|
if Config.Debug then
|
|
debugPrint("=== Performance Stats ===")
|
|
debugPrint("Menü-Öffnungen: " .. performanceStats.menuOpens)
|
|
debugPrint("Lizenz-Anzeigen: " .. performanceStats.licenseShows)
|
|
debugPrint("Fehler: " .. performanceStats.errors)
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- Stats aktualisieren
|
|
local originalOpenLicenseMenu = openLicenseMenu
|
|
openLicenseMenu = function()
|
|
performanceStats.menuOpens = performanceStats.menuOpens + 1
|
|
return originalOpenLicenseMenu()
|
|
end
|
|
|
|
local originalShowLicense = showLicense
|
|
showLicense = function(licenseData)
|
|
performanceStats.licenseShows = performanceStats.licenseShows + 1
|
|
return originalShowLicense(licenseData)
|
|
end
|
|
|
|
-- Erweiterte Fehlerbehandlung für Events
|
|
local function safeEventHandler(eventName, handler)
|
|
RegisterNetEvent(eventName, function(...)
|
|
local success, error = pcall(handler, ...)
|
|
if not success then
|
|
debugPrint("^1Fehler in Event " .. eventName .. ": " .. tostring(error) .. "^7")
|
|
performanceStats.errors = performanceStats.errors + 1
|
|
showNotification('Ein Fehler ist aufgetreten!', 'error')
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- Zusätzliche Validierungen
|
|
local function validateLicenseData(licenseData)
|
|
if not licenseData then
|
|
debugPrint("^1Validierung fehlgeschlagen: licenseData ist nil^7")
|
|
return false
|
|
end
|
|
|
|
if not licenseData.license then
|
|
debugPrint("^1Validierung fehlgeschlagen: license-Objekt fehlt^7")
|
|
return false
|
|
end
|
|
|
|
if not licenseData.license.license_type then
|
|
debugPrint("^1Validierung fehlgeschlagen: license_type fehlt^7")
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- Erweiterte showLicense Funktion mit Validierung
|
|
local function showLicenseWithValidation(licenseData)
|
|
if not validateLicenseData(licenseData) then
|
|
showNotification('Ungültige Lizenz-Daten!', 'error')
|
|
return
|
|
end
|
|
|
|
debugPrint("Zeige Lizenz: " .. licenseData.license.license_type)
|
|
|
|
-- Zusätzliche Daten für NUI vorbereiten
|
|
local nuitData = {
|
|
license = licenseData.license,
|
|
config = licenseData.config or Config.LicenseTypes[licenseData.license.license_type],
|
|
timestamp = GetGameTimer(),
|
|
playerData = QBCore.Functions.GetPlayerData()
|
|
}
|
|
|
|
SendNUIMessage({
|
|
action = 'showLicense',
|
|
data = nuitData
|
|
})
|
|
|
|
SetNuiFocus(true, true)
|
|
isLicenseShowing = true
|
|
performanceStats.licenseShows = performanceStats.licenseShows + 1
|
|
end
|
|
|
|
-- Überschreibe die ursprüngliche showLicense Funktion
|
|
showLicense = showLicenseWithValidation
|
|
|
|
-- Erweiterte Menü-Funktionen mit Error-Handling
|
|
local function safeMenuAction(action, errorMessage)
|
|
return function(...)
|
|
local success, error = pcall(action, ...)
|
|
if not success then
|
|
debugPrint("^1Menü-Fehler: " .. (errorMessage or "Unbekannt") .. "^7")
|
|
debugPrint("^1Details: " .. tostring(error) .. "^7")
|
|
performanceStats.errors = performanceStats.errors + 1
|
|
showNotification('Menü-Fehler aufgetreten!', 'error')
|
|
|
|
-- Menü schließen bei Fehler
|
|
if lib.getOpenContextMenu() then
|
|
lib.hideContext()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Cache für Spieler-Daten
|
|
local playerCache = {}
|
|
local cacheTimeout = 30000 -- 30 Sekunden
|
|
|
|
local function getCachedPlayerData(playerId)
|
|
local now = GetGameTimer()
|
|
local cached = playerCache[playerId]
|
|
|
|
if cached and (now - cached.timestamp) < cacheTimeout then
|
|
debugPrint("Verwende gecachte Spieler-Daten für ID: " .. playerId)
|
|
return cached.data
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function setCachedPlayerData(playerId, data)
|
|
playerCache[playerId] = {
|
|
data = data,
|
|
timestamp = GetGameTimer()
|
|
}
|
|
debugPrint("Spieler-Daten gecacht für ID: " .. playerId)
|
|
end
|
|
|
|
-- Cache-Cleanup
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(60000) -- Jede Minute
|
|
|
|
local now = GetGameTimer()
|
|
local cleaned = 0
|
|
|
|
for playerId, cached in pairs(playerCache) do
|
|
if (now - cached.timestamp) > cacheTimeout then
|
|
playerCache[playerId] = nil
|
|
cleaned = cleaned + 1
|
|
end
|
|
end
|
|
|
|
if cleaned > 0 then
|
|
debugPrint("Cache bereinigt: " .. cleaned .. " Einträge entfernt")
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- Erweiterte Nearby-Players Funktion mit Cache
|
|
local function getNearbyPlayersWithCache(radius)
|
|
radius = radius or 5.0
|
|
local players = {}
|
|
local playerPed = PlayerPedId()
|
|
local playerCoords = GetEntityCoords(playerPed)
|
|
|
|
for _, playerId in ipairs(GetActivePlayers()) do
|
|
local targetPed = GetPlayerPed(playerId)
|
|
if targetPed ~= playerPed then
|
|
local targetCoords = GetEntityCoords(targetPed)
|
|
local distance = #(playerCoords - targetCoords)
|
|
|
|
if distance <= radius then
|
|
local serverId = GetPlayerServerId(playerId)
|
|
local playerName = GetPlayerName(playerId)
|
|
|
|
-- Cache-Daten verwenden wenn verfügbar
|
|
local cachedData = getCachedPlayerData(serverId)
|
|
local playerData = cachedData or {
|
|
id = serverId,
|
|
name = playerName,
|
|
ped = targetPed
|
|
}
|
|
|
|
playerData.distance = math.floor(distance * 100) / 100
|
|
|
|
-- Daten cachen wenn nicht bereits gecacht
|
|
if not cachedData then
|
|
setCachedPlayerData(serverId, playerData)
|
|
end
|
|
|
|
table.insert(players, playerData)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Nach Entfernung sortieren
|
|
table.sort(players, function(a, b)
|
|
return a.distance < b.distance
|
|
end)
|
|
|
|
debugPrint("Gefundene Spieler in der Nähe: " .. #players)
|
|
return players
|
|
end
|
|
|
|
-- Überschreibe die ursprüngliche getNearbyPlayers Funktion
|
|
getNearbyPlayers = getNearbyPlayersWithCache
|
|
|
|
-- Erweiterte Notification-Funktion
|
|
local function showNotificationWithSound(message, type, sound)
|
|
QBCore.Functions.Notify(message, type or 'primary')
|
|
|
|
if sound and Config.Sounds and Config.Sounds[sound] then
|
|
PlaySoundFrontend(-1, Config.Sounds[sound].name, Config.Sounds[sound].set, true)
|
|
end
|
|
end
|
|
|
|
-- Erweiterte Event-Handler mit besserer Fehlerbehandlung
|
|
RegisterNetEvent('license-system:client:receiveLicenseWithValidation', function(licenseData)
|
|
debugPrint("=== Event: receiveLicenseWithValidation ===")
|
|
|
|
if not validateLicenseData(licenseData) then
|
|
showNotificationWithSound('Ungültige Lizenz-Daten erhalten!', 'error', 'error')
|
|
return
|
|
end
|
|
|
|
debugPrint("Gültige Lizenz-Daten erhalten: " .. licenseData.license.license_type)
|
|
showLicense(licenseData)
|
|
end)
|
|
|
|
-- Erweiterte Lizenz-Anzeige mit Animationen
|
|
local function showLicenseWithAnimation(licenseData)
|
|
if not validateLicenseData(licenseData) then
|
|
showNotification('Ungültige Lizenz-Daten!', 'error')
|
|
return
|
|
end
|
|
|
|
-- Animation starten
|
|
local playerPed = PlayerPedId()
|
|
|
|
-- Lizenz-Animation (falls gewünscht)
|
|
if Config.Animations and Config.Animations.show_license then
|
|
local anim = Config.Animations.show_license
|
|
RequestAnimDict(anim.dict)
|
|
|
|
while not HasAnimDictLoaded(anim.dict) do
|
|
Wait(10)
|
|
end
|
|
|
|
TaskPlayAnim(playerPed, anim.dict, anim.name, 8.0, -8.0, -1, anim.flag or 49, 0, false, false, false)
|
|
end
|
|
|
|
-- Lizenz anzeigen
|
|
showLicense(licenseData)
|
|
|
|
-- Animation nach kurzer Zeit stoppen
|
|
if Config.Animations and Config.Animations.show_license then
|
|
CreateThread(function()
|
|
Wait(2000)
|
|
ClearPedTasks(playerPed)
|
|
end)
|
|
end
|
|
end
|
|
|
|
-- Erweiterte Menü-Navigation
|
|
local menuHistory = {}
|
|
|
|
local function pushMenuHistory(menuId)
|
|
table.insert(menuHistory, menuId)
|
|
debugPrint("Menü-Historie: " .. menuId .. " hinzugefügt")
|
|
end
|
|
|
|
local function popMenuHistory()
|
|
if #menuHistory > 0 then
|
|
local lastMenu = table.remove(menuHistory)
|
|
debugPrint("Menü-Historie: " .. lastMenu .. " entfernt")
|
|
return lastMenu
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function clearMenuHistory()
|
|
menuHistory = {}
|
|
debugPrint("Menü-Historie geleert")
|
|
end
|
|
|
|
-- Erweiterte Cleanup-Funktion
|
|
local function cleanup()
|
|
debugPrint("Führe erweiterte Cleanup-Routine aus...")
|
|
|
|
-- NUI schließen
|
|
if isLicenseShowing then
|
|
closeLicense()
|
|
end
|
|
|
|
-- Menüs schließen
|
|
if lib.getOpenContextMenu() then
|
|
lib.hideContext()
|
|
end
|
|
|
|
-- Cache leeren
|
|
playerCache = {}
|
|
|
|
-- Historie leeren
|
|
clearMenuHistory()
|
|
|
|
-- Pending Requests leeren
|
|
pendingRequests = {}
|
|
|
|
-- Animationen stoppen
|
|
local playerPed = PlayerPedId()
|
|
if DoesEntityExist(playerPed) then
|
|
ClearPedTasks(playerPed)
|
|
end
|
|
|
|
debugPrint("Cleanup abgeschlossen")
|
|
end
|
|
|
|
-- Erweiterte Resource-Stop Handler
|
|
AddEventHandler('onResourceStop', function(resourceName)
|
|
if GetCurrentResourceName() == resourceName then
|
|
cleanup()
|
|
debugPrint("License-System Client gestoppt (erweitert)")
|
|
end
|
|
end)
|
|
|
|
-- Erweiterte Resource-Start Handler
|
|
AddEventHandler('onResourceStart', function(resourceName)
|
|
if GetCurrentResourceName() == resourceName then
|
|
debugPrint("License-System Client gestartet (erweitert)")
|
|
|
|
-- Initialisierung
|
|
CreateThread(function()
|
|
-- Warten bis QBCore geladen ist
|
|
while not QBCore do
|
|
Wait(100)
|
|
end
|
|
|
|
debugPrint("QBCore erfolgreich geladen (erweitert)")
|
|
|
|
-- Zusätzliche Initialisierung
|
|
Wait(1000)
|
|
|
|
-- Performance-Stats zurücksetzen
|
|
performanceStats = {
|
|
menuOpens = 0,
|
|
licenseShows = 0,
|
|
errors = 0
|
|
}
|
|
|
|
debugPrint("Erweiterte Initialisierung abgeschlossen")
|
|
end)
|
|
end
|
|
end)
|
|
|
|
-- Erweiterte Debug-Funktionen
|
|
local function debugPlayerInfo()
|
|
if not Config.Debug then return end
|
|
|
|
local PlayerData = QBCore.Functions.GetPlayerData()
|
|
debugPrint("=== PLAYER DEBUG INFO ===")
|
|
debugPrint("Name: " .. (PlayerData.charinfo and PlayerData.charinfo.firstname .. " " .. PlayerData.charinfo.lastname or "Unbekannt"))
|
|
debugPrint("Job: " .. (PlayerData.job and PlayerData.job.name or "Unbekannt"))
|
|
debugPrint("CitizenID: " .. (PlayerData.citizenid or "Unbekannt"))
|
|
debugPrint("Server ID: " .. GetPlayerServerId(PlayerId()))
|
|
debugPrint("========================")
|
|
end
|
|
|
|
-- Debug-Command
|
|
RegisterCommand('licensedebug', function()
|
|
if not Config.Debug then
|
|
showNotification('Debug-Modus ist deaktiviert!', 'error')
|
|
return
|
|
end
|
|
|
|
debugPlayerInfo()
|
|
|
|
debugPrint("=== SYSTEM DEBUG INFO ===")
|
|
debugPrint("Nearby Players: " .. #nearbyPlayers)
|
|
debugPrint("License Showing: " .. tostring(isLicenseShowing))
|
|
debugPrint("Menu Open: " .. tostring(lib.getOpenContextMenu() ~= nil))
|
|
debugPrint("Cached Players: " .. #playerCache)
|
|
debugPrint("Menu History: " .. #menuHistory)
|
|
debugPrint("Pending Requests: " .. #pendingRequests)
|
|
debugPrint("========================")
|
|
|
|
showNotification('Debug-Informationen in der Konsole ausgegeben!', 'success')
|
|
end, false)
|
|
|
|
-- Erweiterte Keybind-Behandlung
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(0)
|
|
|
|
-- ESC-Taste für Lizenz schließen
|
|
if isLicenseShowing then
|
|
if IsControlJustPressed(0, 322) then -- ESC
|
|
closeLicense()
|
|
end
|
|
end
|
|
|
|
-- Zusätzliche Hotkeys (falls konfiguriert)
|
|
if Config.Keybinds and Config.Keybinds.emergency_close then
|
|
if IsControlJustPressed(0, Config.Keybinds.emergency_close.control) then
|
|
cleanup()
|
|
showNotification('Notfall-Schließung aktiviert!', 'info')
|
|
end
|
|
end
|
|
|
|
-- Performance-Optimierung
|
|
if not isLicenseShowing and not lib.getOpenContextMenu() then
|
|
Wait(500)
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- Erweiterte Netzwerk-Events mit Retry-Mechanismus
|
|
local function sendEventWithRetry(eventName, data, maxRetries)
|
|
maxRetries = maxRetries or 3
|
|
local retries = 0
|
|
|
|
local function attemptSend()
|
|
retries = retries + 1
|
|
debugPrint("Sende Event: " .. eventName .. " (Versuch " .. retries .. "/" .. maxRetries .. ")")
|
|
|
|
TriggerServerEvent(eventName, table.unpack(data or {}))
|
|
|
|
-- Timeout für Response
|
|
CreateThread(function()
|
|
Wait(5000) -- 5 Sekunden Timeout
|
|
|
|
if retries < maxRetries then
|
|
debugPrint("^3Timeout für Event " .. eventName .. " - Wiederhole...^7")
|
|
attemptSend()
|
|
else
|
|
debugPrint("^1Maximale Wiederholungen für Event " .. eventName .. " erreicht^7")
|
|
showNotification('Netzwerk-Fehler! Bitte versuche es später erneut.', 'error')
|
|
end
|
|
end)
|
|
end
|
|
|
|
attemptSend()
|
|
end
|
|
|
|
-- Erweiterte Export-Funktionen für andere Resources
|
|
exports('hasLicense', function(licenseType)
|
|
-- Diese Funktion kann von anderen Resources verwendet werden
|
|
local PlayerData = QBCore.Functions.GetPlayerData()
|
|
if not PlayerData or not PlayerData.citizenid then return false end
|
|
|
|
-- Hier würde normalerweise eine Server-Anfrage gemacht werden
|
|
-- Für jetzt geben wir false zurück
|
|
return false
|
|
end)
|
|
|
|
exports('showPlayerLicense', function(targetId, licenseType)
|
|
-- Export für andere Resources um Lizenzen anzuzeigen
|
|
if licenseType then
|
|
TriggerServerEvent('license-system:server:requestSpecificLicense', targetId, licenseType)
|
|
else
|
|
showPlayerLicense(targetId)
|
|
end
|
|
end)
|
|
|
|
exports('openLicenseMenu', function()
|
|
-- Export für andere Resources um das Lizenz-Menü zu öffnen
|
|
openLicenseMenu()
|
|
end)
|
|
|
|
-- Finaler Debug-Output
|
|
debugPrint("License-System Client vollständig geladen - Alle Funktionen verfügbar")
|
|
|