From 4846e045bb21db5d93e6af397da52cae81ff5ff4 Mon Sep 17 00:00:00 2001 From: Wizzard <rich@bandaholics.cash> Date: Sun, 16 Mar 2025 19:04:52 -0400 Subject: [PATCH] Latency optimization --- src/websocket.rs | 118 ++++++++++++++----- webradar/script.js | 275 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 337 insertions(+), 56 deletions(-) diff --git a/src/websocket.rs b/src/websocket.rs index f9b0a83..f781d9c 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, path::PathBuf}; +use std::{sync::Arc, path::PathBuf, collections::HashMap}; use axum::{ extract::{ws::{WebSocketUpgrade, WebSocket, Message}, State}, response::Response, @@ -7,14 +7,21 @@ use axum::{ }; use flate2::{write::GzEncoder, Compression}; use std::io::Write; -use tokio::sync::RwLock; +use tokio::sync::{RwLock, Mutex}; use tower_http::services::ServeDir; -use crate::comms::{RadarData, ArcRwlockRadarData}; +use crate::comms::{RadarData, ArcRwlockRadarData, EntityData}; + +struct ClientState { + last_entity_count: usize, + ping_ms: u32, + high_latency: bool, +} #[derive(Clone)] struct AppState { - data_lock: Arc<RwLock<RadarData>> + data_lock: Arc<RwLock<RadarData>>, + clients: Arc<Mutex<HashMap<String, ClientState>>>, } async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> Response { @@ -23,48 +30,81 @@ async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> Resp } async fn handle_socket(mut socket: WebSocket, state: AppState) { + let client_id = uuid::Uuid::new_v4().to_string(); + + { + let mut clients = state.clients.lock().await; + clients.insert(client_id.clone(), ClientState { + last_entity_count: 0, + ping_ms: 0, + high_latency: false, + }); + } + let mut compression_buffer: Vec<u8> = Vec::with_capacity(65536); + let mut frame_counter = 0; + let mut skip_frames = false; while let Some(msg) = socket.recv().await { if let Ok(msg) = msg { if let Ok(text) = msg.to_text() { if text == "requestInfo" { + frame_counter += 1; + if skip_frames && frame_counter % 2 != 0 { + continue; + } + let radar_data = state.data_lock.read().await; + let mut clients = state.clients.lock().await; + let client_state = clients.get_mut(&client_id).unwrap(); - if let Ok(json) = serde_json::to_string(&*radar_data) { - compression_buffer.clear(); + let entity_count = radar_data.get_entities().len(); - let compression_level = if json.len() > 10000 { - Compression::best() - } else { - Compression::fast() - }; + if entity_count > 5 && !skip_frames && client_state.ping_ms > 100 { + skip_frames = true; + log::info!("Enabling frame skipping for high latency client"); + } - let mut encoder = GzEncoder::new(Vec::new(), compression_level); - if encoder.write_all(json.as_bytes()).is_ok() { - match encoder.finish() { - Ok(compressed) => { - if compressed.len() < json.len() { - let mut message = vec![0x01]; - message.extend_from_slice(&compressed); - let _ = socket.send(Message::Binary(message)).await; - } else { - let mut uncompressed = vec![0x00]; - uncompressed.extend_from_slice(json.as_bytes()); - let _ = socket.send(Message::Binary(uncompressed)).await; - } - }, - Err(_) => { + client_state.last_entity_count = entity_count; + + let Ok(json) = serde_json::to_string(&*radar_data) else { + continue; + }; + + compression_buffer.clear(); + + let compression_level = if json.len() > 20000 || client_state.high_latency { + Compression::best() + } else if json.len() > 5000 { + Compression::default() + } else { + Compression::fast() + }; + + let mut encoder = GzEncoder::new(Vec::new(), compression_level); + if encoder.write_all(json.as_bytes()).is_ok() { + match encoder.finish() { + Ok(compressed) => { + if compressed.len() < json.len() { + let mut message = vec![0x01]; + message.extend_from_slice(&compressed); + let _ = socket.send(Message::Binary(message)).await; + } else { let mut uncompressed = vec![0x00]; uncompressed.extend_from_slice(json.as_bytes()); let _ = socket.send(Message::Binary(uncompressed)).await; } + }, + Err(_) => { + let mut uncompressed = vec![0x00]; + uncompressed.extend_from_slice(json.as_bytes()); + let _ = socket.send(Message::Binary(uncompressed)).await; } - } else { - let mut uncompressed = vec![0x00]; - uncompressed.extend_from_slice(json.as_bytes()); - let _ = socket.send(Message::Binary(uncompressed)).await; } + } else { + let mut uncompressed = vec![0x00]; + uncompressed.extend_from_slice(json.as_bytes()); + let _ = socket.send(Message::Binary(uncompressed)).await; } } else if text == "toggleMoneyReveal" { let new_value = { @@ -80,19 +120,35 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) { }); let _ = socket.send(Message::Text(response.to_string())).await; + } else if text.starts_with("ping:") { + if let Some(ping_str) = text.strip_prefix("ping:") { + if let Ok(ping_ms) = ping_str.parse::<u32>() { + let mut clients = state.clients.lock().await; + if let Some(client) = clients.get_mut(&client_id) { + client.ping_ms = ping_ms; + client.high_latency = ping_ms > 100; + } + } + } + let _ = socket.send(Message::Text("pong".to_string())).await; } } } else { break; } } + let mut clients = state.clients.lock().await; + clients.remove(&client_id); } pub async fn run(path: PathBuf, port: u16, data_lock: Arc<RwLock<RadarData>>) -> anyhow::Result<()> { let app = Router::new() .nest_service("/", ServeDir::new(path)) .route("/ws", get(ws_handler)) - .with_state(AppState { data_lock }); + .with_state(AppState { + data_lock, + clients: Arc::new(Mutex::new(HashMap::new())) + }); let address = format!("0.0.0.0:{}", port); log::info!("Starting WebSocket server on {}", address); diff --git a/webradar/script.js b/webradar/script.js index cee8670..e522725 100644 --- a/webradar/script.js +++ b/webradar/script.js @@ -17,6 +17,18 @@ let drawNames = true; let drawGuns = true; let drawMoney = true; +const NETWORK_SETTINGS = { + useInterpolation: true, + interpolationAmount: 0.6, + pingInterval: 3000 +}; + +let connectionHealthy = true; +let lastResponseTime = 0; +let requestTimeoutTimer = null; +let reconnecting = false; +let retryCount = 0; + let isRequestPending = false; let frameCounter = 0; let fpsStartTime = 0; @@ -25,6 +37,10 @@ let currentFps = 0; let temporarilyDisableRotation = false; let rotationDisabledUntilRespawn = false; let lastKnownPositions = {}; +let entityInterpolationData = {}; +let lastUpdateTime = 0; +let networkLatencyHistory = []; +let lastPingSent = 0; let focusedPlayerYaw = 0; let focusedPlayerName = "YOU"; @@ -73,6 +89,29 @@ const websocketAddr = location.protocol === 'https:' // Util functions const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const degreesToRadians = (degrees) => degrees * (Math.PI / 180); +const lerp = (start, end, t) => start * (1 - t) + end * t; + +function lerpPosition(pos1, pos2, t) { + if (!pos1 || !pos2) return pos2 || pos1 || null; + return { + x: lerp(pos1.x, pos2.x, t), + y: lerp(pos1.y, pos2.y, t), + z: lerp(pos1.z, pos2.z, t) + }; +} + +function lerpAngle(a, b, t) { + while (a > 360) a -= 360; + while (a < 0) a += 360; + while (b > 360) b -= 360; + while (b < 0) b += 360; + + let diff = b - a; + if (diff > 180) diff -= 360; + if (diff < -180) diff += 360; + + return a + diff * t; +} const pingTracker = { history: [], @@ -103,6 +142,74 @@ const pingTracker = { } }; +function updateEntityInterpolation(entityId, newData) { + const now = performance.now(); + + if (!entityInterpolationData[entityId]) { + entityInterpolationData[entityId] = { + current: JSON.parse(JSON.stringify(newData)), + target: JSON.parse(JSON.stringify(newData)), + lastUpdateTime: now + }; + return entityInterpolationData[entityId].current; + } + + entityInterpolationData[entityId].current = JSON.parse(JSON.stringify(entityInterpolationData[entityId].target)); + entityInterpolationData[entityId].target = JSON.parse(JSON.stringify(newData)); + entityInterpolationData[entityId].lastUpdateTime = now; + + return entityInterpolationData[entityId].current; +} + +function getInterpolatedEntityData(entityId) { + if (!NETWORK_SETTINGS.useInterpolation || !entityInterpolationData[entityId]) { + return null; + } + + const data = entityInterpolationData[entityId]; + const now = performance.now(); + const elapsed = now - data.lastUpdateTime; + + const pingTime = pingTracker.getAveragePing(); + const targetDuration = Math.min(200, Math.max(50, pingTime * 0.8)); + const t = Math.min(1, elapsed / targetDuration); + const easedT = t * (2 - t); + + const result = JSON.parse(JSON.stringify(data.current)); + + if (result.Player) { + if (data.current.Player && data.target.Player) { + if (data.current.Player.pos && data.target.Player.pos) { + result.Player.pos = lerpPosition( + data.current.Player.pos, + data.target.Player.pos, + easedT * NETWORK_SETTINGS.interpolationAmount + ); + } + + if (data.current.Player.yaw !== undefined && data.target.Player.yaw !== undefined) { + result.Player.yaw = lerpAngle( + data.current.Player.yaw, + data.target.Player.yaw, + easedT * NETWORK_SETTINGS.interpolationAmount + ); + } + } + } else if (result.Bomb) { + if (data.current.Bomb && data.target.Bomb) { + if (data.current.Bomb.pos && data.target.Bomb.pos) { + result.Bomb.pos = lerpPosition( + data.current.Bomb.pos, + data.target.Bomb.pos, + easedT * NETWORK_SETTINGS.interpolationAmount + ); + } + } + } + + return result; +} + function render() { requestAnimationFrame(render); @@ -124,6 +231,37 @@ function render() { renderFrame(); } +function sendRequest() { + isRequestPending = true; + pingTracker.startRequest(); + + clearTimeout(requestTimeoutTimer); + requestTimeoutTimer = setTimeout(() => { + if (isRequestPending) { + console.warn("[radarflow] Request timeout, retrying..."); + isRequestPending = false; + + if (retryCount < NETWORK_SETTINGS.maxRetries) { + retryCount++; + sendRequest(); + } else { + retryCount = 0; + console.error("[radarflow] Maximum retries reached, reconnecting..."); + reconnecting = true; + if (websocket) { + try { + websocket.close(); + } catch (e) { + } + websocket = null; + } + setTimeout(connect, NETWORK_SETTINGS.reconnectDelay); + } + } + }, NETWORK_SETTINGS.requestTimeout); + + websocket.send("requestInfo"); +} function renderFrame() { fillCanvas(); @@ -170,9 +308,14 @@ function processPlayerPositions() { let oldPlayerList = { ...playerList }; playerList = {}; - entityData.forEach(data => { + entityData.forEach((data, index) => { + const entityId = `entity_${index}`; + if (data.Player) { const player = data.Player; + if (NETWORK_SETTINGS.useInterpolation) { + updateEntityInterpolation(entityId, data); + } if (player.playerType === "Local") { localYaw = player.yaw; @@ -211,8 +354,6 @@ function processPlayerPositions() { rotationDisabledUntilRespawn = true; } } - - console.log(`[radarflow] Focused player: ${focusedPlayerName}, Position: ${focusedPlayerPos ? 'Found' : 'Not found'}, Rotation disabled: ${temporarilyDisableRotation || rotationDisabledUntilRespawn}`); } function drawImage() { @@ -314,11 +455,50 @@ function drawPlayerHealth(pos, playerType, health, hasBomb) { function drawEntities() { if (!entityData) return; - entityData.forEach(entity => { - if (entity.Bomb) { - drawBomb(entity.Bomb.pos, entity.Bomb.isPlanted); - } else if (entity.Player) { - const player = entity.Player; + const clipRect = { + x: -50, + y: -50, + width: canvas.width + 100, + height: canvas.height + 100 + }; + + entityData.forEach((entity, index) => { + const entityId = `entity_${index}`; + let interpolatedEntity = null; + + if (NETWORK_SETTINGS.useInterpolation) { + interpolatedEntity = getInterpolatedEntityData(entityId); + } + + const renderEntity = interpolatedEntity || entity; + + if (!renderEntity) return; + + let pos; + if (renderEntity.Bomb) { + pos = renderEntity.Bomb.pos; + } else if (renderEntity.Player) { + pos = renderEntity.Player.pos; + } else { + return; + } + + if (!pos) return; + + const transformed = mapAndTransformCoordinates(pos); + const mapPos = transformed.pos; + + const isVisible = mapPos.x >= clipRect.x && + mapPos.x <= clipRect.x + clipRect.width && + mapPos.y >= clipRect.y && + mapPos.y <= clipRect.y + clipRect.height; + + if (!isVisible) return; + + if (renderEntity.Bomb) { + drawBomb(renderEntity.Bomb.pos, renderEntity.Bomb.isPlanted); + } else if (renderEntity.Player) { + const player = renderEntity.Player; let fillStyle = localColor; switch (player.playerType) { @@ -715,7 +895,7 @@ function drawEntity(pos, fillStyle, dormant, hasBomb, yaw, hasAwp, playerType, i const transformed = mapAndTransformCoordinates(pos); const mapPos = transformed.pos; - const circleRadius = transformed.textSize * 0.6; + let circleRadius = transformed.textSize * 0.6; const distance = circleRadius + 2; const radius = distance + 5; const arrowWidth = 35; @@ -864,6 +1044,12 @@ function unloadMap() { function processData(data) { if (!data) return; + const now = performance.now(); + lastUpdateTime = now; + lastResponseTime = now; + connectionHealthy = true; + isRequestPending = false; + radarData = data; freq = data.freq; entityData = data.entityData; @@ -889,6 +1075,17 @@ function decompressData(data) { try { pingTracker.endRequest(); + clearTimeout(requestTimeoutTimer); + lastResponseTime = performance.now(); + connectionHealthy = true; + retryCount = 0; + + const rtt = pingTracker.getAveragePing(); + networkLatencyHistory.push(rtt); + if (networkLatencyHistory.length > 10) { + networkLatencyHistory.shift(); + } + if (data[0] === 0x01) { try { if (typeof pako === 'undefined') { @@ -917,37 +1114,53 @@ function decompressData(data) { } } catch (e) { console.error("[radarflow] Data processing error:", e); + isRequestPending = false; return null; } } function connect() { + reconnecting = true; + if (websocket == null) { console.log(`[radarflow] Connecting to ${websocketAddr}`); let socket = new WebSocket(websocketAddr); + socket.binaryType = "arraybuffer"; socket.onopen = () => { console.log("[radarflow] Connection established"); - requestAnimationFrame(render); + lastResponseTime = performance.now(); + connectionHealthy = true; + reconnecting = false; + isRequestPending = false; + retryCount = 0; + + setTimeout(() => { + socket.send(`ping:0`); + }, 500); + + if (!fpsStartTime) { + requestAnimationFrame(render); + } }; socket.onmessage = (event) => { - isRequestPending = false; - - if (event.data === "error") { - console.error("[radarflow] Server error"); + if (event.data === "pong") { + lastResponseTime = performance.now(); return; } - if (event.data instanceof Blob) { - event.data.arrayBuffer().then(buffer => { - const data = new Uint8Array(buffer); - const jsonData = decompressData(data); - if (jsonData) processData(jsonData); - }).catch(err => { - console.error("[radarflow] Buffer processing error:", err); - }); + if (event.data === "error") { + console.error("[radarflow] Server error"); + isRequestPending = false; + return; + } + + if (event.data instanceof ArrayBuffer) { + const data = new Uint8Array(event.data); + const jsonData = decompressData(data); + if (jsonData) processData(jsonData); } else if (typeof event.data === 'string') { try { const jsonData = JSON.parse(event.data); @@ -956,6 +1169,8 @@ function connect() { } else { processData(jsonData); } + + lastResponseTime = performance.now(); } catch (e) { console.error("[radarflow] JSON parse error:", e); } @@ -965,8 +1180,12 @@ function connect() { socket.onclose = (event) => { console.log("[radarflow] Connection closed"); websocket = null; - unloadMap(); - setTimeout(connect, 1000); + + if (!reconnecting) { + unloadMap(); + } + + setTimeout(connect, NETWORK_SETTINGS.reconnectDelay); }; socket.onerror = (error) => { @@ -974,6 +1193,8 @@ function connect() { }; websocket = socket; + } else { + reconnecting = false; } } @@ -1023,18 +1244,22 @@ function togglePerformanceMode() { drawMoney = false; drawHealth = false; + NETWORK_SETTINGS.interpolationAmount = 0.85; + document.getElementById("namesCheck").checked = false; document.getElementById("gunsCheck").checked = false; document.getElementById("moneyDisplay").checked = false; document.getElementById("healthCheck").checked = false; - console.log("[radarflow] Performance mode enabled"); + console.log("[radarflow] Performance mode enabled with enhanced smoothing"); } else { drawNames = document.getElementById("namesCheck").checked = true; drawGuns = document.getElementById("gunsCheck").checked = true; drawMoney = document.getElementById("moneyDisplay").checked = true; drawHealth = document.getElementById("healthCheck").checked = true; + NETWORK_SETTINGS.interpolationAmount = 0.7; + console.log("[radarflow] Performance mode disabled"); } }