added bomb tracking

This commit is contained in:
Linrador 2025-09-09 14:07:02 +02:00
parent aefe779db8
commit 4e0acf9e7c

135
server.js
View File

@ -1,3 +1,4 @@
// server.js
import express from "express"; import express from "express";
import http from "http"; import http from "http";
import { WebSocketServer } from "ws"; import { WebSocketServer } from "ws";
@ -16,17 +17,23 @@ const GSI_TOKEN = process.env.GSI_TOKEN || "SUPERSECRET";
const app = express(); const app = express();
app.use(cors()); app.use(cors());
app.use(bodyParser.json({ limit: "512kb" })); app.use(bodyParser.json({ limit: "512kb" }));
app.use(express.static("public")); // Radar-Frontend app.use(express.static("public")); // optional: dein Radar-Frontend
const server = http.createServer(app); const server = http.createServer(app);
// --- WS Hub für Frontends --- /*
* WS Hub für Frontends
* */
const wss = new WebSocketServer({ server, path: WS_PATH }); const wss = new WebSocketServer({ server, path: WS_PATH });
const clients = new Set(); const clients = new Set();
/** Aktueller Map-Key und Round-/Bomben-State */
let currentMap = null; let currentMap = null;
let lastPhase = null;
/** lastBomb: { x,y,z, status: 'planted'|'dropped'|'carried'|'unknown' } */
let lastBomb = null;
// Hilfsfunktion für IDs /** Hilfsfunktion für IDs */
let _nextId = 1; let _nextId = 1;
function clientId(ws) { return ws._id ?? "anon"; } function clientId(ws) { return ws._id ?? "anon"; }
@ -40,7 +47,7 @@ wss.on("connection", (ws, req) => {
ws.on("pong", () => { ws.isAlive = true; }); ws.on("pong", () => { ws.isAlive = true; });
ws.on("close", (code, reason) => { ws.on("close", (code) => {
clients.delete(ws); clients.delete(ws);
console.log(`[WS] - disconnect id=${ws._id} code=${code} total=${clients.size}`); console.log(`[WS] - disconnect id=${ws._id} code=${code} total=${clients.size}`);
}); });
@ -49,13 +56,13 @@ wss.on("connection", (ws, req) => {
console.warn(`[WS] ! error id=${ws._id}:`, err?.message || err); console.warn(`[WS] ! error id=${ws._id}:`, err?.message || err);
}); });
// Begrüßung // Begrüßung mit aktuellem Kontext (Map + ggf. Bombe)
try { try {
ws.send(JSON.stringify({ type: "hello", id: ws._id, map: currentMap })); ws.send(JSON.stringify({ type: "hello", id: ws._id, map: currentMap, bomb: lastBomb }));
} catch {} } catch {}
}); });
// Heartbeat, um tote Verbindungen zu killen /** Heartbeat, um tote Verbindungen zu killen */
setInterval(() => { setInterval(() => {
for (const ws of clients) { for (const ws of clients) {
if (ws.isAlive === false) { if (ws.isAlive === false) {
@ -69,17 +76,20 @@ setInterval(() => {
} }
}, 30000); }, 30000);
// Broadcast an alle Clients /** Broadcast an alle Clients */
function broadcast(obj) { function broadcast(obj) {
const msg = JSON.stringify(obj); const msg = JSON.stringify(obj);
for (const ws of clients) { for (const ws of clients) {
if (ws.readyState === 1) ws.send(msg); if (ws.readyState === 1) { // OPEN
try { ws.send(msg); } catch {}
}
} }
} }
/** /*
* Hilfsfunktionen * Hilfsfunktionen
*/ * */
const lastBySteam = new Map(); const lastBySteam = new Map();
function parseVec3(str) { function parseVec3(str) {
@ -120,20 +130,46 @@ function normalizeWeapons(weaponsObj) {
return { all: out, active }; return { all: out, active };
} }
/** /** Bombe aus GSI normalisieren */
function normalizeBombFromGSI(body) {
const b = body?.bomb || body?.c4;
if (!b) return null;
const posStr = b.position || b.pos || b.location || null;
const pos = typeof posStr === "string"
? parseVec3(posStr)
: { x: Number(b?.x) || 0, y: Number(b?.y) || 0, z: Number(b?.z) || 0 };
let raw = String(b?.state || b?.status || "").toLowerCase();
let status = "unknown"; // 'planted'|'dropped'|'carried'|'unknown'
if (raw.includes("plant")) status = "planted";
else if (raw.includes("drop")) status = "dropped";
else if (raw.includes("carry")) status = "carried";
// bool-Fallbacks
if (b?.planted === true) status = "planted";
if (b?.dropped === true) status = "dropped";
if (b?.carried === true) status = "carried";
if (!Number.isFinite(pos.x) || !Number.isFinite(pos.y)) return null;
return { x: pos.x, y: pos.y, z: pos.z, status };
}
function sameBomb(a, b) {
if (!a && !b) return true;
if (!a || !b) return false;
return a.status === b.status && a.x === b.x && a.y === b.y;
}
/*
* HTTP Endpoint für CS2 GSI POSTs * HTTP Endpoint für CS2 GSI POSTs
* * Erwartet typische CS:GO/CS2-GSI-Felder
* Erwartet: * */
* - body.map, body.phase_countdowns
* - body.allplayers: { [steamid]: { name, team, position, forward, state, weapons, ... } }
* - body.player: { steamid, team, activity, ... } (POV)
* - body.auth.token
*/
app.post(GSI_PATH, (req, res) => { app.post(GSI_PATH, (req, res) => {
try { try {
const body = req.body || {}; const body = req.body || {};
// 1) Auth vor Verarbeitung prüfen // 1) Auth
const tok = body?.auth?.token || ""; const tok = body?.auth?.token || "";
if (tok !== GSI_TOKEN) { if (tok !== GSI_TOKEN) {
return res.status(401).json({ ok: false, err: "bad token" }); return res.status(401).json({ ok: false, err: "bad token" });
@ -154,7 +190,22 @@ app.post(GSI_PATH, (req, res) => {
broadcast({ type: "map", name: currentMap }); broadcast({ type: "map", name: currentMap });
} }
// 3) Spieler normalisieren (aus allplayers!) // Round-Events anhand Phase-Transition
const curPhase = (phaseInfo.phase || "").toLowerCase();
if (curPhase !== lastPhase) {
// Start zählt, sobald es "live" wird (alternativ: "freezetime" → "live")
if (curPhase === "live" && lastPhase && lastPhase !== "live") {
broadcast({ type: "round_start", round: phaseInfo.round ?? null });
}
// Ende, wenn "over"
if (curPhase === "over" && lastPhase !== "over") {
broadcast({ type: "round_end", round: phaseInfo.round ?? null });
lastBomb = null; // round-reset
}
lastPhase = curPhase;
}
// 3) Spieler normalisieren (aus allplayers)
const t = Date.now(); const t = Date.now();
const allplayers = body?.allplayers || {}; const allplayers = body?.allplayers || {};
const presentIds = new Set(); const presentIds = new Set();
@ -166,7 +217,7 @@ app.post(GSI_PATH, (req, res) => {
const pos = parseVec3(p.position); const pos = parseVec3(p.position);
const fwd = parseVec3(p.forward); const fwd = parseVec3(p.forward);
const { yaw, pitch } = forwardToYawPitch(fwd); const { yaw, pitch } = forwardToYawPitch(fwd);
const eye = { x: pos.x, y: pos.y, z: pos.z + 64 }; // ~Kopf-/Augenhöhe const eye = { x: pos.x, y: pos.y, z: pos.z + 64 }; // grobe Augenhöhe
const { all: weapons, active } = normalizeWeapons(p.weapons); const { all: weapons, active } = normalizeWeapons(p.weapons);
@ -196,22 +247,42 @@ app.post(GSI_PATH, (req, res) => {
return snap; return snap;
}); });
// 4) Aus Cache entfernen, wer nicht mehr im allplayers-Snapshot ist // Aus Cache entfernen, wer im Snapshot fehlt
for (const sid of Array.from(lastBySteam.keys())) { for (const sid of Array.from(lastBySteam.keys())) {
if (!presentIds.has(sid)) lastBySteam.delete(sid); if (!presentIds.has(sid)) lastBySteam.delete(sid);
} }
// 5) (Optional) Grenades roh durchreichen (du kannst hier später normalisieren) // 4) Grenades roh (du kannst bei Bedarf normalisieren)
const grenades = body?.grenades || {}; const grenades = body?.grenades || {};
// 6) Broadcast eines kompakten "tick"-Pakets für Clients // 5) Bombe normalisieren + Events bei Statuswechsel
const bomb = normalizeBombFromGSI(body);
if (!sameBomb(bomb, lastBomb)) {
if (bomb) {
if (bomb.status === "planted" && (!lastBomb || lastBomb.status !== "planted")) {
broadcast({ type: "bomb_planted", bomb });
} else if (bomb.status === "dropped") {
broadcast({ type: "bomb_dropped", bomb });
} else if (bomb.status === "carried" && lastBomb && lastBomb.status === "dropped") {
broadcast({ type: "bomb_pickup", bomb });
} else {
broadcast({ type: "bomb", bomb });
}
} else if (lastBomb) {
broadcast({ type: "bomb_cleared" });
}
lastBomb = bomb || null;
}
// 6) Tick mit kompletter Momentaufnahme (inkl. bomb)
broadcast({ broadcast({
type: "tick", type: "tick",
t, t,
map: currentMap, map: currentMap,
phase: phaseInfo, phase: phaseInfo,
players, // vollständiger Satz, alle Spieler dieses Snapshots players, // vollständiger Satz aller Spieler dieses Snapshots
grenades // roh grenades, // roh
bomb // für Client-Rendering der C4
}); });
// 7) Erfolg // 7) Erfolg
@ -222,10 +293,11 @@ app.post(GSI_PATH, (req, res) => {
} }
}); });
// health /*
* Health / Stats
* */
app.get("/health", (_req, res) => res.json({ ok: true, clients: clients.size })); app.get("/health", (_req, res) => res.json({ ok: true, clients: clients.size }));
// stats
app.get("/stats", (_req, res) => { app.get("/stats", (_req, res) => {
const list = []; const list = [];
for (const ws of clients) { for (const ws of clients) {
@ -235,9 +307,12 @@ app.get("/stats", (_req, res) => {
isAlive: !!ws.isAlive isAlive: !!ws.isAlive
}); });
} }
res.json({ ok: true, count: list.length, clients: list, map: currentMap }); res.json({ ok: true, count: list.length, clients: list, map: currentMap, bomb: lastBomb, phase: lastPhase });
}); });
/*
* Start
* */
server.listen(PORT, () => { server.listen(PORT, () => {
console.log(`[HTTP] listening on http://0.0.0.0:${PORT}`); console.log(`[HTTP] listening on http://0.0.0.0:${PORT}`);
console.log(`[WS] ws://HOST:${PORT}${WS_PATH}`); console.log(`[WS] ws://HOST:${PORT}${WS_PATH}`);