pc-live/PLUGINS.md

322 lines
11 KiB
Markdown
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
# 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`:
```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`:
```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:** `TriggerEvent` auf ein *Netzwerk*ereignis funktioniert nicht wie
> gedacht — `source` ist dort nicht der Spieler, der Handler bricht ab. Baue
> stattdessen eine Funktion, die beide Wege benutzen:
>
> ```lua
> local function sendeNotizen(src, identifier) … end
> ```
---
## Schritt 3 — Client als Brücke
`client/notizen.lua`:
```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`:
```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`**
```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:
```html
<script src="js/apps/notizen.js"></script>
```
**`nui/js/desktop.js`** — Symbol und Antwortkanal:
```js
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:
```lua
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:
```lua
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.