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");
     }
 }