231 lines
9.3 KiB
Lua
231 lines
9.3 KiB
Lua
|
|
-- ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
-- 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:<id>") 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.")
|