pc-live/server/main.lua

231 lines
9.3 KiB
Lua
Raw Permalink Normal View History

IC-Computer: Schreibtisch, Fenster und Apps Ein Computer im Spiel. Man tritt an ein Terminal, drueckt E, und bekommt eine Oberflaeche mit Mail, Browser, Adressbuch, Kalender und weiteren Anwendungen. Andere Resources haengen sich als App ein. Auf ESX umgestellt: - Fuenf harte Abhaengigkeiten zeigten auf Resources, die es hier nicht gibt. Eine fehlende Abhaengigkeit verhindert den Start vollstaendig - die Resource waere nie hochgekommen. - Die Bruecke zum Framework neu geschrieben; Charakterdaten, Aktenverwaltung und Immobilien ausgebaut, weil die zugehoerigen Systeme fehlen. - pc_live_devices fehlte im Schema, wird vom Code aber gelesen. Ohne die Tabelle ist kein fester PC ansprechbar. Ein PC braucht ausserdem zwei Eintraege mit derselben Kennung: Standort in der Datenbank, Kamerafahrt in der config.lua. Mail ueberarbeitet: - Postfaecher haengen an einer Anmeldung statt an der Person. Mehrere Leute koennen dasselbe Firmenpostfach gleichzeitig offen haben. - Ordner gehoeren zum Postfach, nicht zur Person. Eine Mail an ein geteiltes Postfach wird je Empfaenger einmal gespeichert; damit das Einsortieren trotzdem fuer alle gilt, tragen alle Kopien einer Zustellung dieselbe Kennung. Gelesen und geloescht bleibt persoenlich - das ist keine Eigenschaft der Nachricht, sondern der Person. - Signaturen gehoeren ebenfalls zum Postfach. - Kalender koennen privat, geteilt oder oeffentlich sein. Ein oeffentlicher Termin haengt bewusst auch an einem Postfach, sonst koennte ihn spaeter niemand mehr aendern oder absagen. Neue Apps: Webhosting, Bleeter und dessen Verwaltung. Behoben: - prompt(), confirm() und alert() froren das NUI ein. FiveMs CEF hat keinen Handler fuer die eingebauten Browserdialoge - der Aufruf oeffnet nichts und kehrt nie zurueck. Ersetzt durch eigene Dialoge. - Die Fenster tragen data-wid, keine id. Wer sie mit getElementById sucht, schreibt ins Leere und das Fenster bleibt leer. Enthaelt README.md mit Einrichtung und PLUGINS.md: eine Anleitung, wie man eine eigene App baut und einhaengt, mit vollstaendigem Beispiel von der Tabelle bis zum Schreibtischsymbol.
2026-08-09 12:02:11 +00:00
-- ─────────────────────────────────────────────────────────────────────────────
-- 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.")