-- ───────────────────────────────────────────────────────────────────────────── -- Server Main – session management, boot, init -- ───────────────────────────────────────────────────────────────────────────── -- Bridge namespace (must be declared before bridge file is loaded; -- bridge file is loaded by fxmanifest before this file) Bridge = Bridge or {} -- Active sessions: source -> { identifier, displayName, dbId, pcId } ActiveSessions = {} -- ── PC Cache (loaded from DB) ───────────────────────────────────────────────── local PCCache = {} -- keyed by id (aus DB-Tabelle pc_live_devices) -- Virtuelle/mobile Terminals aus der Config (kein DB-Standort), z.B. Streifenwagen-PC. local MobilePCs = {} for _, pc in ipairs(Config.PCs or {}) do if pc.mobile then MobilePCs[pc.id] = pc end end local function LoadPCsFromDB() local rows = MySQL.query.await('SELECT * FROM pc_live_devices WHERE active = 1') PCCache = {} for _, row in ipairs(rows or {}) do PCCache[row.id] = { id = row.id, label = row.label, coords = vector3(row.x, row.y, row.z), heading = row.heading, type = row.type, job = row.job, start_page = row.start_page, } end Utils.Log(("Loaded %d PC devices from DB"):format(#(rows or {}))) end CreateThread(function() Wait(500) LoadPCsFromDB() end) -- Reload nach Änderungen (z.B. durch Backend oder F6) AddEventHandler('pc-live:reloadDevices', function() LoadPCsFromDB() local list = {} for _, pc in pairs(PCCache) do list[#list + 1] = { id=pc.id, label=pc.label, heading=pc.heading, type=pc.type, job=pc.job, start_page=pc.start_page, coords={ x=pc.coords.x, y=pc.coords.y, z=pc.coords.z } } end TriggerClientEvent('pc-live:client:syncDevices', -1, list) end) -- Client fragt nach Device-Liste RegisterNetEvent('pc-live:server:requestDevices', function() local src = source local list = {} for _, pc in pairs(PCCache) do list[#list + 1] = { id=pc.id, label=pc.label, heading=pc.heading, type=pc.type, job=pc.job, start_page=pc.start_page, coords={ x=pc.coords.x, y=pc.coords.y, z=pc.coords.z } } end TriggerClientEvent('pc-live:client:syncDevices', src, list) end) local function GetPCConfig(pcId) return PCCache[pcId] or MobilePCs[pcId] end local function IsPlayerNearPC(source, pcConfig) -- We trust client position check; server re-validates via ped coords local ped = GetPlayerPed(source) if not ped or ped == 0 then return false end local px, py, pz = table.unpack(GetEntityCoords(ped)) local cx, cy, cz = table.unpack({ pcConfig.coords.x, pcConfig.coords.y, pcConfig.coords.z }) local dist = #(vector3(px, py, pz) - vector3(cx, cy, cz)) return dist <= (Config.InteractRange + 2.0) -- small server-side tolerance end -- ── Open PC session ────────────────────────────────────────────────────────── RegisterNetEvent('pc-live:server:openPC', function(pcId) local src = source if ActiveSessions[src] then return end -- already in session if type(pcId) ~= "string" then return end pcId = pcId:sub(1, 64) local pcConfig = GetPCConfig(pcId) if not pcConfig then Utils.Warn(("Player %d tried unknown PC id: %s"):format(src, pcId)) return end -- Distance check (mobile Terminals wie der Streifenwagen-PC haben keinen Standort) if not pcConfig.mobile and not IsPlayerNearPC(src, pcConfig) then Utils.Warn(("Player %d too far from PC %s"):format(src, pcId)) return end -- Job lock check – pcConfig.job kann eine Liste aus Behörden-Jobs und -- Firmen-Tokens ("company:") sein, z.B. "dpa, police, company:3". if pcConfig.type == "job" and pcConfig.job and pcConfig.job ~= "" then local job = Bridge.GetJob(src) local allowed = false for token in tostring(pcConfig.job):gmatch("[^,%s]+") do local companyId = token:match("^company:(%d+)$") if companyId then if Bridge.IsCompanyEmployee and Bridge.IsCompanyEmployee(src, tonumber(companyId)) then allowed = true; break end elseif job == token then allowed = true; break end end if not allowed then Bridge.Notify(src, "You don't have access to this terminal.", "error") return end end local identifier = Bridge.GetIdentifier(src) local displayName = Bridge.GetName(src) local dbUser = DB.GetOrCreateUser(identifier, displayName) if not dbUser then Utils.Error(("Failed to get/create user for %s"):format(identifier)) return end -- Install default apps if first time local installs = DB.GetInstalls(dbUser.id) local installedIds = {} for _, row in ipairs(installs) do installedIds[row.app_id] = true end for _, app in ipairs(AppRegistry.GetAll()) do if app.default and not installedIds[app.app_id] then DB.InstallApp(dbUser.id, app.app_id, app.version) end end -- Apps eines (mobilen) Terminals sicher installieren, damit sie verfügbar sind. local ensureApps = pcConfig.apps or (pcConfig.auto_open and { pcConfig.auto_open }) or {} if #ensureApps > 0 then local byId = {} for _, app in ipairs(AppRegistry.GetAll()) do byId[app.app_id] = app end for _, appId in ipairs(ensureApps) do if not installedIds[appId] and byId[appId] then DB.InstallApp(dbUser.id, appId, byId[appId].version) end end end -- Reload installs after defaults installs = DB.GetInstalls(dbUser.id) ActiveSessions[src] = { identifier = identifier, displayName = displayName, dbId = dbUser.id, pcId = pcId, } -- Adresse wird über den Tablet-Assistenten erstellt, nicht automatisch -- Postfächer für diesen Spieler laden (ic-mail optional) local sharedMailboxes = {} local personalAddress = '' if GetResourceState and GetResourceState('ic-mail') == 'started' then personalAddress = exports['ic-mail']:GetPrimaryAddress(identifier) or '' local addrs = exports['ic-mail']:GetAddressesForIdentifier(identifier) or {} table.sort(addrs, function(a, b) if a.type == 'personal' and b.type ~= 'personal' then return true end if a.type ~= 'personal' and b.type == 'personal' then return false end return (a.address or '') < (b.address or '') end) sharedMailboxes = addrs end local installed = AppRegistry.BuildInstalledList(installs) -- Mobiles Terminal: nur die freigegebenen Apps anzeigen (Whitelist aus der PC-Config). if pcConfig.apps then local allow = {} for _, a in ipairs(pcConfig.apps) do allow[a] = true end local filtered = {} for _, app in ipairs(installed) do if allow[app.app_id] then filtered[#filtered + 1] = app end end installed = filtered end local unread = DB.CountUnread(identifier) local settings = json.decode(dbUser.settings_json or "{}") or {} TriggerClientEvent('pc-live:client:sessionStart', src, { pcId = pcId, pcLabel = pcConfig.label, startPage = pcConfig.start_page, autoOpenApp = pcConfig.auto_open, identifier = identifier, displayName = displayName, installedApps = installed, unread = unread, settings = settings, sharedMailboxes = sharedMailboxes, personalAddress = personalAddress, -- Signatur des persoenlichen Postfachs. Steckt zwar auch in -- sharedMailboxes, aber der Client kennt es dort nur ueber die Adresse. personalSignature = (function() for _, mb in ipairs(sharedMailboxes) do if mb.address == personalAddress then return mb.signature or '' end end return '' end)(), }) end) -- ── Close PC session ────────────────────────────────────────────────────────── RegisterNetEvent('pc-live:server:closePC', function() local src = source ActiveSessions[src] = nil Utils.Log(("Session closed for player %d"):format(src)) end) -- ── Player disconnect cleanup ───────────────────────────────────────────────── AddEventHandler('playerDropped', function() ActiveSessions[source] = nil end) Utils.Log("Server main loaded.")