ESX = exports['es_extended']:getSharedObject() local isNuiOpen = false local currentFactionId = nil local pendingInvite = nil local isGarageOpen = false local spawnedNpcs = {} -- key -> ped handle local playerLoaded = false -- PlayerData laden (event-basiert, kein Polling) Citizen.CreateThread(function() -- Einmaliger Check bei ensure/restart (Spieler ist evtl. schon geladen) local pd = ESX.GetPlayerData() if pd and pd.job and pd.job.name then ESX.PlayerData = pd playerLoaded = true end end) RegisterNetEvent('esx:playerLoaded') AddEventHandler('esx:playerLoaded', function(xPlayer) ESX.PlayerData = xPlayer playerLoaded = true end) -- ===================== -- NPC SPAWNING -- ===================== function SpawnNPC(key, coords, model, scenario) if spawnedNpcs[key] then return end local hash = GetHashKey(model) RequestModel(hash) while not HasModelLoaded(hash) do Citizen.Wait(10) end local ped = CreatePed(4, hash, coords.x, coords.y, coords.z - 1.0, coords.w, false, true) SetEntityHeading(ped, coords.w) FreezeEntityPosition(ped, true) SetEntityInvincible(ped, true) SetBlockingOfNonTemporaryEvents(ped, true) TaskStartScenarioInPlace(ped, scenario, 0, true) SetModelAsNoLongerNeeded(hash) spawnedNpcs[key] = ped end -- Spawn NPCs fuer alle Fraktionen bei Resource-Start Citizen.CreateThread(function() Citizen.Wait(2000) for factionId, config in pairs(Config.Factions) do -- Garage NPC if config.garageNpc and config.garage then local model = config.garageNpcModel or Config.DefaultGarageNpcModel local scenario = config.garageNpcScenario or Config.DefaultGarageNpcScenario SpawnNPC('garage_' .. factionId, config.garage, model, scenario) end -- Stash NPC if config.stashNpc and config.stash then local model = config.stashNpcModel or Config.DefaultStashNpcModel local scenario = config.stashNpcScenario or Config.DefaultStashNpcScenario SpawnNPC('stash_' .. factionId, config.stash, model, scenario) end end end) -- Cleanup on resource stop AddEventHandler('onResourceStop', function(resourceName) if resourceName ~= GetCurrentResourceName() then return end for _, ped in pairs(spawnedNpcs) do if DoesEntityExist(ped) then DeleteEntity(ped) end end end) -- ===================== -- INTERACTION THREAD (Computer + Garage + Stash) -- ===================== Citizen.CreateThread(function() -- Warte bis ESX PlayerData geladen ist while not playerLoaded do Citizen.Wait(500) end local nearPoint = false local COARSE_DIST = 50.0 while true do local sleep = 1000 local playerCoords = GetEntityCoords(PlayerPedId()) local foundNear = false local playerJob = ESX.PlayerData.job if not isNuiOpen and not isGarageOpen and playerJob and playerJob.name then local factionConfig = Config.Factions[playerJob.name] if factionConfig then -- Computer Check if factionConfig.computer then local compCoords = factionConfig.computer local dist = #(playerCoords - vector3(compCoords.x, compCoords.y, compCoords.z)) if dist <= Config.InteractionDistance then if playerJob.grade >= (factionConfig.minComputerGrade or 0) then sleep = 0 foundNear = true exports[Config.HelpNotify.export][Config.HelpNotify.showFunc]('Fraktions-Computer', 'E') if IsControlJustReleased(0, 38) then OpenFactionMenu(playerJob.name) end end end end -- Garage Check (separater Standort) if not foundNear and factionConfig.garage then local garageCoords = factionConfig.garage local dist = #(playerCoords - vector3(garageCoords.x, garageCoords.y, garageCoords.z)) if dist <= Config.InteractionDistance then sleep = 0 foundNear = true exports[Config.HelpNotify.export][Config.HelpNotify.showFunc]('Fraktions-Garage', 'E') if IsControlJustReleased(0, 38) then OpenGarageMenu(playerJob.name) end end end -- Stash Check (separater Standort) if not foundNear and factionConfig.stash then local stashCoords = factionConfig.stash local dist = #(playerCoords - vector3(stashCoords.x, stashCoords.y, stashCoords.z)) if dist <= Config.InteractionDistance then sleep = 0 foundNear = true exports[Config.HelpNotify.export][Config.HelpNotify.showFunc]('Fraktions-Lager', 'E') if IsControlJustReleased(0, 38) then OpenFactionStash(playerJob.name) end end end end end if not foundNear and nearPoint then exports[Config.HelpNotify.export][Config.HelpNotify.hideFunc]() end nearPoint = foundNear Citizen.Wait(sleep) end end) -- ===================== -- NUI MANAGEMENT -- ===================== function OpenFactionMenu(factionId) if isNuiOpen then return end ESX.TriggerServerCallback('mercyv-fraks:getFactionData', function(data) if not data then return end isNuiOpen = true currentFactionId = factionId SetNuiFocus(true, true) SendNUIMessage({ type = 'open', factionId = data.factionId, factionLabel = data.factionLabel, factionImage = data.factionImage or '', playerGrade = data.playerGrade, grades = data.grades, treasury = data.treasury, totalMembers = data.totalMembers, onlineMembers = data.onlineMembers, permissions = data.permissions, permissionValues = data.permissionValues, permissionLabels = Config.PermissionLabels, theme = data.theme, vehicles = data.vehicles, }) end) end function CloseMenu() if not isNuiOpen then return end isNuiOpen = false currentFactionId = nil SetNuiFocus(false, false) SendNUIMessage({ type = 'close' }) end -- ===================== -- NUI CALLBACKS -- ===================== RegisterNUICallback('close', function(_, cb) CloseMenu() cb('ok') end) RegisterNUICallback('getMembers', function(_, cb) ESX.TriggerServerCallback('mercyv-fraks:getMembers', function(members) SendNUIMessage({ type = 'updateMembers', members = members }) end) cb('ok') end) RegisterNUICallback('getTreasuryLog', function(_, cb) ESX.TriggerServerCallback('mercyv-fraks:getTreasuryLog', function(log) SendNUIMessage({ type = 'updateTreasuryLog', log = log }) end) cb('ok') end) RegisterNUICallback('getNearbyPlayers', function(_, cb) ESX.TriggerServerCallback('mercyv-fraks:getNearbyPlayers', function(players) SendNUIMessage({ type = 'updateNearbyPlayers', players = players }) end) cb('ok') end) RegisterNUICallback('inviteMember', function(data, cb) TriggerServerEvent('mercyv-fraks:inviteMember', data.targetSource) cb('ok') end) RegisterNUICallback('kickMember', function(data, cb) TriggerServerEvent('mercyv-fraks:kickMember', data.identifier) cb('ok') end) RegisterNUICallback('changeRank', function(data, cb) TriggerServerEvent('mercyv-fraks:changeRank', data.identifier, data.grade) cb('ok') end) RegisterNUICallback('depositTreasury', function(data, cb) TriggerServerEvent('mercyv-fraks:depositTreasury', data.amount, data.accountType) cb('ok') end) RegisterNUICallback('withdrawTreasury', function(data, cb) TriggerServerEvent('mercyv-fraks:withdrawTreasury', data.amount, data.accountType) cb('ok') end) RegisterNUICallback('garageSpawn', function(data, cb) TriggerServerEvent('mercyv-fraks:garageSpawn', data.model) cb('ok') end) RegisterNUICallback('garageStore', function(_, cb) TriggerServerEvent('mercyv-fraks:garageStore') cb('ok') end) RegisterNUICallback('closeGarage', function(_, cb) CloseGarage() cb('ok') end) RegisterNUICallback('updatePermission', function(data, cb) TriggerServerEvent('mercyv-fraks:updatePermission', data.permKey, data.minGrade) cb('ok') end) -- ===================== -- SERVER EVENT HANDLERS -- ===================== RegisterNetEvent('mercyv-fraks:notify') AddEventHandler('mercyv-fraks:notify', function(message, nType) TriggerEvent(Config.Notification.event, Config.Notification.title, message, nType or 'info', Config.Notification.duration) end) RegisterNetEvent('mercyv-fraks:updateTreasury') AddEventHandler('mercyv-fraks:updateTreasury', function(bal) if isNuiOpen then SendNUIMessage({ type = 'updateTreasury', treasury = bal }) end end) RegisterNetEvent('mercyv-fraks:forceClose') AddEventHandler('mercyv-fraks:forceClose', function() CloseMenu() end) -- ===================== -- INVITE SYSTEM (Client-Side) -- ===================== RegisterNetEvent('mercyv-fraks:receiveInvite') AddEventHandler('mercyv-fraks:receiveInvite', function(inviteData) pendingInvite = inviteData SetNuiFocus(true, true) SendNUIMessage({ type = 'showInvite', factionLabel = inviteData.factionLabel, inviterName = inviteData.inviterName, }) end) RegisterNUICallback('respondInvite', function(data, cb) SetNuiFocus(false, false) if pendingInvite then TriggerServerEvent('mercyv-fraks:respondInvite', data.accepted, pendingInvite.factionId, pendingInvite.inviterSource) pendingInvite = nil end cb('ok') end) -- ===================== -- GARAGE FUNCTIONS -- ===================== function OpenFactionStash(factionId) if isNuiOpen or isGarageOpen then return end -- Rechte-Check ueber Server ESX.TriggerServerCallback('mercyv-fraks:canUseStash', function(allowed) if not allowed then TriggerEvent(Config.Notification.event, Config.Notification.title, 'Kein Zugriff auf das Lager', 'error', Config.Notification.duration) return end local config = Config.Factions[factionId] local stashId = 'faction_stash_' .. factionId local slots = config.stashSlots or 50 local weight = config.stashWeight or 100000 TriggerServerEvent('codem-inventory:server:openstash', stashId, slots, weight, config.label .. ' Lager') end) end function OpenGarageMenu(factionId) if isGarageOpen or isNuiOpen then return end ESX.TriggerServerCallback('mercyv-fraks:getGarageData', function(data) if not data then return end isGarageOpen = true SetNuiFocus(true, true) SendNUIMessage({ type = 'openGarage', vehicles = data.vehicles, theme = data.theme, factionLabel = data.factionLabel, hasSpawned = data.hasSpawned, }) end) end function CloseGarage() if not isGarageOpen then return end isGarageOpen = false SetNuiFocus(false, false) SendNUIMessage({ type = 'closeGarage' }) end -- ===================== -- VEHICLE SPAWNING (Client-Side) -- ===================== RegisterNetEvent('mercyv-fraks:doSpawnVehicle') AddEventHandler('mercyv-fraks:doSpawnVehicle', function(model, coords, color, livery) local hash = GetHashKey(model) RequestModel(hash) local timeout = 0 while not HasModelLoaded(hash) and timeout < 50 do Citizen.Wait(100) timeout = timeout + 1 end if not HasModelLoaded(hash) then TriggerEvent(Config.Notification.event, Config.Notification.title, 'Fahrzeugmodell nicht gefunden', 'error', Config.Notification.duration) return end -- ✅ SPAWN FIX local vehicle = CreateVehicle(hash, coords.x, coords.y, coords.z, coords.w, true, false) SetModelAsNoLongerNeeded(hash) SetVehicleOnGroundProperly(vehicle) SetVehicleModKit(vehicle, 0) Citizen.Wait(200) -- 🎨 Farbe if color and color.primary then SetVehicleColours(vehicle, color.primary, color.secondary or color.primary) end -- 🔥 LIVERY if livery ~= nil and livery >= 0 then SetVehicleLivery(vehicle, livery) -- Standard-Livery SetVehicleMod(vehicle, 48, livery - 1) -- Mod-basierte Livery (die meisten Custom-Fahrzeuge) end -- Netzwerk + Keys local netId = NetworkGetNetworkIdFromEntity(vehicle) local plate = GetVehicleNumberPlateText(vehicle) TriggerServerEvent('mercyv-fraks:vehicleSpawned', netId, plate) -- Spieler reinsetzen TaskWarpPedIntoVehicle(PlayerPedId(), vehicle, -1) -- Garage schließen CloseGarage() end) RegisterNetEvent('mercyv-fraks:despawnVehicle') AddEventHandler('mercyv-fraks:despawnVehicle', function(netId) local vehicle = NetworkGetEntityFromNetworkId(netId) if vehicle and DoesEntityExist(vehicle) then DeleteEntity(vehicle) end end) -- ESX PlayerData sync RegisterNetEvent('esx:setJob') AddEventHandler('esx:setJob', function(job) ESX.PlayerData.job = job playerLoaded = true end)