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.
11 KiB
Eigene Apps für pc-live
Eine App ist kein eigener Ordner und kein Plugin-Format — sie besteht aus vier Teilen, die an vier Stellen eingetragen werden. Diese Anleitung baut eine vollständige App von Null auf: Notizen, mit Datenbank, Server, Oberfläche und Schreibtischsymbol.
Am Ende steht ein Gerüst zum Kopieren.
Wie eine App aufgebaut ist
NUI (Browser) Client (Lua) Server (Lua)
┌──────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ nui/js/apps/ │ │ client/notizen.lua │ │ server/notizen.lua │
│ notizen.js │ │ │ │ │
│ │ │ │ │ │
│ fetchNui('op') ──┼─────►│ RegisterNUICallback├───►│ RegisterNetEvent │
│ │ │ │ │ │ │
│ window.message ◄─┼──────┤ SendNUIMessage │◄───┤ TriggerClientEvent │
└──────────────────┘ └────────────────────┘ └────────────────────┘
Das NUI kann nur Callbacks der eigenen Resource aufrufen — deshalb der
Umweg über den Client. Antworten laufen umgekehrt über SendNUIMessage und
landen im Nachrichtenverteiler in nui/js/desktop.js.
Schritt 1 — Tabelle
sql/notizen.sql:
CREATE TABLE IF NOT EXISTS `pc_live_notizen` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`identifier` VARCHAR(120) NOT NULL,
`titel` VARCHAR(120) NOT NULL DEFAULT '',
`inhalt` TEXT NOT NULL,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_identifier` (`identifier`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Schritt 2 — Server
server/notizen.lua:
-- Notizen. Jede Notiz gehoert genau einer Person.
RegisterNetEvent('pc-live:server:getNotizen', function()
local src = source
local user = ActiveSessions[src] -- wer sitzt am PC?
if not user then return end
local rows = MySQL.query.await(
'SELECT id, titel, inhalt FROM pc_live_notizen WHERE identifier = ? ORDER BY id DESC',
{ user.identifier }) or {}
TriggerClientEvent('pc-live:client:notizen', src, rows)
end)
RegisterNetEvent('pc-live:server:saveNotiz', function(data)
local src = source
local user = ActiveSessions[src]
if not user then return end
if type(data) ~= 'table' then return end
-- Immer beschneiden: was aus dem NUI kommt, hat der Spieler in der Hand.
local titel = Utils.Sanitize(tostring(data.titel or '')):sub(1, 120)
local inhalt = Utils.Sanitize(tostring(data.inhalt or '')):sub(1, 5000)
if titel == '' then return end
local id = tonumber(data.id)
if id then
-- Die Bedingung auf identifier ist die Besitzpruefung: ohne sie
-- koennte jemand mit einer fremden Id die Notiz eines anderen aendern.
MySQL.update.await(
'UPDATE pc_live_notizen SET titel = ?, inhalt = ? WHERE id = ? AND identifier = ?',
{ titel, inhalt, id, user.identifier })
else
MySQL.insert.await(
'INSERT INTO pc_live_notizen (identifier, titel, inhalt) VALUES (?,?,?)',
{ user.identifier, titel, inhalt })
end
TriggerEvent('pc-live:server:getNotizen') -- geht nicht! siehe unten
end)
Falle:
TriggerEventauf ein Netzwerkereignis funktioniert nicht wie gedacht —sourceist dort nicht der Spieler, der Handler bricht ab. Baue stattdessen eine Funktion, die beide Wege benutzen:local function sendeNotizen(src, identifier) … end
Schritt 3 — Client als Brücke
client/notizen.lua:
-- Die Liste erlaubter Aktionen steht fest. Ohne sie koennte das NUI jedes
-- beliebige Serverereignis ausloesen.
local OPS = {
getNotizen = 'pc-live:server:getNotizen',
saveNotiz = 'pc-live:server:saveNotiz',
}
RegisterNUICallback('notizen', function(data, cb)
cb({}) -- sofort bestaetigen
local event = OPS[tostring(data.op or '')]
if not event then return end
TriggerServerEvent(event, data.payload or {})
end)
RegisterNetEvent('pc-live:client:notizen', function(rows)
SendNUIMessage({ action = 'notizen_data', data = rows or {} })
end)
Schritt 4 — Oberfläche
nui/js/apps/notizen.js:
const NotizenApp = (() => {
const WIN_ID = 'app-notizen';
const el = ICWebRender.el; // kleiner Helfer aus webhosting.js
let notizen = [];
let winEl = null;
function open() {
const win = WindowManager.create({
id: WIN_ID, title: 'Notizen', icon: '📝', width: 720, height: 520,
content: '<div class="app-content" id="notizen-main"></div>',
});
if (!win) return;
winEl = win; // merken! siehe Falle unten
fetchNui('notizen', { op: 'getNotizen' });
render();
}
function root(id) {
if (!winEl || !winEl.isConnected) {
winEl = document.querySelector('[data-wid="' + WIN_ID + '"]');
}
return winEl ? winEl.querySelector('#' + id) : null;
}
function render() {
const main = root('notizen-main');
if (!main) return;
main.replaceChildren();
notizen.forEach(n => {
const karte = el('div', 'wh-row');
// Spielerinhalte immer ueber textContent – nie ueber innerHTML.
karte.appendChild(el('div', 'wh-row-title', n.titel));
karte.appendChild(el('div', 'wh-row-sub', n.inhalt));
main.appendChild(karte);
});
const neu = el('button', 'icweb-btn-primary', 'Neue Notiz');
neu.addEventListener('click', () => {
// Kein prompt() – siehe Fallen.
ICWebRender.promptText('Neue Notiz', { placeholder: 'Titel' }, (titel) => {
fetchNui('notizen', { op: 'saveNotiz', payload: { titel, inhalt: '' } });
});
});
main.appendChild(neu);
}
/** Vom Nachrichtenverteiler in desktop.js. */
function onData(rows) {
notizen = rows || [];
render();
}
return { open, onData };
})();
Schritt 5 — Einhängen
fxmanifest.lua
client_scripts { …, 'client/notizen.lua' }
server_scripts { …, 'server/notizen.lua' }
files { …, 'nui/js/apps/notizen.js' }
nui/index.html — vor desktop.js, und nach webhosting.js, falls du
ICWebRender benutzt:
<script src="js/apps/notizen.js"></script>
nui/js/desktop.js — Symbol und Antwortkanal:
const APP_META = {
…
'pc.notizen': { name: 'Notizen', icon: '📝', open: () => NotizenApp.open() },
};
// im Nachrichtenverteiler:
case 'notizen_data': NotizenApp.onData(data); break;
server/apps.lua — damit die App auf dem Schreibtisch landet:
AppRegistry.Register({
app_id = "pc.notizen",
name = "Notizen",
icon = "📝",
version = "1.0.0",
category = "utility",
permissions = {},
dependencies = {},
default = true, -- true = bei jedem vorinstalliert
description = "Notizen schreiben und wiederfinden.",
price = 0, -- > 0: muss im Store gekauft werden
})
Fertig. Neu starten, PC öffnen — das Symbol ist da.
Aus einer anderen Resource
Der Serverteil kann auch woanders liegen. Die Registrierung geht dann über einen Export:
exports['pc-live']:RegisterApp({ app_id = 'meine.app', name = 'Meine App', … })
Die Oberfläche muss trotzdem in pc-live/nui/ liegen: Das NUI lädt nur
Dateien der eigenen Resource. Deine Resource liefert also die Serverlogik, und
in pc-live liegen Brücke und Oberfläche. So machen es ic-web und bleeter.
Der Client deiner Resource kann direkt mit deinem Server sprechen — pc-live
muss nichts weiterreichen. Nur der Weg ins NUI führt über pc-live, weil
SendNUIMessage immer im eigenen Fenster landet.
Fallen, die Zeit kosten
Das Fenster hat keine id. WindowManager.create setzt data-wid.
document.getElementById(WIN_ID) findet nichts, deine App schreibt ins Leere
und das Fenster bleibt leer. Merke dir das zurückgegebene Element oder suche
mit document.querySelector('[data-wid="…"]').
prompt(), confirm() und alert() frieren das NUI ein. FiveMs CEF hat
keinen Handler für die eingebauten Browserdialoge — der Aufruf öffnet nichts
und kehrt nie zurück. Nimm ICWebRender.promptText(titel, optionen, rückruf)
und ICWebRender.confirmBox(titel, text, rückruf, optionen).
Kein innerHTML für Spielerinhalte. Das NUI ist derselbe Kontext wie der
PC: Wer dort Markup einschleusen kann, kann auch dessen Callbacks aufrufen.
textContent und setAttribute, immer.
TINYINT(1) ist mal 1, mal true. oxmysql wandelt es bei
query/single in einen Boolean, bei prepare nicht. == 1 geht deshalb
irgendwann schief — und zwar still. Schreib dir einen Helfer wie ICWeb.Bool.
Keine Datenbankarbeit in onResourceStop. Ein await wartet dort auf eine
Antwort, die niemand mehr entgegennimmt: „Execution of function reference in
script host failed". Aufräumen gehört in den Start — das erwischt auch den
Absturz, den ein Stop-Handler nie sieht.
TriggerEvent auf ein Netzwerkereignis hat kein source. Zieh die Logik
in eine Funktion und ruf die von beiden Seiten.
Eigene Stile kapseln. Bindest du eine fremde CSS-Datei ein, färbt sie
womöglich den ganzen PC. Setz deine Regeln unter eine eigene Wurzelklasse —
siehe nui/css/erzeuge-bleeter-css.py, das genau das mechanisch erledigt.
Was schon da ist
Bevor du etwas nachbaust:
| aus | ||
|---|---|---|
ICWebRender.el(tag, klasse, text) |
webhosting.js |
Element bauen |
ICWebRender.modal(titel, aufbau, breit) |
webhosting.js |
Dialograhmen |
ICWebRender.promptText / confirmBox |
webhosting.js |
Eingabe und Rückfrage |
Desktop.showNotification(text) |
desktop.js |
kurze Meldung |
WindowManager.create/close/focus |
windowManager.js |
Fenster |
fetchNui(name, daten) |
desktop.js |
Aufruf an den Client |
Für das Aussehen kannst du die vorhandenen Klassen benutzen: wh-page,
wh-row, wh-field, wh-label, icweb-input, icweb-btn-primary,
icweb-btn-ghost, icweb-btn-danger, wh-tag, wh-hint. Dann fügt sich
deine App ohne eigenes CSS ein.