Latency optimization
This commit is contained in:
parent
d10fcdf15e
commit
4846e045bb
118
src/websocket.rs
118
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);
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user