finally updating name and steamID by just reconnecting, no more game relaunching
This commit is contained in:
parent
19ee3072a0
commit
19e7ff4f14
@ -65,3 +65,5 @@ css-client
|
||||
|
||||
## the inbuilt DXVK version 2.3 is replaced with DXVK 3.0 downloaded from https://github.com/doitsujin/dxvk/releases/download/v3.0/dxvk-3.0.tar.gz
|
||||
## docker cp dxvk-3.0/x64/d3d9.dll 'css-test:/home/ubuntu/Counter-Strike Source/bin/x64/dxvk_d3d9.dll' #probably was like this to copy it in.
|
||||
|
||||
## relies on nosteam_celt plugin for receiving the name correctly. -> https://git.unloze.com/UNLOZE/sm-plugins/src/branch/master/CELT_VOICE
|
||||
|
||||
@ -173,6 +173,30 @@ def handle_input(event):
|
||||
xdotool('click', button)
|
||||
elif t == 'release_keys':
|
||||
release_all_keys()
|
||||
elif t == 'connect_to_server':
|
||||
threading.Thread(target=_connect_to_server_sequence, daemon=True).start()
|
||||
|
||||
# ── Automated menu-connect sequence ──────────────────────────────────────────
|
||||
# Triggered when the signaling server writes a new password (real visitor IP)
|
||||
# into GameMenu.res for a new controller. Opens the in-game escape menu,
|
||||
# moves the mouse to the "UNLOZE | ZombieEscape" button at its fixed absolute
|
||||
# position (measured empirically at 800x600: x=110, y=348), and clicks it.
|
||||
# This re-connects the client with the freshly-written password, which the
|
||||
# server-side connect extension reads to assign the correct name/SteamID —
|
||||
# all without restarting the CSS process itself.
|
||||
CONNECT_BUTTON_X = 110
|
||||
CONNECT_BUTTON_Y = 348
|
||||
|
||||
def _connect_to_server_sequence():
|
||||
log.info('connect_to_server: opening menu')
|
||||
xdotool('key', 'Escape')
|
||||
time.sleep(0.5) # let the menu render
|
||||
|
||||
xdotool('mousemove', '--sync', str(CONNECT_BUTTON_X), str(CONNECT_BUTTON_Y))
|
||||
time.sleep(0.2) # let the button highlight/register hover
|
||||
|
||||
xdotool('click', '1')
|
||||
log.info('connect_to_server: clicked ZombieEscape button')
|
||||
|
||||
# ── GStreamer Pipeline ────────────────────────────────────────────────────────
|
||||
class CSSStreamAgent:
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* Signaling Server
|
||||
*
|
||||
* Handles two tracks: video and game audio.
|
||||
* Also manages controller queue and CSS process restart for name changes.
|
||||
* Also manages controller queue.
|
||||
* All Cloudflare API calls happen server-side.
|
||||
*
|
||||
* Run: CF_APP_ID=xxx CF_APP_TOKEN=xxx NAME_SALT=xxx CONTAINER_NAME=css-test node signaling-server.js
|
||||
@ -16,6 +16,7 @@ const WebSocket = require('ws');
|
||||
const fetch = require('node-fetch');
|
||||
const path = require('path');
|
||||
const { spawn, execSync } = require('child_process');
|
||||
const dns = require('dns').promises;
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const CF_APP_ID = process.env.CF_APP_ID || '';
|
||||
@ -25,9 +26,6 @@ const CONTAINER = process.env.CONTAINER_NAME || 'css-test';
|
||||
const IDLE_TIMEOUT = 60000; // 60 seconds
|
||||
const NAME_SALT = process.env.NAME_SALT || 'change-this-secret';
|
||||
|
||||
// How long to wait after relaunching CSS before granting control
|
||||
const CSS_LAUNCH_WAIT = 15000;
|
||||
|
||||
if (!CF_APP_ID || !CF_APP_TOKEN) {
|
||||
console.error('[sig] CF_APP_ID and CF_APP_TOKEN must be set');
|
||||
process.exit(1);
|
||||
@ -62,8 +60,109 @@ const server = http.createServer(app);
|
||||
const wss = new WebSocket.Server({ server });
|
||||
|
||||
app.use(express.json());
|
||||
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.
|
||||
//
|
||||
// The public-IP allowance is required because SteamWorks' embedded HTTP
|
||||
// client (or the Source Engine networking layer beneath it) does NOT route
|
||||
// outbound requests through true OS loopback the way a normal HTTP library
|
||||
// would — even when targeting 127.0.0.1, the connection is sourced from
|
||||
// whatever interface the game server process is bound to for its own
|
||||
// traffic, which is the public interface. This was confirmed directly from
|
||||
// logs: requests aimed at 127.0.0.1 arrived showing the box's public IP as
|
||||
// the remote address. A strict "127.0.0.1 only" check would silently reject
|
||||
// the SourceMod plugin's own requests.
|
||||
//
|
||||
// This is still safe: the TCP handshake proves the sender genuinely owns
|
||||
// that IP (not spoofable over a real connection), so allowing this one
|
||||
// specific, DNS-resolved address does not expose the endpoint to the public
|
||||
// internet — nobody else can complete a TCP handshake claiming to be this
|
||||
// server's own IP.
|
||||
const SERVER_HOSTNAME = process.env.SERVER_HOSTNAME || 'ze.unloze.com';
|
||||
let resolvedServerIp = null;
|
||||
|
||||
async function refreshServerPublicIp() {
|
||||
try {
|
||||
const { address } = await dns.lookup(SERVER_HOSTNAME);
|
||||
resolvedServerIp = address;
|
||||
console.log(`[sig] resolved ${SERVER_HOSTNAME} -> ${address}`);
|
||||
} catch (e) {
|
||||
console.error(`[sig] failed to resolve ${SERVER_HOSTNAME}:`, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
refreshServerPublicIp();
|
||||
|
||||
function isInternalRequest(req) {
|
||||
const ip = req.socket.remoteAddress || '';
|
||||
// Normalise IPv4-mapped IPv6 addresses (::ffff:127.0.0.1 -> 127.0.0.1)
|
||||
const normalized = ip.replace('::ffff:', '');
|
||||
return normalized === '127.0.0.1' ||
|
||||
normalized === '::1' ||
|
||||
normalized.startsWith('172.') || // docker bridge network range
|
||||
normalized.startsWith('10.') || // private network range, adjust as needed
|
||||
(resolvedServerIp && normalized === resolvedServerIp);
|
||||
}
|
||||
|
||||
// SourceMod plugin -> signaling server: reports the name the connect
|
||||
// extension generated for the currently-connected webclient player, so the
|
||||
// browser queue panel and controller-restart-skip logic stay in sync with
|
||||
// the actual in-game name/SteamID.
|
||||
app.post('/webclient-name', (req, res) => {
|
||||
if (!isInternalRequest(req)) {
|
||||
console.log(`[sig] rejected /webclient-name from non-internal IP: ${req.socket.remoteAddress}`);
|
||||
return res.status(403).send('forbidden');
|
||||
}
|
||||
|
||||
const name = (req.body && req.body.name) ? String(req.body.name).trim() : '';
|
||||
if (!name) {
|
||||
return res.status(400).send('missing name');
|
||||
}
|
||||
|
||||
console.log(`[sig] webclient-name received: ${name}`);
|
||||
lastControllerName = name;
|
||||
if (currentController) {
|
||||
currentController.visitorName = name;
|
||||
}
|
||||
broadcastQueueState();
|
||||
|
||||
res.status(200).send('ok');
|
||||
});
|
||||
|
||||
// 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.
|
||||
// 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}`);
|
||||
return res.status(403).send('forbidden');
|
||||
}
|
||||
|
||||
const ip = currentController ? (currentController.ws._realIp || '') : '';
|
||||
res.status(200).send(ip);
|
||||
});
|
||||
|
||||
// ── Session registry ──────────────────────────────────────────────────────────
|
||||
const agentSessions = new Map();
|
||||
const browserToAgent = new Map();
|
||||
@ -100,77 +199,61 @@ function resetIdleTimer(entry) {
|
||||
}, IDLE_TIMEOUT);
|
||||
}
|
||||
|
||||
async function applyController(entry) {
|
||||
function applyController(entry) {
|
||||
// Defensive: if this exact entry is sitting in the queue (e.g. a race
|
||||
// between request_control calls), pull it out so it's never displayed
|
||||
// as both controller and queued.
|
||||
const qIdx = controlQueue.indexOf(entry);
|
||||
if (qIdx !== -1) controlQueue.splice(qIdx, 1);
|
||||
|
||||
containerRestarting = true;
|
||||
broadcastQueueState();
|
||||
|
||||
// Same person returning — no restart needed
|
||||
if (entry.visitorName === lastControllerName) {
|
||||
console.log(`[sig] same controller returning, skipping restart`);
|
||||
containerRestarting = false;
|
||||
// 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").
|
||||
lastControllerName = entry.visitorName;
|
||||
currentController = entry;
|
||||
resetIdleTimer(entry);
|
||||
broadcastQueueState();
|
||||
|
||||
console.log(`[sig] applying controller: ${entry.visitorName}`);
|
||||
|
||||
if (entry.ws.readyState === WebSocket.OPEN) {
|
||||
entry.ws.send(JSON.stringify({ type: 'control_granted' }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Different person — update rev.ini and relaunch CSS only
|
||||
lastControllerName = entry.visitorName;
|
||||
console.log(`[sig] applying controller: ${entry.visitorName}`);
|
||||
// ── 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
|
||||
|
||||
// Update rev.ini
|
||||
try {
|
||||
const safeName = entry.visitorName.replace(/'/g, "'\\''");
|
||||
execSync(`docker exec ${CONTAINER} sed -i "s/PlayerName=.*/PlayerName=${safeName}/" "/home/ubuntu/Counter-Strike Source/rev.ini"`);
|
||||
console.log(`[sig] rev.ini updated with name: ${entry.visitorName}`);
|
||||
} catch (e) {
|
||||
console.error('[sig] failed to update rev.ini:', e.message);
|
||||
}
|
||||
|
||||
// Kill CSS and revLoader (leave Xvfb, PulseAudio, gst_agent running)
|
||||
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] CSS processes killed');
|
||||
} catch (e) {
|
||||
console.error('[sig] failed to kill CSS:', e.message);
|
||||
}
|
||||
console.log('[sig] scheduled restart: CSS processes killed');
|
||||
} catch (e) {
|
||||
console.error('[sig] scheduled restart: failed to kill CSS:', e.message);
|
||||
}
|
||||
|
||||
// Wait for Wine to clean up
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
// Relaunch CSS
|
||||
setTimeout(() => {
|
||||
try {
|
||||
execSync(`docker exec -d ${CONTAINER} bash -c "DISPLAY=:99 wine '/home/ubuntu/Counter-Strike Source/revLoader.exe'"`);
|
||||
console.log('[sig] CSS relaunched');
|
||||
console.log('[sig] scheduled restart: CSS relaunched');
|
||||
} catch (e) {
|
||||
console.error('[sig] failed to relaunch CSS:', e.message);
|
||||
}
|
||||
|
||||
// Wait for CSS to launch and connect to server
|
||||
await new Promise(r => setTimeout(r, CSS_LAUNCH_WAIT));
|
||||
|
||||
containerRestarting = false;
|
||||
currentController = entry;
|
||||
resetIdleTimer(entry);
|
||||
broadcastQueueState();
|
||||
|
||||
if (entry.ws.readyState === WebSocket.OPEN) {
|
||||
entry.ws.send(JSON.stringify({ type: 'control_granted' }));
|
||||
console.error('[sig] scheduled restart: failed to relaunch CSS:', e.message);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
setInterval(scheduledCssRestart, CSS_RESTART_INTERVAL);
|
||||
|
||||
function releaseControl(entry, reason) {
|
||||
if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; }
|
||||
if (currentController !== entry) return;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user