aether-framework/resources/[aether]/aether-admin/client/main.lua
Jerrit Fritzsche 32879ac07b aether-admin: Netzwerk-Race beim Join beheben
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>
2026-07-27 19:36:38 +02:00

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)