From 9a1addcd1fd596df21b155263110e97add9903ab Mon Sep 17 00:00:00 2001 From: pommicket Date: Thu, 8 Aug 2024 20:52:44 -0400 Subject: better position synchronization --- game.js | 34 ++++++++++++++++++++++++++++------ server/src/main.rs | 38 ++++++++++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/game.js b/game.js index 27c0971..811f23c 100644 --- a/game.js +++ b/game.js @@ -17,6 +17,7 @@ window.addEventListener('load', function () { let nibSize = 12; let pieceWidth = 70; let pieceHeight; + let receivedAck = true; document.body.style.setProperty('--image', `url("${imageUrl}")`);// TODO : escaping const image = new Image(); const draggingPieceLastPos = Object.preventExtensions({x: null, y: null}); @@ -177,6 +178,7 @@ window.addEventListener('load', function () { element; nibTypes; connectedComponent; + upToDateWithServer; getClipPath() { const nibTypes = this.nibTypes; let shoulderWidth = (pieceWidth - nibSize) / 2; @@ -211,6 +213,7 @@ window.addEventListener('load', function () { this.y = y; this.u = u; this.v = v; + this.upToDateWithServer = true; this.connectedComponent = [this]; const element = this.element = document.createElement('div'); element.classList.add('piece'); @@ -264,11 +267,6 @@ window.addEventListener('load', function () { if (draggingPiece) { let anyConnected = false; for (const piece of draggingPiece.connectedComponent) { - const canonicalPos = screenPosToCanonical({ - x: piece.x, - y: piece.y, - }); - socket.send(`move ${piece.id} ${canonicalPos.x} ${canonicalPos.y}`); if (solved) break; const col = piece.col(); const row = piece.row(); @@ -312,6 +310,7 @@ window.addEventListener('load', function () { piece.x += dx; piece.y += dy; piece.updatePosition(); + piece.upToDateWithServer = false; } draggingPieceLastPos.x = e.clientX; draggingPieceLastPos.y = e.clientY; @@ -403,6 +402,7 @@ window.addEventListener('load', function () { // only udpate the position of one piece per equivalence class mod is-connected-to if (connectivity[i] !== i) continue; const piece = pieces[i]; + if (!piece.upToDateWithServer) continue; if (draggingPiece && draggingPiece.connectedComponent === piece.connectedComponent) continue; const newPos = canonicalToScreenPos({x: piecePositions[2*i], y: piecePositions[2*i+1]}); const diff = [newPos.x - piece.x, newPos.y - piece.y]; @@ -413,7 +413,23 @@ window.addEventListener('load', function () { piece.updatePosition(); } } - + function sendServerUpdate() { + // send update to server + if (!receivedAck) return; // last update hasn't been acknowledged yet + const motions = []; + for (const piece of pieces) { + if (piece.upToDateWithServer) continue; + const canonicalPos = screenPosToCanonical({ + x: piece.x, + y: piece.y, + }); + motions.push(`move ${piece.id} ${canonicalPos.x} ${canonicalPos.y}`); + } + if (motions.length) { + receivedAck = false; + socket.send(motions.join('\n')); + } + } socket.addEventListener('open', () => { if (joinPuzzle) { socket.send(`join ${joinPuzzle}`); @@ -421,12 +437,18 @@ window.addEventListener('load', function () { socket.send(`new ${puzzleWidth} ${puzzleHeight} ${imageUrl}`); } setInterval(() => socket.send('poll'), 1000); + setInterval(sendServerUpdate, 1000); }); socket.addEventListener('message', (e) => { if (typeof e.data === 'string') { if (e.data.startsWith('id: ')) { let puzzleID = e.data.split(' ')[1]; console.log('ID:', puzzleID); + } else if (e.data === 'ack') { + for (const piece of pieces) { + piece.upToDateWithServer = true; + } + receivedAck = true; } } else { const opcode = new Uint8Array(e.data, 0, 1)[0]; diff --git a/server/src/main.rs b/server/src/main.rs index 1fac1e5..32220a9 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -116,25 +116,43 @@ async fn handle_connection(database: &Database, conn: &mut tokio::net::TcpStream puzzle_id = Some(id); let info = get_puzzle_info(&database, &id)?; ws.send(Message::Binary(info)).await?; - } else if let Some(data) = text.strip_prefix("move ") { - let mut parts = data.split(' '); + } else if text.starts_with("move ") { let puzzle_id = puzzle_id.ok_or_else(|| anyhow!("move without puzzle ID"))?; - let piece: usize = parts.next().ok_or_else(|| anyhow!("bad syntax"))?.parse()?; - let x: f32 = parts.next().ok_or_else(|| anyhow!("bad syntax"))?.parse()?; - let y: f32 = parts.next().ok_or_else(|| anyhow!("bad syntax"))?.parse()?; + #[derive(Clone, Copy)] + struct Motion { + piece: usize, + x: f32, + y: f32, + } + let mut motions = vec![]; + for line in text.split('\n') { + let mut parts = line.split(' '); + parts.next(); // skip "move" + let piece: usize = parts.next().ok_or_else(|| anyhow!("bad syntax"))?.parse()?; + let x: f32 = parts.next().ok_or_else(|| anyhow!("bad syntax"))?.parse()?; + let y: f32 = parts.next().ok_or_else(|| anyhow!("bad syntax"))?.parse()?; + motions.push(Motion { + piece, + x, + y, + }); + } loop { let curr_pieces = database.pieces.get(&puzzle_id)? .ok_or_else(|| anyhow!("bad puzzle ID"))?; let mut new_pieces = curr_pieces.to_vec(); - new_pieces.get_mut(8 * piece..8 * piece + 4).ok_or_else(|| anyhow!("bad piece ID"))? - .copy_from_slice(&x.to_le_bytes()); - new_pieces.get_mut(8 * piece + 4..8 * piece + 8).ok_or_else(|| anyhow!("bad piece ID"))? - .copy_from_slice(&y.to_le_bytes()); + for Motion {piece, x, y} in motions.iter().copied() { + new_pieces.get_mut(8 * piece..8 * piece + 4).ok_or_else(|| anyhow!("bad piece ID"))? + .copy_from_slice(&x.to_le_bytes()); + new_pieces.get_mut(8 * piece + 4..8 * piece + 8).ok_or_else(|| anyhow!("bad piece ID"))? + .copy_from_slice(&y.to_le_bytes()); + } if database.pieces.compare_and_swap(&puzzle_id, Some(curr_pieces), Some(new_pieces))?.is_ok() { break; } - tokio::time::sleep(std::time::Duration::from_millis(10)).await; + tokio::time::sleep(std::time::Duration::from_millis(1)).await; // yield maybe (don't let contention hog resources) } + ws.send(Message::Text("ack".to_string())).await?; } else if text == "poll" { let puzzle_id = puzzle_id.ok_or_else(|| anyhow!("poll without puzzle ID"))?; let pieces = database.pieces.get(&puzzle_id)?.ok_or_else(|| anyhow!("bad puzzle ID"))?; -- cgit v1.2.3