added option for mouse acceleration and button to manually restart the game if stuck

This commit is contained in:
2026-07-24 10:57:53 +01:00
parent 19e7ff4f14
commit 62e74b740f
2 changed files with 151 additions and 38 deletions
+67 -36
View File
@@ -64,10 +64,11 @@ app.use(express.urlencoded({ extended: false }));
app.get('/', (req, res) => res.sendFile(path.join(__dirname, '..', 'client.html')));
// ── Internal-request gate ─────────────────────────────────────────────────────
// Used by both /webclient-name (POST, from the SourceMod plugin) and
// /webclient-current-ip (GET, polled by the SourceMod plugin). Restricted to
// loopback / docker-bridge / private ranges, PLUS this box's own resolved
// public IP.
// Used by /webclient-name (POST), /webclient-current-ip (GET), and
// /webclient-force-restart (POST) — all internal-only endpoints intended for
// the SourceMod plugin (and, for restart, an optional safety-net trigger).
// Restricted to loopback / docker-bridge / private ranges, PLUS this box's
// own resolved public IP.
//
// The public-IP allowance is required because SteamWorks' embedded HTTP
// client (or the Source Engine networking layer beneath it) does NOT route
@@ -121,15 +122,25 @@ app.post('/webclient-name', (req, res) => {
}
const name = (req.body && req.body.name) ? String(req.body.name).trim() : '';
const ip = (req.body && req.body.ip) ? String(req.body.ip).trim() : '';
if (!name) {
return res.status(400).send('missing name');
}
// Only apply this name if it corresponds to the CURRENTLY active
// controller's real IP. Without this check, a delayed/stale POST from a
// previous webclient session (the connect extension's OnClientAuthorized
// can fire with a noticeable delay after the actual in-game reconnect)
// can land AFTER a new person has already taken over control, silently
// overwriting the new controller's correct name with the old one's.
if (!currentController || !ip || currentController.ws._realIp !== ip) {
console.log(`[sig] ignored stale /webclient-name (ip=${ip || 'none'}) — does not match current controller`);
return res.status(200).send('ignored (stale)');
}
console.log(`[sig] webclient-name received: ${name}`);
lastControllerName = name;
if (currentController) {
currentController.visitorName = name;
}
currentController.visitorName = name;
broadcastQueueState();
res.status(200).send('ok');
@@ -138,21 +149,9 @@ app.post('/webclient-name', (req, res) => {
// SourceMod plugin -> signaling server (polled): returns the current
// controller's real IP as plain text, so the plugin can detect when it
// changes and force the already-connected webclient player to reconnect
// with "password <ip>; retry" — reusing the exact same connect-extension
// path already proven correct when a password is supplied manually.
// via ClientCommand(client, "retry")the connect extension backfills the
// real password from this same cached IP via OnClientPreConnectEx.
// Returns an empty body when nobody is currently controlling.
//
// This is the ONLY mechanism used to propagate the new controller's IP into
// the game now — there is no more GameMenu.res rewriting, no sed, no
// automated Escape/mouse-move/click sequence. That whole approach was
// removed: GameMenu.res is only parsed once by the engine at menu-build
// time, so rewriting the file on disk while CSS keeps running indefinitely
// (which we do, by design, to avoid restarting the game on every handoff)
// never actually took effect after the very first read — every subsequent
// person's reconnect silently reused whichever password was cached from
// that first parse, which is why everyone ended up with the same generated
// name/SteamID regardless of how many times the file was correctly
// rewritten on disk.
app.get('/webclient-current-ip', (req, res) => {
if (!isInternalRequest(req)) {
console.log(`[sig] rejected /webclient-current-ip from non-internal IP: ${req.socket.remoteAddress}`);
@@ -163,6 +162,22 @@ app.get('/webclient-current-ip', (req, res) => {
res.status(200).send(ip);
});
// Internal-only safety net: force-kill and relaunch CSS on demand. Intended
// for use by the SourceMod plugin (or any other trusted internal caller) if
// it detects the game client itself has wedged/crashed and needs a hard
// restart, separate from the manual in-browser restart button and the
// scheduled 5-hour restart.
app.post('/webclient-force-restart', (req, res) => {
if (!isInternalRequest(req)) {
console.log(`[sig] rejected /webclient-force-restart from non-internal IP: ${req.socket.remoteAddress}`);
return res.status(403).send('forbidden');
}
console.log('[sig] /webclient-force-restart triggered');
performCssRestart('force-restart endpoint');
res.status(200).send('ok');
});
// ── Session registry ──────────────────────────────────────────────────────────
const agentSessions = new Map();
const browserToAgent = new Map();
@@ -209,7 +224,7 @@ function applyController(entry) {
// Granting control is now instant — no CSS restart, no GameMenu.res
// rewrite, no automated click sequence. The SourceMod plugin picks up the
// new controller's IP by polling GET /webclient-current-ip and forces the
// in-game reconnect itself via ClientCommand(client, "password %s; retry").
// in-game reconnect itself via ClientCommand(client, "retry").
lastControllerName = entry.visitorName;
currentController = entry;
resetIdleTimer(entry);
@@ -222,37 +237,42 @@ function applyController(entry) {
}
}
// ── Periodic CSS restart ──────────────────────────────────────────────────────
// Controller handoffs no longer restart the game at all, so CSS now stays
// running indefinitely across many sessions. Long Wine sessions have
// historically accumulated issues (audio/keyboard corruption, memory
// growth), so restart the game process on a fixed schedule regardless of
// who's currently in control.
const CSS_RESTART_INTERVAL = 5 * 60 * 60 * 1000; // 5 hours
// ── CSS restart (shared by scheduled timer, manual button, and the internal
// force-restart endpoint) ──────────────────────────────────────────────────
function performCssRestart(reason) {
console.log(`[sig] CSS restart starting (${reason})`);
containerRestarting = true;
broadcastQueueState();
function scheduledCssRestart() {
console.log('[sig] scheduled 5-hour CSS restart starting');
try {
execSync(`docker exec ${CONTAINER} pkill -f "cstrike_win64.exe" || true`);
execSync(`docker exec ${CONTAINER} pkill -f "revLoader.exe" || true`);
execSync(`docker exec ${CONTAINER} pkill -f "wineserver" || true`);
execSync(`docker exec ${CONTAINER} pkill -f "winedevice.exe" || true`);
console.log('[sig] scheduled restart: CSS processes killed');
console.log('[sig] CSS restart: processes killed');
} catch (e) {
console.error('[sig] scheduled restart: failed to kill CSS:', e.message);
console.error('[sig] CSS restart: failed to kill CSS:', e.message);
}
setTimeout(() => {
try {
execSync(`docker exec -d ${CONTAINER} bash -c "DISPLAY=:99 wine '/home/ubuntu/Counter-Strike Source/revLoader.exe'"`);
console.log('[sig] scheduled restart: CSS relaunched');
console.log('[sig] CSS restart: relaunched');
} catch (e) {
console.error('[sig] scheduled restart: failed to relaunch CSS:', e.message);
console.error('[sig] CSS restart: failed to relaunch CSS:', e.message);
}
containerRestarting = false;
broadcastQueueState();
}, 2000);
}
setInterval(scheduledCssRestart, CSS_RESTART_INTERVAL);
// Periodic restart: controller handoffs no longer restart the game at all,
// so CSS stays running indefinitely across many sessions. Long Wine
// sessions have historically accumulated issues (audio/keyboard corruption,
// memory growth, wedged connection state), so restart on a fixed schedule
// regardless of who's currently in control.
const CSS_RESTART_INTERVAL = 5 * 60 * 60 * 1000; // 5 hours
setInterval(() => performCssRestart('scheduled 5-hour restart'), CSS_RESTART_INTERVAL);
function releaseControl(entry, reason) {
if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; }
@@ -426,6 +446,17 @@ wss.on('connection', (ws, req) => {
}
}
else if (msg.type === 'restart_css') {
// Manual restart button — only the CURRENT controller may trigger
// this. Anyone else's message is silently ignored.
if (currentController && currentController.ws === ws) {
console.log(`[sig] manual CSS restart requested by ${currentController.visitorName}`);
performCssRestart(`manual button — ${currentController.visitorName}`);
} else {
console.log('[sig] restart_css ignored — sender is not the current controller');
}
}
else if (msg.type === 'mic_stop') {
if (browserSessId) stopPacat(browserSessId);
}