From ffa2979da3a468a852960c664cdf64ac66275e31 Mon Sep 17 00:00:00 2001 From: Wizzard <rich@bandaholics.cash> Date: Sat, 15 Mar 2025 23:36:47 -0400 Subject: [PATCH] Add: Network compression (Attempts to fix stutter when sharing the radar) --- Cargo.lock | 4 +- Cargo.toml | 2 + src/comms.rs | 16 + src/websocket.rs | 49 ++- webradar/index.html | 6 +- webradar/script.js | 1014 +++++++++++++++++++++---------------------- 6 files changed, 556 insertions(+), 535 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb33b80..8a35693 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "abi_stable" @@ -1886,12 +1886,14 @@ dependencies = [ "clap", "dataview 1.0.1", "enum-primitive-derive", + "flate2", "itertools 0.13.0", "local-ip-address", "log", "memflow", "memflow-native", "num-traits", + "rand", "reqwest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 9b15988..ac1887d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,8 @@ local-ip-address = "0.6.3" # other itertools = "0.13.0" +flate2 = "1.0" +rand = "0.8" [build-dependencies] reqwest = { version = "0.12.9", features = ["blocking"] } diff --git a/src/comms.rs b/src/comms.rs index 67963ba..4e89a7a 100755 --- a/src/comms.rs +++ b/src/comms.rs @@ -58,6 +58,14 @@ impl PlayerData { money } } + + pub fn get_pos(&self) -> &Vec3 { + &self.pos + } + + pub fn get_player_name(&self) -> &str { + &self.player_name + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -174,6 +182,14 @@ impl RadarData { money_reveal_enabled: false } } + + pub fn get_entities(&self) -> &Vec<EntityData> { + &self.player_data + } + + pub fn set_entities(&mut self, entities: Vec<EntityData>) { + self.player_data = entities; + } } unsafe impl Send for RadarData {} diff --git a/src/websocket.rs b/src/websocket.rs index b4fd054..0ac6459 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -1,12 +1,12 @@ use std::{sync::Arc, path::PathBuf}; - use axum::{ extract::{ws::{WebSocketUpgrade, WebSocket, Message}, State}, response::Response, routing::get, Router, }; - +use flate2::{write::GzEncoder, Compression}; +use std::io::Write; use tokio::sync::RwLock; use tower_http::services::ServeDir; @@ -23,25 +23,37 @@ async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> Resp } async fn handle_socket(mut socket: WebSocket, state: AppState) { + let mut compression_buffer: Vec<u8> = Vec::with_capacity(65536); + while let Some(msg) = socket.recv().await { if let Ok(msg) = msg { if let Ok(text) = msg.to_text() { if text == "requestInfo" { - let str = { - let data = state.data_lock.read().await; + let radar_data = state.data_lock.read().await; - match serde_json::to_string(&*data) { - Ok(json) => json, - Err(e) => { - log::error!("Could not serialize data into json: {}", e.to_string()); - log::error!("Sending \"error\" instead"); - "error".to_string() - }, + if let Ok(json) = serde_json::to_string(&*radar_data) { + compression_buffer.clear(); + + let mut encoder = GzEncoder::new(Vec::new(), Compression::fast()); + if encoder.write_all(json.as_bytes()).is_ok() { + match encoder.finish() { + Ok(compressed) => { + let mut message = vec![0x01]; + message.extend_from_slice(&compressed); + + let _ = socket.send(Message::Binary(message)).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; } - }; - - if socket.send(Message::Text(str)).await.is_err() { - return; } } else if text == "toggleMoneyReveal" { let new_value = { @@ -56,13 +68,11 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) { "enabled": new_value }); - if socket.send(Message::Text(response.to_string())).await.is_err() { - return; - } + let _ = socket.send(Message::Text(response.to_string())).await; } } } else { - return; + break; } } } @@ -74,6 +84,7 @@ pub async fn run(path: PathBuf, port: u16, data_lock: Arc<RwLock<RadarData>>) -> .with_state(AppState { data_lock }); let address = format!("0.0.0.0:{}", port); + log::info!("Starting WebSocket server on {}", address); let listener = tokio::net::TcpListener::bind(address).await?; axum::serve(listener, app.into_make_service()) .await?; diff --git a/webradar/index.html b/webradar/index.html index 619e021..58ccb87 100644 --- a/webradar/index.html +++ b/webradar/index.html @@ -6,6 +6,7 @@ <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>radarflow</title> <link href="styles.css" rel="stylesheet" type="text/css" /> + <script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js"></script> </head> <body> @@ -13,6 +14,10 @@ <button id="showMenuBtn" onclick="toggleMenu(true)" style="display: none;">Show Menu</button> <div id="settingsHolder"> <div class="settings"> + <div> + <input type="checkbox" onclick="togglePerformanceMode()" id="performanceMode" /> + <label for="performanceMode">Performance Mode</label> + </div> <div> <input type="checkbox" onclick="toggleZoom()" id="zoomCheck" name="zoom" /> <label for="zoomCheck">Zoom</label> @@ -65,5 +70,4 @@ <script src="webstuff.js"></script> </body> - </html> \ No newline at end of file diff --git a/webradar/script.js b/webradar/script.js index 47d4e2e..8ec307c 100644 --- a/webradar/script.js +++ b/webradar/script.js @@ -6,412 +6,158 @@ const bombColor = "#eda338" const textColor = "#d1d1d1" // Settings -shouldZoom = false -rotateMap = true -playerCentered = true +let shouldZoom = false; +let rotateMap = true; +let playerCentered = true; -drawStats = true -drawNames = true -drawGuns = true -drawMoney = true +let drawStats = true; +let drawNames = true; +let drawGuns = true; +let drawMoney = true; + +let isRequestPending = false; +let frameCounter = 0; +let fpsStartTime = 0; +let currentFps = 0; let focusedPlayerYaw = 0; +let focusedPlayerName = "YOU"; +let focusedPlayerPos = null; +let playerList = {}; // Common -canvas = null -ctx = null - -let focusedPlayerName = "YOU" -let focusedPlayerPos = null -let playerList = {} +let canvas = null; +let ctx = null; // radarflow specific -radarData = null -freq = 0 -image = null -map = null -mapName = null -loaded = false -entityData = null -update = false -localYaw = 0 -localPlayerPos = null +let radarData = null; +let freq = 0; +let image = null; +let map = null; +let mapName = null; +let loaded = false; +let entityData = null; +let update = false; +let localYaw = 0; +let localPlayerPos = null; /// Radarflow zoom in -zoomSet = false -safetyBound = 50 -boundingRect = null +let zoomSet = false; +let safetyBound = 50; +let boundingRect = null; // Weapon IDs const weaponIdMap = { - 1: "DEAGLE", - 2: "DUALIES", - 3: "FIVE-SEVEN", - 4: "GLOCK", - 7: "AK-47", - 8: "AUG", - 9: "AWP", - 10: "FAMAS", - 11: "G3SG1", - 13: "GALIL", - 14: "M249", - 16: "M4A4", - 17: "MAC-10", - 19: "P90", - 23: "MP5", - 24: "UMP", - 25: "XM1014", - 26: "BIZON", - 27: "MAG-7", - 28: "NEGEV", - 29: "SAWED-OFF", - 30: "TEC-9", - 31: "ZEUS", - 32: "P2000", - 33: "MP7", - 34: "MP9", - 35: "NOVA", - 36: "P250", - 38: "SCAR-20", - 39: "SG 553", - 40: "SCOUT", - 60: "M4A1-S", - 61: "USP-S", - 63: "CZ75", - 64: "REVOLVER", - 43: "FLASH", - 44: "HE", - 45: "SMOKE", - 46: "MOLOTOV", - 47: "DECOY", - 48: "INCENDIARY", - 49: "C4", - 0: "KNIFE" + 1: "DEAGLE", 2: "DUALIES", 3: "FIVE-SEVEN", 4: "GLOCK", 7: "AK-47", + 8: "AUG", 9: "AWP", 10: "FAMAS", 11: "G3SG1", 13: "GALIL", 14: "M249", + 16: "M4A4", 17: "MAC-10", 19: "P90", 23: "MP5", 24: "UMP", 25: "XM1014", + 26: "BIZON", 27: "MAG-7", 28: "NEGEV", 29: "SAWED-OFF", 30: "TEC-9", + 31: "ZEUS", 32: "P2000", 33: "MP7", 34: "MP9", 35: "NOVA", 36: "P250", + 38: "SCAR-20", 39: "SG 553", 40: "SCOUT", 60: "M4A1-S", 61: "USP-S", + 63: "CZ75", 64: "REVOLVER", 43: "FLASH", 44: "HE", 45: "SMOKE", 46: "MOLOTOV", + 47: "DECOY", 48: "INCENDIARY", 49: "C4", 0: "KNIFE" }; -// networking -websocket = null -if (location.protocol == 'https:') { - websocketAddr = `wss://${window.location.host}/ws` -} else { - websocketAddr = `ws://${window.location.host}/ws` -} -//websocketAddr = "ws://192.168.0.235:8000/ws" +// Networking +let websocket = null; +const websocketAddr = location.protocol === 'https:' + ? `wss://${window.location.host}/ws` + : `ws://${window.location.host}/ws`; // Util functions const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const degreesToRadians = (degrees) => degrees * (Math.PI / 180); -function mapCoordinates(coordinates) { - let offset_x = coordinates.x - map.pos_x; - let offset_y = coordinates.y - map.pos_y; - - offset_x /= map.scale; - offset_y /= -map.scale; - - return { x: offset_x, y: offset_y }; -} - -function boundingCoordinates(coordinates, boundingRect) { - const xScale = boundingRect.width / image.width; - const yScale = boundingRect.height / image.height; - - const newX = (coordinates.x - boundingRect.x) / xScale; - const newY = (coordinates.y - boundingRect.y) / yScale; - - return { x: newX, y: newY }; -} - -function boundingScale(value, boundingRect) { - const scale = image.width / boundingRect.width; - return value * scale; -} - -function rotatePoint(cx, cy, x, y, angle) { - const radians = degreesToRadians(angle); - const cos = Math.cos(radians); - const sin = Math.sin(radians); - - const nx = x - cx; - const ny = y - cy; - - const rx = nx * cos - ny * sin; - const ry = nx * sin + ny * cos; - - return { - x: rx + cx, - y: ry + cy - }; -} - -function makeBoundingRect(x1, y1, x2, y2, aspectRatio) { - const topLeftX = x1; - const topLeftY = y1; - const bottomRightX = x2; - const bottomRightY = y2; - - const width = bottomRightX - topLeftX; - const height = bottomRightY - topLeftY; - - let newWidth, newHeight; - if (width / height > aspectRatio) { - // Wider rectangle - newHeight = width / aspectRatio; - newWidth = width; - } else { - // Taller rectangle - newWidth = height * aspectRatio; - newHeight = height; - } - - const centerX = (topLeftX + bottomRightX) / 2; - const centerY = (topLeftY + bottomRightY) / 2; - - const rectMinX = centerX - newWidth / 2; - const rectMaxX = centerX + newWidth / 2; - const rectMinY = centerY - newHeight / 2; - const rectMaxY = centerY + newHeight / 2; - - return { - x: rectMinX, - y: rectMinY, - width: rectMaxX - rectMinX, - height: rectMaxY - rectMinY, - }; -} - function render() { - if (update) { - fillCanvas(); - if (loaded) { - update = false; + requestAnimationFrame(render); - localPlayerPos = null; - focusedPlayerPos = null; - focusedPlayerYaw = 0; + const now = performance.now(); + if (!fpsStartTime) fpsStartTime = now; + frameCounter++; - if (entityData != null) { - playerList = {}; - - entityData.forEach((data) => { - if (data.Player !== undefined) { - if (data.Player.playerType === "Local") { - localYaw = data.Player.yaw; - localPlayerPos = data.Player.pos; - - playerList["YOU"] = { - pos: data.Player.pos, - yaw: data.Player.yaw - }; - } else { - playerList[data.Player.playerName] = { - pos: data.Player.pos, - yaw: data.Player.yaw - }; - } - - if (data.Player.playerName === focusedPlayerName || - (focusedPlayerName === "YOU" && data.Player.playerType === "Local")) { - focusedPlayerPos = data.Player.pos; - focusedPlayerYaw = data.Player.yaw; - } - } - }); - - updatePlayerDropdown(); - } - - if (entityData != null && map != null && image != null && shouldZoom && !playerCentered) { - let minX = Infinity; - let minY = Infinity; - let maxX = -Infinity; - let maxY = -Infinity; - - entityData.forEach((data) => { - let mapCords = null; - - if (data.Bomb !== undefined) { - mapCords = mapCoordinates(data.Bomb.pos); - } else { - mapCords = mapCoordinates(data.Player.pos); - } - - minX = Math.min(minX, mapCords.x); - minY = Math.min(minY, mapCords.y); - maxX = Math.max(maxX, mapCords.x); - maxY = Math.max(maxY, mapCords.y); - }); - - boundingRect = makeBoundingRect(minX - safetyBound, minY - safetyBound, maxX + safetyBound, maxY + safetyBound, image.width / image.height); - zoomSet = true; - } else if (zoomSet && !playerCentered) { - zoomSet = false; - } - - drawImage(); - - if (entityData != null) { - entityData.forEach((data) => { - if (data.Bomb !== undefined) { - drawBomb(data.Bomb.pos, data.Bomb.isPlanted); - } else { - let fillStyle = localColor; - - switch (data.Player.playerType) { - case "Team": - fillStyle = teamColor; - break; - - case "Enemy": - fillStyle = enemyColor; - break; - } - - drawEntity( - data.Player.pos, - fillStyle, - data.Player.isDormant, - data.Player.hasBomb, - data.Player.yaw, - data.Player.hasAwp, - data.Player.playerType, - data.Player.isScoped, - data.Player.playerName, - false, - data.Player.weaponId - ); - - if (drawNames && !data.Player.isDormant) { - drawPlayerName( - data.Player.pos, - data.Player.playerName, - data.Player.playerType, - data.Player.hasAwp, - data.Player.hasBomb, - data.Player.isScoped - ); - } - - if (drawGuns && !data.Player.isDormant) { - drawPlayerWeapon( - data.Player.pos, - data.Player.playerType, - data.Player.weaponId - ); - } - - if (data.Player.hasBomb && !data.Player.isDormant) { - drawPlayerBomb( - data.Player.pos, - data.Player.playerType - ); - } - - if (drawMoney && !data.Player.isDormant && typeof data.Player.money === 'number') { - drawPlayerMoney( - data.Player.pos, - data.Player.playerType, - data.Player.money, - data.Player.hasBomb - ); - } - } - }); - } - - if (radarData != null) { - if (radarData.bombPlanted && !radarData.bombExploded && radarData.bombDefuseTimeleft >= 0) { - let maxWidth = 1024 - 128 - 128; - let timeleft = radarData.bombDefuseTimeleft; - - // Base bar - ctx.fillStyle = "black"; - ctx.fillRect(128, 16, maxWidth, 16); - - // Bomb timer - if (radarData.bombBeingDefused) { - if (radarData.bombCanDefuse) { - ctx.fillStyle = teamColor; - } else { - ctx.fillStyle = enemyColor; - } - } else { - ctx.fillStyle = bombColor; - } - - ctx.fillRect(130, 18, (maxWidth - 2) * (timeleft / 40), 12); - - ctx.font = "24px Arial"; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillStyle = textColor; - ctx.fillText(`${timeleft.toFixed(1)}s`, 1024 / 2, 28 + 24); - - // Defuse time lines - ctx.strokeStyle = "black"; - ctx.lineWidth = 2; - - // Kit defuse - ctx.beginPath(); - ctx.moveTo(128 + (maxWidth * (5 / 40)), 16); - ctx.lineTo(128 + (maxWidth * (5 / 40)), 32); - ctx.stroke(); - - // Normal defuse - ctx.beginPath(); - ctx.moveTo(130 + (maxWidth - 2) * (10 / 40), 16); - ctx.lineTo(130 + (maxWidth - 2) * (10 / 40), 32); - ctx.stroke(); - - // Defuse stamp line - if (radarData.bombCanDefuse) { - ctx.strokeStyle = "green"; - ctx.beginPath(); - ctx.moveTo(130 + (maxWidth - 2) * (radarData.bombDefuseEnd / 40), 16); - ctx.lineTo(130 + (maxWidth - 2) * (radarData.bombDefuseEnd / 40), 32); - ctx.stroke(); - } - } - } - } else { - if (websocket != null) { - ctx.font = "100px Arial"; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillStyle = textColor; - ctx.fillText("Not on a server", 1024 / 2, 1024 / 2); - } else { - ctx.font = "100px Arial"; - ctx.textAlign = "center"; - ctx.fillStyle = textColor; - ctx.fillText("Disconnected", 1024 / 2, 1024 / 2); - } - } - - if (drawStats) { - ctx.font = "16px Arial"; - ctx.textAlign = "left"; - ctx.fillStyle = textColor; - ctx.lineWidth = 2; - ctx.strokeStyle = "black"; - ctx.strokeText(`${freq} Hz`, 2, 18); - ctx.fillText(`${freq} Hz`, 2, 18); - } + if (now - fpsStartTime > 1000) { + currentFps = Math.round(frameCounter * 1000 / (now - fpsStartTime)); + frameCounter = 0; + fpsStartTime = now; } - if (websocket != null) { + if (!isRequestPending && websocket && websocket.readyState === WebSocket.OPEN) { + isRequestPending = true; websocket.send("requestInfo"); } + + renderFrame(); } -function fillCanvas() { - ctx.fillStyle = "#0f0f0f"; - ctx.fillRect(0, 0, canvas.width, canvas.height); +function renderFrame() { + fillCanvas(); + + if (entityData && loaded && map && image) { + processPlayerPositions(); + + if (update) { + updatePlayerDropdown(); + update = false; + } + + drawImage(); + + drawEntities(); + + drawBombTimer(); + } else if (!loaded) { + ctx.font = "40px Arial"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillStyle = textColor; + ctx.fillText(websocket ? "Not on server" : "Disconnected", canvas.width / 2, canvas.height / 2); + } + + if (drawStats) { + ctx.font = "16px Arial"; + ctx.textAlign = "left"; + ctx.fillStyle = "#00FF00"; + ctx.fillText(`${currentFps} FPS | ${freq} Hz`, 10, 20); + } +} + +function processPlayerPositions() { + if (!entityData) return; + + localPlayerPos = null; + focusedPlayerPos = null; + focusedPlayerYaw = 0; + playerList = {}; + + entityData.forEach(data => { + if (data.Player) { + const player = data.Player; + + if (player.playerType === "Local") { + localYaw = player.yaw; + localPlayerPos = player.pos; + playerList["YOU"] = { + pos: player.pos, + yaw: player.yaw + }; + } else { + playerList[player.playerName] = { + pos: player.pos, + yaw: player.yaw + }; + } + + if (player.playerName === focusedPlayerName || + (focusedPlayerName === "YOU" && player.playerType === "Local")) { + focusedPlayerPos = player.pos; + focusedPlayerYaw = player.yaw; + } + } + }); } function drawImage() { - if (image == null || canvas == null) - return + if (!image || !canvas || !map) return; ctx.save(); @@ -422,29 +168,160 @@ function drawImage() { } if (playerCentered && focusedPlayerPos) { - const playerMapPos = mapCoordinates(focusedPlayerPos); + const playerX = (focusedPlayerPos.x - map.pos_x) / map.scale; + const playerY = (focusedPlayerPos.y - map.pos_y) / -map.scale; + const zoomLevel = 0.5; const viewWidth = image.width * zoomLevel; const viewHeight = image.height * zoomLevel; - const viewX = playerMapPos.x - (viewWidth / 2); - const viewY = playerMapPos.y - (viewHeight / 2); ctx.drawImage( image, - viewX, viewY, viewWidth, viewHeight, + playerX - (viewWidth / 2), playerY - (viewHeight / 2), viewWidth, viewHeight, + 0, 0, canvas.width, canvas.height + ); + } else if (zoomSet && boundingRect?.x != null) { + ctx.drawImage( + image, + boundingRect.x, boundingRect.y, boundingRect.width, boundingRect.height, 0, 0, canvas.width, canvas.height ); - } else if (zoomSet != false && boundingRect.x != null) { - ctx.drawImage(image, boundingRect.x, boundingRect.y, boundingRect.width, boundingRect.height, 0, 0, canvas.width, canvas.height) } else { - ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height) + ctx.drawImage( + image, + 0, 0, image.width, image.height, + 0, 0, canvas.width, canvas.height + ); } ctx.restore(); } +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; + let fillStyle = localColor; + + switch (player.playerType) { + case "Team": fillStyle = teamColor; break; + case "Enemy": fillStyle = enemyColor; break; + } + + drawEntity( + player.pos, + fillStyle, + player.isDormant, + player.hasBomb, + player.yaw, + player.hasAwp, + player.playerType, + player.isScoped, + player.playerName, + false, + player.weaponId + ); + + if (!player.isDormant) { + if (drawNames) { + drawPlayerName( + player.pos, + player.playerName, + player.playerType, + player.hasAwp, + player.hasBomb, + player.isScoped + ); + } + + if (drawGuns) { + drawPlayerWeapon( + player.pos, + player.playerType, + player.weaponId + ); + } + + if (player.hasBomb) { + drawPlayerBomb( + player.pos, + player.playerType + ); + } + + if (drawMoney && typeof player.money === 'number') { + drawPlayerMoney( + player.pos, + player.playerType, + player.money, + player.hasBomb + ); + } + } + } + }); +} + +function drawBombTimer() { + if (!radarData || !radarData.bombPlanted || radarData.bombExploded || radarData.bombDefuseTimeleft < 0) { + return; + } + + const maxWidth = 1024 - 128 - 128; + const timeleft = radarData.bombDefuseTimeleft; + + ctx.fillStyle = "black"; + ctx.fillRect(128, 16, maxWidth, 16); + + if (radarData.bombBeingDefused) { + ctx.fillStyle = radarData.bombCanDefuse ? teamColor : enemyColor; + } else { + ctx.fillStyle = bombColor; + } + + ctx.fillRect(130, 18, (maxWidth - 2) * (timeleft / 40), 12); + + ctx.font = "24px Arial"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillStyle = textColor; + ctx.fillText(`${timeleft.toFixed(1)}s`, 1024 / 2, 28 + 24); + + ctx.strokeStyle = "black"; + ctx.lineWidth = 2; + + ctx.beginPath(); + ctx.moveTo(128 + (maxWidth * (5 / 40)), 16); + ctx.lineTo(128 + (maxWidth * (5 / 40)), 32); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(130 + (maxWidth - 2) * (10 / 40), 16); + ctx.lineTo(130 + (maxWidth - 2) * (10 / 40), 32); + ctx.stroke(); + + if (radarData.bombCanDefuse) { + ctx.strokeStyle = "green"; + ctx.beginPath(); + ctx.moveTo(130 + (maxWidth - 2) * (radarData.bombDefuseEnd / 40), 16); + ctx.lineTo(130 + (maxWidth - 2) * (radarData.bombDefuseEnd / 40), 32); + ctx.stroke(); + } +} + +function fillCanvas() { + ctx.fillStyle = "#0f0f0f"; + ctx.fillRect(0, 0, canvas.width, canvas.height); +} + function updatePlayerDropdown() { const dropdown = document.getElementById('playerSelect'); + if (!dropdown) return; + const currentValue = dropdown.value; while (dropdown.options.length > 1) { @@ -474,39 +351,84 @@ function updatePlayerDropdown() { function changePlayerFocus() { const dropdown = document.getElementById('playerSelect'); - if (dropdown.value === "local") { - focusedPlayerName = "YOU"; - } else { - focusedPlayerName = dropdown.value; - } - console.log(`[radarflow] Changed focus to player: ${focusedPlayerName}`); + focusedPlayerName = dropdown.value === "local" ? "YOU" : dropdown.value; update = true; } -function mapAndTransformCoordinates(pos) { - let mapPos = mapCoordinates(pos); - let textSize = 12; - - if (zoomSet) { - mapPos = boundingCoordinates(mapPos, boundingRect); - textSize = boundingScale(12, boundingRect); - } else if (playerCentered && focusedPlayerPos) { - const playerMapPos = mapCoordinates(focusedPlayerPos); - const zoomLevel = 0.5; - const viewWidth = image.width * zoomLevel; - const viewHeight = image.height * zoomLevel; - - mapPos.x = (mapPos.x - (playerMapPos.x - viewWidth / 2)) * canvas.width / viewWidth; - mapPos.y = (mapPos.y - (playerMapPos.y - viewHeight / 2)) * canvas.height / viewHeight; - } else { - mapPos.x = mapPos.x * canvas.width / image.width; - mapPos.y = mapPos.y * canvas.height / image.height; +function mapCoordinates(coordinates) { + if (!map || !coordinates) { + return { x: 0, y: 0 }; } - if (rotateMap) { - const canvasCenter = { x: canvas.width / 2, y: canvas.height / 2 }; + const offset_x = (coordinates.x - map.pos_x) / map.scale; + const offset_y = (coordinates.y - map.pos_y) / -map.scale; + + return { x: offset_x, y: offset_y }; +} + +function mapAndTransformCoordinates(pos) { + const canvasWidth = canvas.width; + const canvasHeight = canvas.height; + const imageWidth = image ? image.width : 1; + const imageHeight = image ? image.height : 1; + + if (!map || !pos) return { pos: { x: 0, y: 0 }, textSize: 12 }; + + const offset_x = (pos.x - map.pos_x) / map.scale; + const offset_y = (pos.y - map.pos_y) / -map.scale; + + let mapPos = { x: offset_x, y: offset_y }; + let textSize = 12; + + if (zoomSet && boundingRect && boundingRect.x != null) { + const xScale = boundingRect.width / imageWidth; + const yScale = boundingRect.height / imageHeight; + mapPos = { + x: (mapPos.x - boundingRect.x) / xScale, + y: (mapPos.y - boundingRect.y) / yScale + }; + textSize = (imageWidth / boundingRect.width) * 12; + } + else if (playerCentered && focusedPlayerPos) { + const zoomLevel = 0.5; + const viewWidth = imageWidth * zoomLevel; + const viewHeight = imageHeight * zoomLevel; + + let playerMapPos; + if (focusedPlayerName === "YOU" && localPlayerPos) { + const lpx = (localPlayerPos.x - map.pos_x) / map.scale; + const lpy = (localPlayerPos.y - map.pos_y) / -map.scale; + playerMapPos = { x: lpx, y: lpy }; + } else if (focusedPlayerPos) { + const fpx = (focusedPlayerPos.x - map.pos_x) / map.scale; + const fpy = (focusedPlayerPos.y - map.pos_y) / -map.scale; + playerMapPos = { x: fpx, y: fpy }; + } else { + playerMapPos = { x: 0, y: 0 }; + } + + mapPos.x = (mapPos.x - (playerMapPos.x - viewWidth / 2)) * canvasWidth / viewWidth; + mapPos.y = (mapPos.y - (playerMapPos.y - viewHeight / 2)) * canvasHeight / viewHeight; + } + else { + mapPos.x = mapPos.x * canvasWidth / imageWidth; + mapPos.y = mapPos.y * canvasHeight / imageHeight; + } + + if (rotateMap && typeof focusedPlayerYaw === 'number') { + const canvasCenter = { x: canvasWidth / 2, y: canvasHeight / 2 }; const rotationYaw = focusedPlayerName === "YOU" ? localYaw : focusedPlayerYaw; - mapPos = rotatePoint(canvasCenter.x, canvasCenter.y, mapPos.x, mapPos.y, rotationYaw + 270); + const angle = rotationYaw + 270; + + const radians = angle * (Math.PI / 180); + const cos = Math.cos(radians); + const sin = Math.sin(radians); + + const nx = mapPos.x - canvasCenter.x; + const ny = mapPos.y - canvasCenter.y; + + mapPos.x = nx * cos - ny * sin + canvasCenter.x; + mapPos.y = nx * sin + ny * cos + canvasCenter.y; } return { pos: mapPos, textSize: textSize }; @@ -627,8 +549,7 @@ function drawPlayerBomb(pos, playerType) { } function drawBomb(pos, planted) { - if (map == null) - return + if (!map) return; const transformed = mapAndTransformCoordinates(pos); const mapPos = transformed.pos; @@ -661,8 +582,7 @@ function drawBomb(pos, planted) { } function drawEntity(pos, fillStyle, dormant, hasBomb, yaw, hasAwp, playerType, isScoped, playerName, isPlanted, weaponId) { - if (map == null) - return + if (!map) return; const transformed = mapAndTransformCoordinates(pos); const mapPos = transformed.pos; @@ -710,20 +630,6 @@ function drawEntity(pos, fillStyle, dormant, hasBomb, yaw, hasAwp, playerType, i ctx.arc(mapPos.x, mapPos.y, circleRadius, 0, 2 * Math.PI); ctx.fillStyle = fillStyle; ctx.fill(); - - if (hasAwp && false) { - let style = "yellow"; - - if (playerType == "Enemy") { - style = "orange"; - } - - ctx.beginPath(); - ctx.arc(mapPos.x, mapPos.y, circleRadius / 1.5, 0, 2 * Math.PI); - ctx.fillStyle = style; - ctx.fill(); - } - ctx.closePath(); const arrowHeadX = mapPos.x + radius * Math.cos(adjustedYaw * (Math.PI / 180)); @@ -756,11 +662,7 @@ function drawEntity(pos, fillStyle, dormant, hasBomb, yaw, hasAwp, playerType, i ctx.moveTo(arrowHeadX, arrowHeadY); ctx.lineTo(lineOfSightX, lineOfSightY); - if (playerType == "Enemy") - ctx.strokeStyle = enemyColor; - else - ctx.strokeStyle = teamColor; - + ctx.strokeStyle = playerType == "Enemy" ? enemyColor : teamColor; ctx.lineWidth = 1; ctx.stroke(); } @@ -780,133 +682,165 @@ function getWeaponName(weaponId) { } function loadMap(mapName) { - console.log(`[radarflow] loading map ${mapName}`); - loaded = true; - const map_img = new Image(); - map_img.src = `assets/image/${mapName}_radar_psd.png`; + if (!mapName) return; - fetch(`assets/json/${mapName}.json`) - .then(response => response.json()) + console.log(`[radarflow] Loading map "${mapName}"`); + loaded = true; + + // Load JSON data + const jsonPath = `assets/json/${mapName}.json`; + fetch(jsonPath) + .then(response => { + if (!response.ok) throw new Error(`JSON not found: ${response.status}`); + return response.json(); + }) .then(data => { + console.log("[radarflow] Map data loaded"); map = data; + update = true; }) .catch(error => { - console.error('Error loading JSON file:', error); + console.error(`[radarflow] Error loading JSON: ${error}`); }); + const imagePath = `assets/image/${mapName}_radar_psd.png`; + const map_img = new Image(); + map_img.onload = () => { + console.log("[radarflow] Map image loaded"); image = map_img; update = true; }; + + map_img.onerror = (e) => { + console.error(`[radarflow] Error loading image: ${e}`); + }; + + map_img.src = imagePath; } function unloadMap() { - console.log("[radarflow] unloading map"); ctx.clearRect(0, 0, canvas.width, canvas.height); map = null; mapName = null; - loaded = false, + loaded = false; update = true; - requestAnimationFrame(render); +} + +function processData(data) { + if (!data) return; + + radarData = data; + freq = data.freq; + entityData = data.entityData; + + if (data.money_reveal_enabled !== undefined) { + const checkbox = document.getElementById("moneyReveal"); + if (checkbox) checkbox.checked = data.money_reveal_enabled; + } + + if (data.ingame === false) { + if (loaded) unloadMap(); + } else { + if (!loaded && data.mapName) { + mapName = data.mapName; + loadMap(mapName); + } + } + + update = true; +} + +function decompressData(data) { + try { + if (data[0] === 0x01) { + try { + if (typeof pako === 'undefined') { + console.error("[radarflow] Pako library not available"); + return null; + } + + const decompressed = pako.inflate(data.slice(1)); + const text = new TextDecoder().decode(decompressed); + return JSON.parse(text); + } catch (e) { + console.error("[radarflow] Decompression error:", e); + return null; + } + } else if (data[0] === 0x00) { + try { + const text = new TextDecoder().decode(data.slice(1)); + return JSON.parse(text); + } catch (e) { + console.error("[radarflow] Parse error:", e); + return null; + } + } else { + console.error("[radarflow] Unknown data format"); + return null; + } + } catch (e) { + console.error("[radarflow] Data processing error:", e); + return null; + } } function connect() { if (websocket == null) { + console.log(`[radarflow] Connecting to ${websocketAddr}`); + let socket = new WebSocket(websocketAddr); socket.onopen = () => { console.log("[radarflow] Connection established"); - websocket.send("requestInfo"); + requestAnimationFrame(render); }; socket.onmessage = (event) => { - if (event.data == "error") { - console.log("[radarflow] Server had an unknown error"); - } else { + isRequestPending = false; + + if (event.data === "error") { + console.error("[radarflow] Server error"); + 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); + }); + } else if (typeof event.data === 'string') { try { - let data = JSON.parse(event.data); - radarData = data; - freq = data.freq; - - if (data.money_reveal_enabled !== undefined) { - document.getElementById("moneyReveal").checked = data.money_reveal_enabled; - } - - if (data.ingame == false) { - mapName = null; - entityData = null; - - if (loaded) - unloadMap(); + const jsonData = JSON.parse(event.data); + if (jsonData.action === "toggleMoneyReveal") { + document.getElementById("moneyReveal").checked = jsonData.enabled; } else { - if (!loaded) { - mapName = data.mapName; - entityData = data.entityData; - loadMap(mapName); - } else { - entityData = data.entityData; - } + processData(jsonData); } - - update = true; - requestAnimationFrame(render); } catch (e) { - console.error("[radarflow] Error parsing server message:", e, event.data); + console.error("[radarflow] JSON parse error:", e); } } }; socket.onclose = (event) => { - if (event.wasClean) { - console.log("[radarflow] connection closed"); - } else { - console.log("[radarflow] connection died"); - } - - playerData = null; + console.log("[radarflow] Connection closed"); websocket = null; unloadMap(); - - setTimeout(function () { - connect(); - }, 1000); + setTimeout(connect, 1000); }; socket.onerror = (error) => { - console.log(`[radarflow] websocket error: ${error}`); + console.error("[radarflow] WebSocket error:", error); }; websocket = socket; - } else { - setTimeout(() => { - connect(); - }, 1000); } } -addEventListener("DOMContentLoaded", (e) => { - const savedDrawMoney = localStorage.getItem('drawMoney'); - drawMoney = savedDrawMoney !== null ? savedDrawMoney === 'true' : true; - - document.getElementById("zoomCheck").checked = false; - document.getElementById("statsCheck").checked = true; - document.getElementById("namesCheck").checked = true; - document.getElementById("gunsCheck").checked = true; - document.getElementById("moneyDisplay").checked = drawMoney; - document.getElementById("moneyReveal").checked = false; - document.getElementById("rotateCheck").checked = true; - document.getElementById("centerCheck").checked = true; - - canvas = document.getElementById('canvas'); - canvas.width = 1024; - canvas.height = 1024; - canvasAspectRatio = canvas.width / canvas.height; - ctx = canvas.getContext('2d'); - - console.log(`[radarflow] connecting to ${websocketAddr}`); - connect(); -}); - function toggleZoom() { shouldZoom = !shouldZoom; } @@ -933,6 +867,7 @@ function toggleCentered() { function toggleMoneyReveal() { if (websocket && websocket.readyState === WebSocket.OPEN) { + console.log("[radarflow] Sending toggleMoneyReveal command"); websocket.send("toggleMoneyReveal"); } } @@ -940,8 +875,59 @@ function toggleMoneyReveal() { function toggleDisplayMoney() { drawMoney = !drawMoney; update = true; - - console.log("[radarflow] Money display toggled:", drawMoney); - localStorage.setItem('drawMoney', drawMoney ? 'true' : 'false'); -} \ No newline at end of file +} + +function togglePerformanceMode() { + const performanceMode = document.getElementById('performanceMode').checked; + + if (performanceMode) { + drawNames = false; + drawGuns = false; + drawMoney = false; + + document.getElementById("namesCheck").checked = false; + document.getElementById("gunsCheck").checked = false; + document.getElementById("moneyDisplay").checked = false; + + console.log("[radarflow] Performance mode enabled"); + } else { + drawNames = document.getElementById("namesCheck").checked = true; + drawGuns = document.getElementById("gunsCheck").checked = true; + drawMoney = document.getElementById("moneyDisplay").checked = true; + + console.log("[radarflow] Performance mode disabled"); + } +} + +addEventListener("DOMContentLoaded", () => { + const savedDrawMoney = localStorage.getItem('drawMoney'); + drawMoney = savedDrawMoney !== null ? savedDrawMoney === 'true' : true; + + const checkboxes = { + "zoomCheck": false, + "statsCheck": true, + "namesCheck": true, + "gunsCheck": true, + "moneyDisplay": drawMoney, + "moneyReveal": false, + "rotateCheck": true, + "centerCheck": true + }; + + Object.entries(checkboxes).forEach(([id, state]) => { + const checkbox = document.getElementById(id); + if (checkbox) checkbox.checked = state; + }); + + canvas = document.getElementById('canvas'); + if (canvas) { + canvas.width = 1024; + canvas.height = 1024; + ctx = canvas.getContext('2d'); + + connect(); + } else { + console.error("[radarflow] Canvas element not found"); + } +}); \ No newline at end of file