Ein Image mit Webseite, Backend, SQLite und Docker-Client; startet Match-Container über /var/run/docker.sock auf demselben Host. Tests laufen beim Bauen mit. Auf amp-cluster gebaut (301 MB) und geprüft. Dazu: Testbefehl für Node 22 angepasst, beim Beenden werden ausstehende Live-Updates verworfen und laufende Server-Aktionen abgewartet, damit nach dem Schließen der Datenbank nichts mehr darauf zugreift. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CS2 Mapveto
Webanwendung zur Organisation von Counter-Strike-2-Matches: Match-Lobbys mit Team-Einladungen, abwechselndes Map-Veto der Captains, Zuschauerzugang und automatische Verwaltung eines CS2-Servers pro Match.
- Frontend: React 18 + Vite, deutschsprachig, responsives dunkles Design
- Backend: Node.js (≥ 18.18), Express, Socket.IO
- Datenbank: SQLite (better-sqlite3), Datei unter
data/ - Echtzeit: Socket.IO; jede Änderung schickt allen Teilnehmern ihren rollenabhängigen Match-Zustand
- Serververwaltung: austauschbare Adapter hinter einer Schnittstelle (
start,status,configure,stop)
Standardmäßig läuft der Demo-Adapter. Er startet keine echten CS2-Server. Oberfläche, Statusanzeigen und Verbindungsdaten sind dann überall als „Demo“ bzw. „simuliert“ gekennzeichnet. Wie die echte Anbindung eingerichtet wird, steht im Abschnitt Echte Server anbinden.
Schnellstart
Als Container (empfohlen für den Betrieb): ein Container mit Webseite, Backend und Datenbank, der die CS2-Server-Container auf demselben Docker-Host startet. Siehe docs/container.md:
docker -H ssh://cs2docker build -t cs2-mapveto:latest .
…dann den Stack aus der Anleitung in Portainer anlegen.
Zum Entwickeln:
npm install
cp .env.example .env # optional, Standardwerte funktionieren für die Demo
npm run dev # Backend auf :3000, Frontend auf :5173
Dann http://localhost:5173 öffnen.
Produktion (ein Prozess, Backend liefert das gebaute Frontend aus):
npm install
npm run build
PUBLIC_URL=https://veto.example.de npm start # lauscht auf PORT (Standard 3000)
Tests:
npm test
Die Tests decken Veto-Engine und Ablaufvalidierung, RCON-Protokoll (gegen einen lokalen Test-RCON-Server), Befehls-Bereinigung und den kompletten API-Ablauf ab. Dazu gehören Berechtigungen, doppelte Aktionen, Server-Konfiguration mit Wiederholung, nächste Map, automatisches Herunterfahren und dass keine RCON-Daten an Clients gelangen.
Ablauf: von der Match-Erstellung bis zum Serverbeitritt
-
Match erstellen (
/neu): Der Organisator trägt beide Teamnamen ein und wählt Spielmodus (Wingman 2 gegen 2 oder Competitive 5 gegen 5), Serie (Best of 1/3/5) und wer das Veto beginnt (Münzwurf oder festes Team). Formate, für die der Map-Pool keinen gültigen Veto-Ablauf hat, sind ausgegraut und werden auch vom Backend abgelehnt. Der Browser des Organisators erhält einen geheimen Organisator-Token. Er wird lokal gespeichert und lässt sich über „Organisator-Zugang auf einem anderen Gerät öffnen“ übertragen. -
Server vorbereiten: Direkt beim Anlegen der Lobby fordert das Backend im Hintergrund einen Server an. Er fährt also schon während Lobby und Veto hoch. Der Status ist auf der Match-Seite live sichtbar.
-
Einladungen: Das Match hat drei Zugangscodes (Format
ABCD-EFGH) mit Links:- Team A:
/join/<code> - Team B:
/join/<code> - Zuschauer:
/watch/<code>für Stream und Kommentatoren
Diese Codes gelten nur für die Webseite. Der CS2-Serverzugang wird getrennt bereitgestellt (Schritt 6). Der Organisator kann jeden Code jederzeit neu erzeugen; der alte Link wird damit ungültig.
- Team A:
-
Spieler treten bei: Über den Teamlink gibt ein Spieler seinen Namen ein (optional die SteamID64) und landet in genau diesem Team. Pro Team sind maximal 2 bzw. 5 Spieler erlaubt.
- Der erste Spieler eines Teams wird Captain. Organisator oder Captain können den Captain wechseln.
- Jeder Spieler setzt sich auf „bereit“. Ein Team ist bereit, sobald mindestens ein Spieler beigetreten ist, es einen Captain hat und alle beigetretenen Spieler bereit sind. Mit
REQUIRE_FULL_TEAMS=truemuss das Team zusätzlich vollständig sein (2 bzw. 5 Spieler).
-
Map-Veto: Sind beide Teams bereit, startet der Organisator das Veto. Die Captains bannen und wählen abwechselnd nach dem festen Ablauf (siehe unten). Die Seite zeigt jederzeit:
- welches Team am Zug ist
- welche Aktion ansteht
- den Zustand jeder Map (verfügbar, gebannt, gewählt, Entscheidungsmap)
- den gesamten Verlauf mit Uhrzeit
Die letzte verbleibende Map wird automatisch als Entscheidungsmap übernommen. Zuschauer und Organisator sehen alles live, können aber nicht wählen.
-
Server konfigurieren: Nach dem Veto wird der bereits laufende Server automatisch eingerichtet: Spielmodus, erste Map, Teamnamen und Startseiten, Match-Einstellungen, Server- und CSTV-Passwort. Die restliche Map-Reihenfolge ist gespeichert.
- Spieler sehen danach den kopierbaren Konsolenbefehl
password …; connect host:portund einensteam://connect-Link. - Zuschauer sehen den CSTV-Zugang (eigener Port, eigenes Passwort, Verzögerung).
- Spieler sehen danach den kopierbaren Konsolenbefehl
-
Während der Serie: Der Organisator kann die Aufwärmphase beenden (
mp_warmup_end), die nächste Map laden, den Server stoppen und das Match abschließen oder abbrechen. -
Fehlerfälle: Startet der Server nicht, schlägt die Konfiguration fehl oder fällt der Server aus, zeigt die Seite den Grund. Der Organisator bekommt einen Knopf für den erneuten Versuch: „Server neu starten“ bzw. „Konfiguration wiederholen“.
Rollen und Berechtigungen
| Rolle | Zugang über | Darf |
|---|---|---|
| Organisator | Token beim Erstellen | Veto starten, Spieler entfernen, Captain setzen, Codes erneuern, Server neu starten/stoppen/konfigurieren, nächste Map, Match beenden; sieht Einladungen, Spieler- und CSTV-Verbindung |
| Captain | Teamcode (erster Spieler oder ernannt) | alles wie Spieler, dazu Veto-Aktionen seines Teams und Captain-Übergabe |
| Spieler | Teamcode | bereit melden, Team verlassen; sieht die Spieler-Verbindungsdaten nach dem Veto |
| Zuschauer | Zuschauercode | alles live verfolgen; sieht nur den CSTV-Zugang, nicht das Spielerpasswort |
| Gast | ohne Code | nur Teamnamen und Status |
Organisator spielt selbst mit: In der Lobby kann der Organisator unter „Selbst mitspielen?“ direkt einem Team beitreten, oder er öffnet einen Teamlink im selben Browser. Er bleibt Organisator und ist zusätzlich Spieler bzw. Captain. Ein zweiter Browser ist nicht nötig. Technisch speichert der Browser dafür beide Zugänge und schickt den Spieler-Token im Header X-Player-Token mit. Pro Aufrufer zählt höchstens ein Spieler.
Alle Prüfungen passieren im Backend. Die Oberfläche blendet nur zusätzlich aus, was die Rolle nicht darf.
Schutzmaßnahmen
- Fremde Teambeitritte: Jeder Code gehört zu genau einem Team eines Matches. Zuschauercodes erlauben keinen Beitritt. Volle Teams, doppelte Namen und doppelte Steam-IDs werden abgelehnt. Codebasierte Endpunkte sind pro IP begrenzt (20 Versuche pro Minute).
- Doppelte Aktionen: Jede Veto-Aktion trägt die Nummer des Schritts. Sie wird in einer SQLite-Transaktion geprüft und gespeichert;
(match_id, idx)ist Primärschlüssel. Ein doppelter Klick oder ein veralteter Stand liefert „Diese Aktion ist veraltet“. Serveraktionen sind pro Match gesperrt, solange eine läuft. - Unberechtigte Befehle: Veto nur durch den Captain des Teams am Zug, Server- und Match-Befehle nur durch den Organisator.
- Tokens: Organisator- und Spieler-Tokens sind 256 Bit zufällig und werden nur als SHA-256-Hash gespeichert.
- Server-Zugangsdaten: RCON-Passwort, Container-Handle und GSLT liegen ausschließlich in der Datenbank bzw. den Umgebungsvariablen des Backends. Die Snapshot-Funktion gibt sie nie aus (per Test abgesichert). Server- und CSTV-Passwort sind als Beitrittsdaten nötig und werden nur an die jeweils berechtigte Rolle geliefert.
- Befehls-Injection: Teamnamen werden vor der Übergabe an die Serverkonsole von Anführungszeichen,
;und Steuerzeichen bereinigt. Passwörter sind rein alphanumerisch.
Wiederherstellung nach Neuladen oder Verbindungsabbruch
Der gesamte Zustand liegt in SQLite. Der Browser speichert pro Match nur seinen Zugangstoken (localStorage).
- Neuladen der Seite: Die Seite lädt den Stand per REST und abonniert danach den Socket.
- Abgebrochene Socket-Verbindung: Nach dem Reconnect meldet sich der Client automatisch neu an und erhält den vollständigen aktuellen Stand. Socket-Nachrichten enthalten immer den kompletten Zustand und keine Deltas, deshalb geht nichts verloren.
- Backend-Neustart: Unterbrochene Serverstarts werden als fehlgeschlagen markiert (mit „Server neu starten“). Unterbrochene Konfigurationen werden automatisch wiederholt.
Automatisches Herunterfahren
Alle SERVER_REAPER_INTERVAL_SECONDS prüft das Backend alle aktiven Server:
- Ein Match ist abgeschlossen oder abgebrochen: Server wird gestoppt.
- Lobby, Veto oder Server ohne Spielerzahl: Ist niemand mit der Match-Seite verbunden und gab es länger als
SERVER_IDLE_TIMEOUT_MINUTESkeine Aktivität, wird der Server gestoppt. - Konfigurierter echter Server: Gestoppt wird, wenn länger als die Frist keine Spieler auf dem Server waren. Die Spielerzahl kommt per RCON aus
status. - Unabhängig davon: Nach
SERVER_MAX_LIFETIME_HOURSwird jeder Server gestoppt.
Ein gestoppter Server lässt sich vom Organisator neu starten.
Map-Pools und Veto-Abläufe
Konfiguration in server/config/map-pools.json (Pfad änderbar über MAP_POOL_FILE). Jeder Pool hat:
- eine eigene Map-Liste mit ID, Anzeigename und Kartenfarbe
- Teamgröße und
game_type/game_mode - je Format einen Ablauf
Schritte im Ablauf:
| Schritt | Bedeutung |
|---|---|
ban:X / ban:Y |
Team X bzw. Y bannt eine Map |
pick:X / pick:Y |
Team X bzw. Y wählt eine Map für die Serie |
side:X / side:Y |
Team wählt die Startseite (CT/T) auf der zuletzt gewählten Map bzw. der Entscheidungsmap |
decider |
die letzte verbleibende Map wird automatisch übernommen |
X ist das Team, das das Veto beginnt (Münzwurf oder vom Organisator festgelegt), Y das andere.
Beim Start wird jeder Ablauf geprüft:
- Anzahl Picks = Maps der Serie − 1
- Bans + Picks + 1 = Pool-Größe
- genau eine Entscheidungsmap
- Seitenwahl nur direkt nach einem Pick oder der Entscheidungsmap
Ungültige oder fehlende Abläufe machen nur dieses Format unauswählbar (mit Begründung in der Oberfläche).
Mitgelieferte Abläufe
Competitive (7 Maps): Ancient, Anubis, Dust II, Inferno, Mirage, Nuke, Train
| Format | Ablauf |
|---|---|
| BO1 | X bannt, Y bannt, X bannt, Y bannt, X bannt, Y bannt → Entscheidungsmap, X wählt Seite |
| BO3 | X bannt, Y bannt, X pickt (Y wählt Seite), Y pickt (X wählt Seite), X bannt, Y bannt → Entscheidungsmap, X wählt Seite |
| BO5 | X bannt, Y bannt, X pickt (Y Seite), Y pickt (X Seite), X pickt (Y Seite), Y pickt (X Seite) → Entscheidungsmap, Y wählt Seite |
Wingman (4 Maps): Inferno, Nuke, Overpass, Vertigo
| Format | Ablauf |
|---|---|
| BO1 | X bannt, Y bannt, X bannt → Entscheidungsmap, Y wählt Seite |
| BO3 | X bannt, Y pickt (X Seite), X pickt (Y Seite) → Entscheidungsmap, X wählt Seite |
| BO5 | nicht unterstützt: Mit 4 Maps sind keine 5 Karten möglich. Mit einem Pool von mindestens 5 Maps und einem bo5-Ablauf wird es automatisch auswählbar. |
Regel für Startseiten
- Auf einer gewählten Map wählt immer das Gegnerteam des wählenden Teams die Startseite.
- Auf der Entscheidungsmap wählt das Team, das im Ablauf dafür vorgesehen ist. In BO1 ist das in Competitive Team X, weil Y den letzten Ban hatte, und in Wingman Team Y. Ohne
side-Schritt werden die Seiten im Spiel festgelegt.
Die Wahl wird als mp_teamname_1 (Team mit CT-Start) und mp_teamname_2 (T-Start) an den Server übergeben.
Bilder für Maps und Seitenwahl
- Map-Bilder liegen lokal unter
client/public/maps/<map>.webp(960 px breit, je ca. 50–90 KB)."image"inmap-pools.jsonverweist darauf. Neue oder fehlende Bilder lädtnpm run imagesvon ghostcap-gaming/cs2-map-images, verkleinert sie und trägt den Pfad ein (npm run images -- --forcelädt alle neu). Die Bilder stammen aus dem Spiel; die Rechte liegen bei Valve. Ohne Bild zeigt die Kachel ein Muster in der Map-Farbe. - Seiten-Embleme für CT und T liegen unter
client/public/sides/ct.pngundclient/public/sides/t.png(quadratisch, z. B. 256×256). Fehlt eine Datei, erscheint ein schlichtes Ersatzsymbol. - Logo und Farben: Das MaxLAN-e.V.-Logo liegt freigestellt unter
client/public/brand/maxlan-logo.webp. Die Farben stehen als Variablen am Anfang vonclient/src/styles.css: Logo-Blau#344A99für Team A und Hauptbuttons, Logo-Rot#A72A1Cfür Team B und den Abschluss der Kopfzeile, jeweils aufgehellt für die Lesbarkeit. - Bestehende Matches behalten die Map-Liste, mit der sie erstellt wurden. Neue Bilder erscheinen in neu erstellten Matches.
Die mitgelieferten Map-Pools entsprechen dem Stand der Entwicklung. Valve ändert den aktiven Map-Pool regelmäßig. Map-IDs bitte gegen die eigene Serverinstallation prüfen. Eine Map, die nicht existiert, führt zu „Konfiguration fehlgeschlagen“ mit Meldung.
Echte Server anbinden
Schritt-für-Schritt mit Portainer: docs/portainer-cs2.md beschreibt, wie du einen CS2-Server als Portainer-Stack aufsetzt, dessen Spieldateien nur einmal heruntergeladen werden, und ihn mit CS2 Mapveto verbindest.
Die Serververwaltung ist hinter server/src/servers/adapter.js gekapselt. Auswahl über SERVER_ADAPTER.
Ablauf, den jeder Adapter bedient
-
start({ matchId, mode, secrets }): Server starten bzw. reservieren.- Rückgabe:
{ handle, host, port, tvPort }. secrets(RCON-, Server- und CSTV-Passwort) erzeugt das Backend pro Match.
- Rückgabe:
-
status(handle): liefertstarting | running | stopped | failed, optional mit Spielerzahl.- Wird alle
SERVER_STATUS_POLL_SECONDSabgefragt, solange der Server startet, danach alle 30 s.
- Wird alle
-
configure(handle, plan): in zwei Phasen.game_type,game_mode,sv_password,tv_enable,tv_delay,tv_password, dannchangelevel <map>.- Warten, bis
statusdie Map meldet (max. 2 Minuten), dann die Match-Einstellungen (mp_teamname_1/2,mp_maxrounds,mp_overtime_enable,mp_match_can_clinch,mp_warmup_pausetimer 1,CS2_EXTRA_COMMANDS).
Die zweite Phase kommt erst nach dem Mapwechsel, weil die
gamemode_*.cfgdes Spiels diese Werte beim Laden der Map sonst überschreibt. -
stop(handle): Server beenden bzw. freigeben.
Option A: Docker (SERVER_ADAPTER=docker)
Startet pro Match einen Container mit dem Community-Image joedwards32/cs2 (SteamCMD, CS2-Dedicated-Server App 730; gepflegter Nachfolger von cm2network/cs2).
Voraussetzungen:
- Docker auf dem Backend-Host. Das Backend ruft die Docker-CLI auf und braucht dafür Zugriff auf den Docker-Daemon.
- Game Server Login Token für App-ID 730 unter https://steamcommunity.com/dev/managegameservers erstellen und als
CS2_GSLTeintragen. Ohne GSLT sind Server nicht öffentlich erreichbar. Pro gleichzeitig laufendem Server ist ein eigenes Token empfehlenswert. CS2_PUBLIC_HOST: IP oder DNS-Name, unter dem Spieler den Host erreichen.- Ports
CS2_PORT_RANGE_START–CS2_PORT_RANGE_ENDin Firewall bzw. Router freigeben:- pro Match zwei aufeinanderfolgende Ports
- Spielport per UDP und TCP (TCP wird für RCON genutzt, nur für das Backend freigeben)
- CSTV-Port per UDP
- Etwa 60 GB Speicher für die Spieldateien im Volume
CS2_DOCKER_VOLUME. Das Volume wird von allen Match-Containern geteilt. Der erste Start lädt das Spiel herunter und kann deutlich länger als 10 Minuten dauern (CS2_START_TIMEOUT_MINUTES). Spieldateien am besten vorab mit einem einzelnen Container laden, bevor mehrere Matches gleichzeitig starten.
Container heißen cs2mv-<matchId> und tragen das Label cs2-mapveto=1. Stoppen bedeutet docker rm -f.
Option B: Vorhandene Server (SERVER_ADAPTER=static)
Für selbst betriebene, dauerhaft laufende CS2-Server:
SERVER_ADAPTER=static
CS2_STATIC_SERVERS=203.0.113.10:27015:rconPasswort1:27020,203.0.113.10:27025:rconPasswort2:27030
Auf jedem Server müssen gesetzt sein:
rcon_password- CSTV-Port
tv_port, falls abweichend von Spielport + 5 (dieser Wert wird angenommen, wenn kein vierter Teil angegeben ist) - ein GSLT (
sv_setsteamaccount)
Bei diesem Adapter bedeutet „Start“: einen freien Server reservieren und per RCON prüfen. „Stopp“ gibt ihn frei und setzt ein neues zufälliges sv_password. Der Serverprozess selbst wird nicht beendet.
Eigener Adapter (z. B. Hosting-API)
- Klasse mit
name,simulated = falseund den vier Methoden anlegen. Vorlage:dockerAdapter.js. Für die Konfiguration per RCON gibt esconfigureViaRconausrconConfigure.js. - In
createServerAdapterinadapter.jsregistrieren. - Zugangsdaten der Hosting-API ausschließlich über Umgebungsvariablen in
config.jseinlesen.
Verbindung für Spieler
password <serverpasswort>; connect <host>:<port>
Zusätzlich bietet die Seite einen steam://connect/<host>:<port>/<passwort>-Link an.
Zuschauer: CSTV (früher GOTV)
CS2 bringt CSTV mit (in der Konsole weiterhin tv_*-Befehle; früher GOTV). Die Anwendung setzt:
tv_enable 1(greift erst nach dem Mapwechsel, deshalb vorchangelevel)tv_delay(Standard 90 s) als Verzögerung gegen Ghostingtv_passwordals eigenes Passwort, getrennt vom Spielerpasswort
Zuschauer verbinden sich mit password <tvpasswort>; connect <host>:<tvPort>. Sie belegen keinen Spielerplatz und können die Spieler nicht beeinflussen. Für größere Zuschauerzahlen unterstützt CS2 CSTV-Relays (tv_relay) und HTTP-Broadcast (tv_broadcast_url). Beides ist nicht eingebaut und kann über CS2_EXTRA_COMMANDS bzw. die Serverkonfiguration ergänzt werden.
Prüfstand der CS2-Angaben
Die offizielle Seite Counter-Strike 2/Dedicated Servers im Valve Developer Wiki war während der Entwicklung wegen eines Bot-Schutzes nicht automatisiert abrufbar. Grundlage waren:
- das Source-RCON-Protokoll
- die Dokumentation des Images joedwards32/cs2 (vormals cm2network/cs2): Umgebungsvariablen, Ports,
TV_* - Hoster-Anleitungen zu CSTV, z. B. ZAP-Hosting
- FACEIT zu CSTV-Zuschauern
Bekannte Werte: game_type 0 + game_mode 1 = Competitive, game_mode 2 = Wingman; CSTV-Standardport 27020.
Nicht gegen einen echten CS2-Server getestet: Docker- und Static-Adapter sowie die Auswertung der status-Ausgabe (Map-Name, Spielerzahl). Das RCON-Protokoll ist gegen einen Test-Server geprüft. Vor dem ersten echten Einsatz bitte ein Testmatch durchspielen und bei Bedarf parseStatus in server/src/servers/rcon.js an die tatsächliche Ausgabe anpassen.
Bekannte Grenzen von Vanilla-CS2
- Ohne Server-Plugin kann CS2 Spieler nicht automatisch einem Team zuordnen. Spieler wählen ihr Team im Spiel selbst. Teamnamen und Startseiten setzt die Anwendung. Für feste Teamzuordnung, Pausen und automatischen Mapwechsel eignen sich Plugins wie MatchZy oder Get5-Nachfolger. Sie lassen sich über einen eigenen Adapter anbinden; die SteamID64 der Spieler wird dafür bereits erfasst.
- Das Team eines Spielers ergibt sich aus der Seite, die er im Spiel wählt. Wechselt ein Spieler ins falsche Team, zählen seine Runden für das falsche Team.
Live-Ergebnisse
Beim Konfigurieren setzt CS2 Mapveto auf dem Server log on und logaddress_add_http "<LOG_RECEIVER_URL>/api/logs/<match>/<schlüssel>". Der CS2-Server schickt damit seine Logzeilen per HTTP an das Backend. Der Schlüssel ist pro Match und Serverstart zufällig, Zeilen mit falschem Schlüssel werden abgelehnt.
- Was ausgewertet wird:
- Matchstart nach der Aufwärmphase
- jedes Rundenende mit Sieger und Grund (Bombe, Entschärfung, Zeit, Ausschalten)
- die Seitenzuordnung der Teams
- das Spielende
- Was angezeigt wird:
- Live-Spielstand und Rundenverlauf pro Map
- Serienstand
- Ergebnisse in der Übersicht
- Stream-Seite
- Nach dem Spielende: Ist die Serie noch offen, lädt CS2 Mapveto nach
SERIES_AUTO_ADVANCE_SECONDS(Standard 30) die nächste Map. Ist sie entschieden (z. B. 2:0 im Best of 3), wird das Match abgeschlossen und der Server freigegeben. Mit-1geht beides nur manuell. - Voraussetzung: Der CS2-Server muss
LOG_RECEIVER_URLerreichen, im LAN z. B.http://192.168.178.31:3000. Ob Daten ankommen, zeigt das Serverpanel des Organisators. MitLOG_DEBUG=truegibt das Backend alle empfangenen Zeilen aus. - Geprüft: mit einem AMP-Server (Wingman), siehe
server/test/fixtures/amp-wingman-inferno.log.
Zwei Eigenheiten von CS2 berücksichtigt der Parser:
- CS2 sendet
Match_Startschon beim Laden der Map. Gezählt wird erst, nachdem der Organisator „Aufwärmphase beenden“ geklickt hat. - Direkt nach der Halbzeit nennt CS2 zu Rundenbeginn noch die alte Seitenzuordnung. Ausgewertet werden deshalb nur die Zeilen direkt nach einem Rundenende.
Stream-Übersicht aller Matches
Unter /stream (in der Kopfzeile verlinkt) zeigt eine 1920×1080-Seite alle laufenden Vetos und Matches gleichzeitig, live aktualisiert:
- Karten: eine Karte pro Match.
- Im Veto: wer am Zug ist, Fortschritt, Map-Board.
- Im Match: aktuelle Map mit Live-Stand, CT/T-Seiten und Serienstand.
- Raster: passt sich der Anzahl an (1, 2, 4 oder 6 Karten). Bei mehr als 6 Matches blättert die Seite alle 12 Sekunden weiter.
- Unten: offene Lobbys und die Ergebnisse der letzten drei Stunden.
- Datenschutz: Codes und Verbindungsdaten erscheinen nicht.
- Schlüssel: Mit
STREAM_KEYist die Seite nur mit/stream?key=<schlüssel>erreichbar, sonst offen.?bg=transparentfunktioniert ebenfalls.
Stream-Übersicht eines Matches
Unter /stream/<Zuschauercode> gibt es eine Vollbild-Übersicht für den Stream. Den Link findet der Organisator bei den Einladungen.
- Format: 1920×1080, skaliert auf jede Fenstergröße. In OBS als Browserquelle mit 1920×1080 einbinden. Mit
?bg=transparentist der Hintergrund durchsichtig. - Ablauf: Die Seite wechselt automatisch zwischen Lobby (Aufstellungen), Map-Veto (live) und Match (Spielstand, Rundenverlauf, Map-Leiste).
- Laufband: zeigt das letzte für Zuschauer relevante Ereignis, keine technischen Servermeldungen.
- Datenschutz: Verbindungsdaten und Passwörter erscheinen dort nie.
Umgebungsvariablen
Alle Variablen mit Standardwerten stehen in .env.example. Die wichtigsten:
| Variable | Standard | Zweck |
|---|---|---|
PORT |
3000 |
Port des Backends |
PUBLIC_URL |
http://localhost:5173 |
Basis für Einladungslinks |
DATABASE_FILE |
data/cs2-mapveto.db |
SQLite-Datei |
CREATE_MATCH_PASSWORD |
leer | Passwort zum Erstellen von Matches |
REQUIRE_FULL_TEAMS |
false |
true: Veto nur mit vollständigen Teams |
SERVER_ADAPTER |
demo |
demo, docker oder static |
SERVER_IDLE_TIMEOUT_MINUTES |
30 |
Frist bis zum automatischen Herunterfahren |
SERVER_MAX_LIFETIME_HOURS |
8 |
harte Obergrenze pro Server |
CS2_TV_DELAY |
90 |
CSTV-Verzögerung in Sekunden |
CS2_GSLT |
leer | Docker: Game Server Login Token; leer = LAN-Modus (sv_lan 1) |
CS2_PUBLIC_HOST |
– | Pflicht bei Docker: öffentliche Adresse |
CS2_STATIC_SERVERS |
– | Pflicht bei Static: Serverliste |
DEMO_FAIL_FIRST_CONFIGURE |
false |
Demo: Konfigurationsfehler simulieren |
DEMO_ROUND_SECONDS |
4 |
Demo: Sekunden pro simulierter Runde |
LOG_RECEIVER_URL |
PUBLIC_URL |
Adresse, unter der der CS2-Server das Backend für Live-Logs erreicht |
LOG_DEBUG |
false |
empfangene Logzeilen im Terminal ausgeben |
STREAM_KEY |
leer | Schlüssel für die Stream-Übersicht aller Matches (/stream?key=…); leer = offen |
SERIES_AUTO_ADVANCE_SECONDS |
30 |
Wartezeit nach Map-Ende bis zur nächsten Map bzw. zum Abschluss; -1 = manuell |
Benötigte Zugangsdaten für den Echtbetrieb:
- Docker: Zugriff auf den Docker-Daemon (lokal oder per
DOCKER_HOST=ssh://…), Portfreigaben, für Internet-Betrieb ein GSLT (im LAN nicht nötig) - Static: RCON-Passwörter der Server
RCON-Passwörter für Docker-Container erzeugt das Backend selbst.
Projektstruktur
client/ React-Frontend (Vite)
src/pages/ Übersicht, Match erstellen, Match-Seite, Beitritt
src/components/ Teamkarten, Veto-Board, Serverpanel, Einladungen, Verlauf
src/lib/ API, Socket-Hook, lokale Zugangsdaten
server/
config/map-pools.json Map-Pools und Veto-Abläufe
src/veto.js reine Veto-Logik und Ablaufvalidierung
src/matchService.js Lobby, Rollen, Veto, rollenabhängige Snapshots
src/servers/ Adapter-Schnittstelle, Demo/Docker/Static, RCON, ServerManager
src/http.js REST-API
src/realtime.js Socket.IO
test/ node:test-Tests
REST-API (Auszug)
Alle Aktionen laufen per POST mit Authorization: Bearer <token>. Der Token ist der Organisator- oder Spieler-Token bzw. der Zuschauercode.
| Endpunkt | Wer |
|---|---|
GET /api/config, GET /api/matches |
alle |
POST /api/matches |
alle (ggf. mit CREATE_MATCH_PASSWORD) |
POST /api/access {code} |
prüft einen Code |
GET /api/matches/:id |
rollenabhängiger Zustand |
POST /api/matches/:id/join {code, nickname, steamId?} |
mit Teamcode |
…/ready, …/leave |
Spieler |
…/veto/action {stepIndex, map?, side?} |
Captain am Zug |
…/captain {playerId} |
Organisator, Captain des Teams |
…/veto/start, …/kick, …/codes/regenerate, …/server/retry, …/server/stop, …/server/warmup-end, …/series/next, …/finish, …/cancel |
Organisator |
Socket.IO: match:subscribe {matchId, token} liefert match:state bei jeder Änderung; overview:subscribe liefert overview.