Die Client-Threads (Positions-Meldung, Wetter/Zeit) liefen ab Resource- Start ungebremst — also schon während Ladebildschirm/Charakterauswahl, bevor der Spieler gespawnt ist. Zusammen mit den Server-Events der Character-NUI (eigener Thread) führte das zu gleichzeitigen Netz-Sends: "SendNetPacket called from multiple threads" -> Client-Crash. Fix: Nichts Vernetztes vor 'aether:client:bereit' (Spieler geladen). Weltzustand wird beim Spawn einmalig angewandt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
307 lines
11 KiB
Lua
307 lines
11 KiB
Lua
-- =====================================================================
|
|
-- aether-admin — Clientlogik
|
|
--
|
|
-- Öffnet das Panel und führt die Eingriffe aus, die nur am Client
|
|
-- möglich sind (Teleport, Heilen, Spectate, Fahrzeug spawnen …). Der
|
|
-- Client entscheidet nichts selbst — er tut, was der (rechteprüfende)
|
|
-- Server ihm aufträgt.
|
|
-- =====================================================================
|
|
|
|
local istOffen = false
|
|
local darfPanel = false -- vom Server bestätigt
|
|
local geladen = false -- Spieler ist fertig geladen und gespawnt
|
|
|
|
-- Erst wenn der Core den Spieler gespawnt hat, darf dieser Client etwas
|
|
-- Vernetztes tun. Vorher (Ladebildschirm/Charakterauswahl) würden Netz-
|
|
-- Sends aus Hintergrund-Threads mit dem Spawn kollidieren
|
|
-- ("SendNetPacket called from multiple threads").
|
|
AddEventHandler('aether:client:bereit', function()
|
|
geladen = true
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Hilfen
|
|
-- ---------------------------------------------------------------------
|
|
local function SendeAnNui(nachricht)
|
|
SendNuiMessage(json.encode(nachricht))
|
|
end
|
|
|
|
local function Meldung(text)
|
|
if not text or text == '' then return end
|
|
BeginTextCommandThefeedPost('STRING')
|
|
AddTextComponentSubstringPlayerName(text)
|
|
EndTextCommandThefeedPostTicker(false, true)
|
|
end
|
|
|
|
local function Schliessen()
|
|
if not istOffen then return end
|
|
istOffen = false
|
|
SetNuiFocus(false, false)
|
|
SendeAnNui({ aktion = 'schliessen' })
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Panel öffnen / schließen
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether-admin:zeige', function(daten)
|
|
darfPanel = true
|
|
istOffen = true
|
|
SetNuiFocus(true, true)
|
|
daten.aktion = 'oeffnen'
|
|
SendeAnNui(daten)
|
|
end)
|
|
|
|
RegisterNetEvent('aether-admin:verweigert', function(text)
|
|
Meldung('~r~' .. (text or 'Kein Zugriff.'))
|
|
end)
|
|
|
|
RegisterNetEvent('aether-admin:antwort', function(ruf, ergebnis, fehler)
|
|
SendeAnNui({ aktion = 'antwort', ruf = ruf, ergebnis = ergebnis, fehler = fehler })
|
|
end)
|
|
|
|
RegisterCommand('adminpanel', function()
|
|
TriggerServerEvent('aether-admin:oeffnen')
|
|
end, false)
|
|
RegisterKeyMapping('adminpanel', 'Admin-Panel öffnen', 'keyboard', 'F9')
|
|
|
|
-- Melder aus dem Spiel: /report <Text>
|
|
RegisterCommand('report', function(source, args)
|
|
local text = table.concat(args, ' ')
|
|
if #text < 2 then
|
|
Meldung('~y~Nutzung: /report <dein Anliegen>')
|
|
return
|
|
end
|
|
TriggerServerEvent('aether-admin:report', text:sub(1, 120), text)
|
|
end, false)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- NUI-Callbacks
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNUICallback('schliessen', function(_, cb)
|
|
Schliessen()
|
|
cb({ ok = true })
|
|
end)
|
|
|
|
--- Zentraler Aufruf: Oberfläche → Server → Antwort
|
|
RegisterNUICallback('aufruf', function(daten, cb)
|
|
TriggerServerEvent('aether-admin:aufruf',
|
|
tostring(daten.modul or ''), tostring(daten.aktion or ''),
|
|
daten.daten or {}, daten.ruf)
|
|
cb({ ok = true })
|
|
end)
|
|
|
|
--- Wegpunkt setzen (aus Weltkarte / Spieleraktionen)
|
|
RegisterNUICallback('wegpunkt', function(daten, cb)
|
|
if daten.x and daten.y then SetNewWaypoint(daten.x + 0.0, daten.y + 0.0) end
|
|
cb({ ok = true })
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Zustands-Eingriffe am eigenen Ped (vom Server ausgelöst)
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether-admin:client:heal', function()
|
|
local ped = PlayerPedId()
|
|
SetEntityHealth(ped, GetEntityMaxHealth(ped))
|
|
ClearPedBloodDamage(ped)
|
|
ClearPedWetness(ped)
|
|
Meldung('~g~Du wurdest geheilt.')
|
|
end)
|
|
|
|
RegisterNetEvent('aether-admin:client:revive', function()
|
|
local ped = PlayerPedId()
|
|
local erledigt = pcall(function() exports['aether-death']:Wiederbeleben() end)
|
|
if not erledigt then
|
|
local pos = GetEntityCoords(ped)
|
|
NetworkResurrectLocalPlayer(pos.x, pos.y, pos.z, GetEntityHeading(ped), true, false)
|
|
SetEntityHealth(ped, GetEntityMaxHealth(ped))
|
|
end
|
|
Meldung('~g~Du wurdest wiederbelebt.')
|
|
end)
|
|
|
|
RegisterNetEvent('aether-admin:client:armor', function()
|
|
SetPedArmour(PlayerPedId(), 100)
|
|
Meldung('~g~Rüstung aufgefüllt.')
|
|
end)
|
|
|
|
RegisterNetEvent('aether-admin:client:freeze', function(an)
|
|
local ped = PlayerPedId()
|
|
FreezeEntityPosition(ped, an and true or false)
|
|
Meldung(an and '~b~Du wurdest eingefroren.' or '~g~Du wurdest aufgetaut.')
|
|
end)
|
|
|
|
RegisterNetEvent('aether-admin:client:god', function(an)
|
|
local ped = PlayerPedId()
|
|
SetEntityInvincible(ped, an and true or false)
|
|
SetPlayerInvincible(PlayerId(), an and true or false)
|
|
end)
|
|
|
|
RegisterNetEvent('aether-admin:client:teleport', function(pos)
|
|
if not pos or not pos.x then return end
|
|
local ped = PlayerPedId()
|
|
SetEntityCoords(ped, pos.x + 0.0, pos.y + 0.0, pos.z + 0.0, false, false, false, false)
|
|
end)
|
|
|
|
RegisterNetEvent('aether-admin:client:hinweis', function(text)
|
|
Meldung('~y~' .. tostring(text))
|
|
end)
|
|
|
|
RegisterNetEvent('aether-admin:client:broadcast', function(daten)
|
|
local farbe = daten.typ == 'Warnung' and '~r~' or daten.typ == 'Neustart' and '~o~' or '~b~'
|
|
Meldung(('%s[%s] ~s~%s'):format(farbe, daten.typ or 'Info', daten.text or ''))
|
|
PlaySoundFrontend(-1, 'Menu_Accept', 'Phone_SoundSet_Default', true)
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Fahrzeug spawnen (am eigenen Ped)
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether-admin:client:fahrzeug', function(modell)
|
|
local hash = GetHashKey(modell)
|
|
if not IsModelInCdimage(hash) or not IsModelAVehicle(hash) then
|
|
Meldung('~r~Unbekanntes Fahrzeugmodell: ' .. tostring(modell))
|
|
return
|
|
end
|
|
RequestModel(hash)
|
|
local zeit = GetGameTimer()
|
|
while not HasModelLoaded(hash) and GetGameTimer() - zeit < 5000 do Wait(10) end
|
|
if not HasModelLoaded(hash) then return end
|
|
|
|
local ped = PlayerPedId()
|
|
local pos = GetEntityCoords(ped)
|
|
local heading = GetEntityHeading(ped)
|
|
local veh = CreateVehicle(hash, pos.x + 2.5, pos.y, pos.z, heading, true, false)
|
|
SetVehicleNumberPlateText(veh, 'ADMIN')
|
|
SetPedIntoVehicle(ped, veh, -1)
|
|
SetModelAsNoLongerNeeded(hash)
|
|
Meldung('~g~Fahrzeug gespawnt: ' .. modell)
|
|
end)
|
|
|
|
RegisterNetEvent('aether-admin:client:fahrzeugFix', function(netId)
|
|
local veh = NetworkGetEntityFromNetworkId(netId)
|
|
if veh and veh ~= 0 and DoesEntityExist(veh) then
|
|
SetVehicleFixed(veh); SetVehicleDeformationFixed(veh)
|
|
SetVehicleEngineHealth(veh, 1000.0); SetVehicleBodyHealth(veh, 1000.0)
|
|
SetVehicleDirtLevel(veh, 0.0)
|
|
Meldung('~g~Fahrzeug repariert.')
|
|
else
|
|
Meldung('~y~Fahrzeug nicht in Reichweite — bitte näher heranfahren.')
|
|
end
|
|
end)
|
|
|
|
RegisterNetEvent('aether-admin:client:fahrzeugHer', function(netId)
|
|
local veh = NetworkGetEntityFromNetworkId(netId)
|
|
if veh and veh ~= 0 and DoesEntityExist(veh) then
|
|
local pos = GetEntityCoords(PlayerPedId())
|
|
local heading = GetEntityHeading(PlayerPedId())
|
|
SetEntityCoords(veh, pos.x + 3.0, pos.y, pos.z, false, false, false, false)
|
|
SetEntityHeading(veh, heading)
|
|
Meldung('~g~Fahrzeug hergeholt.')
|
|
else
|
|
Meldung('~y~Fahrzeug nicht in Reichweite.')
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Beobachten (native Spectate) — zusätzlich zur WebRTC-Kamera
|
|
-- ---------------------------------------------------------------------
|
|
local beobachtet = false
|
|
|
|
RegisterNetEvent('aether-admin:client:spectate', function(daten)
|
|
if beobachtet then StoppeSpectate(); return end
|
|
local zielSpieler = GetPlayerFromServerId(daten.ziel)
|
|
|
|
-- Ohne Ziel in Reichweite zuerst zur letzten bekannten Position
|
|
if daten.pos then
|
|
SetEntityCoords(PlayerPedId(), daten.pos.x + 0.0, daten.pos.y + 0.0,
|
|
daten.pos.z + 30.0, false, false, false, false)
|
|
Wait(600)
|
|
end
|
|
|
|
local zielPed = GetPlayerFromServerId(daten.ziel) ~= -1
|
|
and GetPlayerPed(GetPlayerFromServerId(daten.ziel)) or 0
|
|
if zielPed == 0 or not DoesEntityExist(zielPed) then
|
|
Meldung('~y~Ziel nicht erreichbar (zu weit weg?).')
|
|
return
|
|
end
|
|
|
|
NetworkSetInSpectatorMode(true, zielPed)
|
|
beobachtet = true
|
|
Meldung('~b~Beobachtungsmodus — [Rücktaste] beendet ihn.')
|
|
|
|
CreateThread(function()
|
|
while beobachtet do
|
|
Wait(0)
|
|
if IsControlJustReleased(0, 194) then StoppeSpectate() end -- Backspace
|
|
end
|
|
end)
|
|
end)
|
|
|
|
function StoppeSpectate()
|
|
if not beobachtet then return end
|
|
NetworkSetInSpectatorMode(false, PlayerPedId())
|
|
beobachtet = false
|
|
Meldung('~g~Beobachtung beendet.')
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Weltzustand anwenden (Zeit / Wetter / Blackout)
|
|
-- ---------------------------------------------------------------------
|
|
local welt = { stunde = 12, minute = 0, wetter = 'CLEAR',
|
|
zeitFix = false, wetterFix = false, blackout = false }
|
|
|
|
--- Weltzustand merken. Angewandt wird er erst, wenn der Spieler im Spiel ist
|
|
--- (sonst kollidiert das Wetter-Native mit dem Spawn).
|
|
RegisterNetEvent('aether-admin:client:welt', function(neu)
|
|
if type(neu) ~= 'table' then return end
|
|
welt = neu
|
|
if geladen then
|
|
SetWeatherTypeOverTime(welt.wetter, 15.0)
|
|
SetArtificialLightsState(welt.blackout and true or false)
|
|
end
|
|
end)
|
|
|
|
-- Beim Laden den zwischengespeicherten Weltzustand einmalig anwenden
|
|
AddEventHandler('aether:client:bereit', function()
|
|
SetWeatherTypeOverTime(welt.wetter, 15.0)
|
|
SetArtificialLightsState(welt.blackout and true or false)
|
|
end)
|
|
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(geladen and welt.zeitFix and 0 or 2000)
|
|
if geladen then
|
|
if welt.zeitFix then
|
|
NetworkOverrideClockTime(welt.stunde, welt.minute, 0)
|
|
end
|
|
if welt.wetterFix then
|
|
SetWeatherTypeNow(welt.wetter)
|
|
end
|
|
if welt.blackout then
|
|
SetArtificialLightsState(true)
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Positions-Cache füttern (Weltkarte, Teleport, Spectate)
|
|
-- ---------------------------------------------------------------------
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(3000)
|
|
-- Erst melden, wenn der Spieler wirklich im Spiel ist
|
|
if geladen then
|
|
local pos = GetEntityCoords(PlayerPedId())
|
|
TriggerServerEvent('aether-admin:pos', { x = pos.x, y = pos.y, z = pos.z })
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Aufräumen
|
|
-- ---------------------------------------------------------------------
|
|
AddEventHandler('onResourceStop', function(ressource)
|
|
if ressource ~= GetCurrentResourceName() then return end
|
|
SetNuiFocus(false, false)
|
|
if beobachtet then NetworkSetInSpectatorMode(false, PlayerPedId()) end
|
|
end)
|