summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpommicket <pommicket@gmail.com>2024-08-24 22:05:33 -0400
committerpommicket <pommicket@gmail.com>2024-08-24 22:05:33 -0400
commitb20773247f97d0edb3475c8b56ec21e4237f7842 (patch)
treef74a875ef2ec10acca06f6420e15e21d0caca5f7
parent2cd2a3a6aa10546b0334187cb7f155f664760d43 (diff)
start single-player mode
-rw-r--r--TODO.txt4
-rw-r--r--game.html1
-rw-r--r--game.js180
-rw-r--r--server/src/main.rs227
4 files changed, 207 insertions, 205 deletions
diff --git a/TODO.txt b/TODO.txt
index 9ef13bd..7610b29 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -1,6 +1,2 @@
- single player mode with no server
-- congratualations
- handle server errors on the client side
-- let user select piece size
-- change piece.x,y to relative (% of play-area)
-- webgl? maybe?
diff --git a/game.html b/game.html
index 47a05a8..6965025 100644
--- a/game.html
+++ b/game.html
@@ -13,6 +13,7 @@
<div id="header">
<a href="index.html">← Back</a>
<a id="image-link" style="visibility: hidden;">🖼️ Link to image</a>
+ <button style="display: none;" id="host-multiplayer">👥 Host multiplayer puzzle</button>
<a id="join-link" style="display: none;">🔗 Invite others to join</a>
<button style="width:3em;overflow:hidden;" id="piece-size-minus">🔍−</button> <button style="width:3em;overflow:hidden;" id="piece-size-plus">🔍+</button>
</div>
diff --git a/game.js b/game.js
index ceea26d..66165d4 100644
--- a/game.js
+++ b/game.js
@@ -1,8 +1,11 @@
'use strict';
window.addEventListener('load', function () {
+ const ACTION_MOVE = 3;
+ const ACTION_CONNECT = 4;
const socket = new WebSocket(location.protocol === "file:" || location.hostname === "localhost" ? "ws://localhost:54472" : "wss://jigsaw.pommicket.com");
const searchParams = new URL(location.href).searchParams;
socket.binaryType = "arraybuffer";
+ let puzzleSeed = Math.floor(Math.random() * 0x7fffffff);
// direct URL to image file
let imageUrl = searchParams.has('image') ? encodeURI(searchParams.get('image')) : undefined;
// link to page with info about image (e.g. https://commons.wikimedia.org/wiki/File:Foo.jpg)
@@ -32,7 +35,8 @@ window.addEventListener('load', function () {
let nibSize;
let pieceWidth;
let pieceHeight;
- let receivedAck = true;
+ let multiplayer = !joinLink;
+ let waitingForAck = 0;
if (imageUrl && imageUrl.startsWith('http')) {
// make sure we use https
let url = new URL(imageUrl);
@@ -231,7 +235,7 @@ window.addEventListener('load', function () {
this.x = x;
this.y = y;
this.nibTypes = nibTypes;
- this.needsServerUpdate = false;
+ this.needsServerUpdate = true;
this.connectedComponent = [this];
const element = this.element = document.createElement('div');
element.classList.add('piece');
@@ -359,7 +363,9 @@ window.addEventListener('load', function () {
if (sqDist < connectRadius * connectRadius) {
anyConnected = true;
connectPieces(piece, neighbour, true);
- socket.send(`connect ${piece.id} ${neighbour.id}`);
+ if (multiplayer) {
+ socket.send(new Uint32Array([ACTION_CONNECT, piece.id, neighbour.id]));
+ }
}
}
}
@@ -424,33 +430,7 @@ window.addEventListener('load', function () {
deriveConnectedPiecePositions();
if (anyConnected) connectAudio.play();
}
- async function initPuzzle(payload) {
- const data = new Uint8Array(payload, payload.length);
- if (joinPuzzle) {
- puzzleWidth = data[8];
- puzzleHeight = data[9];
- } else {
- console.assert(puzzleWidth === data[8]);
- console.assert(puzzleHeight === data[9]);
- }
- const nibTypesOffset = 10;
- const nibTypeCount = 2 * puzzleWidth * puzzleHeight - puzzleWidth - puzzleHeight;
- const nibTypes = new Uint16Array(payload, nibTypesOffset, nibTypeCount);
- const imageUrlOffset = nibTypesOffset + nibTypeCount * 2;
- const imageUrlLen = new Uint8Array(payload, imageUrlOffset, data.length - imageUrlOffset).indexOf(0);
- const imageUrlBytes = new Uint8Array(payload, imageUrlOffset, imageUrlLen);
- let piecePositionsOffset = imageUrlOffset + imageUrlLen + 1;
- piecePositionsOffset = Math.floor((piecePositionsOffset + 7) / 8) * 8; // align to 8 bytes
- const piecePositions = new Float32Array(payload, piecePositionsOffset, puzzleWidth * puzzleHeight * 2);
- const connectivityOffset = piecePositionsOffset + piecePositions.length * 4;
- const connectivity = new Uint16Array(payload, connectivityOffset, puzzleWidth * puzzleHeight);
- if (joinPuzzle) {
- const parts = new TextDecoder().decode(imageUrlBytes).split(' ');
- imageUrl = parts[0];
- imageLink = parts.length > 1 ? parts[1] : parts[0];
- await loadImage();
- }
- let nibTypeIndex = 0;
+ function createPieces() {
if (playArea.clientWidth / puzzleWidth < playArea.clientHeight / puzzleHeight) {
pieceWidth = 0.8 * playArea.clientWidth / puzzleWidth;
pieceHeight = pieceWidth * (puzzleWidth / puzzleHeight) * (image.height / image.width);
@@ -470,18 +450,68 @@ window.addEventListener('load', function () {
let id = pieces.length;
if (v > 0) nibs[0] = pieces[id - puzzleWidth].nibTypes[2].inverse();
if (u < puzzleWidth - 1) {
- setRandomSeed(nibTypes[nibTypeIndex++]);
nibs[1] = NibType.random(Math.floor(random() * 2) ? RIGHT_IN : RIGHT_OUT);
}
if (v < puzzleHeight - 1) {
- setRandomSeed(nibTypes[nibTypeIndex++]);
nibs[2] = NibType.random(Math.floor(random() * 2) ? BOTTOM_IN : BOTTOM_OUT);
}
if (u > 0) nibs[3] = pieces[id - 1].nibTypes[1].inverse();
pieces.push(new Piece(id, 0, 0, nibs));
}
}
- console.assert(nibTypeIndex === nibTypeCount);
+ }
+ async function createPuzzle() {
+ await loadImage();
+ if (isNaN(roughPieceCount) || roughPieceCount < 10 || roughPieceCount > 1000) {
+ // TODO : better error reporting
+ console.error('bad piece count');
+ return;
+ }
+ let bestWidth = 1;
+ let bestDiff = Infinity;
+ function heightFromWidth(w) {
+ return Math.min(255, Math.max(2, Math.round(w * image.height / image.width)));
+ }
+ for (let width = 2; width < 256; width++) {
+ const height = heightFromWidth(width);
+ if (width * height > 1000) break;
+ const diff = Math.abs(width * height - roughPieceCount);
+ if (diff < bestDiff) {
+ bestDiff = diff;
+ bestWidth = width;
+ }
+ }
+ puzzleWidth = bestWidth;
+ puzzleHeight = heightFromWidth(puzzleWidth);
+ getById('host-multiplayer').style.display = 'inline-block';
+ setRandomSeed(puzzleSeed);
+ createPieces();
+ // a bit janky, but it stops the pieces from animating to their starting positions
+ setTimeout(() => {
+ for (const piece of pieces) {
+ piece.setAnimate(true);
+ }
+ }, 100);
+ }
+ async function initPuzzle(payload) {
+ const data = new Uint8Array(payload, payload.length);
+ puzzleSeed = new Uint32Array(payload, 8, 1)[0];
+ setRandomSeed(puzzleSeed);
+ puzzleWidth = data[12];
+ puzzleHeight = data[13];
+ const imageUrlOffset = 14;
+ const imageUrlLen = new Uint8Array(payload, imageUrlOffset, data.length - imageUrlOffset).indexOf(0);
+ const imageUrlBytes = new Uint8Array(payload, imageUrlOffset, imageUrlLen);
+ let piecePositionsOffset = imageUrlOffset + imageUrlLen + 1;
+ piecePositionsOffset = Math.floor((piecePositionsOffset + 7) / 8) * 8; // align to 8 bytes
+ const piecePositions = new Float32Array(payload, piecePositionsOffset, puzzleWidth * puzzleHeight * 2);
+ const connectivityOffset = piecePositionsOffset + piecePositions.length * 4;
+ const connectivity = new Uint16Array(payload, connectivityOffset, puzzleWidth * puzzleHeight);
+ const parts = new TextDecoder().decode(imageUrlBytes).split(' ');
+ imageUrl = parts[0];
+ imageLink = parts.length > 1 ? parts[1] : parts[0];
+ await loadImage();
+ createPieces();
for (let id = 0; id < pieces.length; id++) {
if (id !== connectivity[id]) continue; // only set one piece positions per piece group
pieces[id].x = piecePositions[2 * connectivity[id]];
@@ -496,6 +526,10 @@ window.addEventListener('load', function () {
}
}, 100);
}
+ async function hostPuzzle() {
+ socket.send(`new ${puzzleWidth} ${puzzleHeight} ${imageUrl};${imageLink} ${puzzleSeed}`);
+ multiplayer = true;
+ }
function applyUpdate(update) {
const piecePositions = new Float32Array(update, 8, puzzleWidth * puzzleHeight * 2);
const connectivity = new Uint16Array(update, 8 + piecePositions.length * 4, puzzleWidth * puzzleHeight);
@@ -524,48 +558,34 @@ window.addEventListener('load', function () {
}
function sendServerUpdate() {
// send update to server
- if (!receivedAck) return; // last update hasn't been acknowledged yet
- const motions = [];
+ if (!multiplayer) return;
+ if (waitingForAck) return; // last update hasn't been acknowledged yet
+ let actions = new Uint32Array(pieces.length * 4 + 1);
+ const pos = new Float32Array(2);
+ const posU8 = new Uint8Array(pos.buffer);
+ let i = 0;
+ let messageID = 0x12345678; // message ID for regular updates
+ actions[i++] = messageID;
for (const piece of pieces) {
if (!piece.needsServerUpdate) continue;
- motions.push(`move ${piece.id} ${piece.x} ${piece.y}`);
+ actions[i++] = ACTION_MOVE;
+ actions[i++] = piece.id;
+ pos[0] = piece.x;
+ pos[1] = piece.y;
+ new Uint8Array(actions.buffer, 4 * i, 8).set(posU8);
+ i += 2;
}
- if (motions.length) {
- receivedAck = false;
- socket.send(motions.join('\n'));
+ if (i > 1) {
+ waitingForAck = messageID;
+ socket.send(new Uint32Array(actions.buffer, 0, i));
}
- }
- async function hostPuzzle() {
- await loadImage();
- if (isNaN(roughPieceCount) || roughPieceCount < 10 || roughPieceCount > 1000) {
- // TODO : better error reporting
- console.error('bad piece count');
- return;
- }
- let bestWidth = 1;
- let bestDiff = Infinity;
- function heightFromWidth(w) {
- return Math.min(255, Math.max(2, Math.round(w * image.height / image.width)));
- }
- for (let width = 2; width < 256; width++) {
- const height = heightFromWidth(width);
- if (width * height > 1000) break;
- const diff = Math.abs(width * height - roughPieceCount);
- if (diff < bestDiff) {
- bestDiff = diff;
- bestWidth = width;
- }
- }
- puzzleWidth = bestWidth;
- puzzleHeight = heightFromWidth(puzzleWidth);
- socket.send(`new ${puzzleWidth} ${puzzleHeight} ${imageUrl};${imageLink}`);
}
let waitingForServerToGiveUsImageUrl = false;
socket.addEventListener('open', async () => {
if (joinPuzzle) {
socket.send(`join ${joinPuzzle}`);
} else if (imageUrl.startsWith('http')) {
- hostPuzzle();
+ createPuzzle();
} else if (imageUrl === 'randomFeaturedWikimedia') {
socket.send('randomFeaturedWikimedia');
waitingForServerToGiveUsImageUrl = true;
@@ -581,19 +601,30 @@ window.addEventListener('load', function () {
if (typeof e.data === 'string') {
if (e.data.startsWith('id: ')) {
let puzzleID = e.data.split(' ')[1];
+ sendServerUpdate(); // send piece positions
+ const connectivityUpdate = [0 /* message ID */];
+ for (const piece of pieces) {
+ if (piece !== piece.connectedComponent[0]) {
+ connectivityUpdate.push(ACTION_CONNECT, piece.connectedComponent[0].id, piece.id);
+ }
+ }
+ socket.send(new Uint32Array(connectivityUpdate));
history.pushState({}, null, `?join=${puzzleID}`);
setJoinLink(puzzleID);
- } else if (e.data === 'ack') {
- for (const piece of pieces) {
- piece.needsServerUpdate = false;
+ } else if (e.data.startsWith('ack')) {
+ const messageID = parseInt(e.data.split(' ')[1]);
+ if (messageID === waitingForAck) {
+ for (const piece of pieces) {
+ piece.needsServerUpdate = false;
+ }
+ waitingForAck = 0;
}
- receivedAck = true;
} else if (waitingForServerToGiveUsImageUrl && e.data.startsWith('useImage ')) {
waitingForServerToGiveUsImageUrl = false;
const parts = e.data.substring('useImage '.length).split(' ');
imageUrl = parts[0];
imageLink = parts.length > 1 ? parts[1] : imageUrl;
- hostPuzzle();
+ createPuzzle();
} else if (e.data.startsWith('error ')) {
const error = e.data.substring('error '.length);
console.error(error); // TODO : better error handling
@@ -602,7 +633,11 @@ window.addEventListener('load', function () {
const opcode = new Uint8Array(e.data, 0, 1)[0];
if (opcode === 1 && !pieces.length) { // init puzzle
await initPuzzle(e.data);
- setInterval(() => socket.send('poll'), 1000);
+ setInterval(() => {
+ if (multiplayer) {
+ socket.send('poll');
+ }
+ }, 1000);
setInterval(sendServerUpdate, 1000);
} else if (opcode === 2) { // update puzzle
applyUpdate(e.data);
@@ -634,5 +669,8 @@ window.addEventListener('load', function () {
getById('piece-size-minus').addEventListener('click', () => {
setPieceSize(pieceWidth / 1.2, pieceHeight / 1.2);
});
+ getById('host-multiplayer').addEventListener('click', () => {
+ hostPuzzle();
+ });
requestAnimationFrame(everyFrame);
});
diff --git a/server/src/main.rs b/server/src/main.rs
index 7de3d68..a0a1d39 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -17,6 +17,8 @@ const PUZZLE_ID_CHARSET: &[u8] = b"23456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLM
const PUZZLE_ID_LEN: usize = 7;
const MAX_PLAYERS: u16 = 20;
const MAX_PIECES: usize = 1000;
+const ACTION_MOVE: u32 = 3;
+const ACTION_CONNECT: u32 = 4;
fn generate_puzzle_id() -> [u8; PUZZLE_ID_LEN] {
let mut rng = rand::thread_rng();
@@ -47,41 +49,11 @@ struct PuzzleInfo {
width: u8,
height: u8,
url: String,
- nib_types: Vec<i16>,
+ seed: u32,
piece_info: PieceInfo,
}
impl Server {
- async fn create_table_if_not_exists(&self) -> Result<()> {
- if self
- .database
- .query("SELECT FROM puzzles", &[])
- .await
- .is_err()
- {
- self.database
- .execute(
- &format!(
- "CREATE TABLE puzzles (
- id char({PUZZLE_ID_LEN}) PRIMARY KEY,
- url text,
- width int4,
- height int4,
- create_time timestamp DEFAULT CURRENT_TIMESTAMP,
- nib_types int2[],
- connectivity int2[],
- positions float4[]
- )"
- ),
- &[],
- )
- .await?;
- self.database
- .execute("CREATE INDEX by_id ON puzzles (id)", &[])
- .await?;
- }
- Ok(())
- }
async fn try_register_id(&self, id: [u8; PUZZLE_ID_LEN]) -> Result<bool> {
let id = std::str::from_utf8(&id)?;
Ok(self
@@ -96,31 +68,22 @@ impl Server {
width: u8,
height: u8,
url: &str,
- nib_types: Vec<u16>,
piece_positions: &[f32],
connectivity: Vec<u16>,
+ seed: u32,
) -> Result<()> {
let id = std::str::from_utf8(&id)?;
let width = i32::from(width);
let height = i32::from(height);
+ let seed = seed as i32;
// transmuting u16 to i16 should never give an error. they have the same alignment.
- let nib_types: &[i16] =
- transmute_many_pedantic(transmute_to_bytes(&nib_types[..])).unwrap();
let connectivity: &[i16] =
transmute_many_pedantic(transmute_to_bytes(&connectivity[..])).unwrap();
let positions = &piece_positions;
self.database
.execute(
&self.set_puzzle_data,
- &[
- &width,
- &height,
- &url,
- &nib_types,
- &connectivity,
- &positions,
- &id,
- ],
+ &[&width, &height, &url, &connectivity, &positions, &id, &seed],
)
.await?;
Ok(())
@@ -179,13 +142,13 @@ impl Server {
let height: i32 = row.try_get(1)?;
let url: String = row.try_get(2)?;
let positions: Vec<f32> = row.try_get(3)?;
- let nib_types: Vec<i16> = row.try_get(4)?;
+ let seed: i32 = row.try_get(4)?;
let connectivity: Vec<i16> = row.try_get(5)?;
Ok(PuzzleInfo {
width: width as u8,
height: height as u8,
url,
- nib_types,
+ seed: seed as u32,
piece_info: PieceInfo {
positions,
connectivity,
@@ -267,15 +230,15 @@ async fn get_puzzle_info(server: &Server, id: &[u8]) -> Result<Vec<u8>> {
width,
height,
url,
- nib_types,
+ seed,
piece_info: PieceInfo {
positions,
connectivity,
},
} = server.get_puzzle_info(id).await?;
+ data.extend(seed.to_le_bytes());
data.push(width);
data.push(height);
- data.extend(transmute_to_bytes(&nib_types[..]));
data.extend(url.as_bytes());
data.push(0);
while data.len() % 8 != 0 {
@@ -315,37 +278,19 @@ async fn handle_websocket(
if width < 3 || height < 3 {
return Err(Error::BadSyntax);
}
- let url: String = parts.next().ok_or(Error::BadSyntax)?.replace(';', " ");
- if url.len() > 2048 {
- return Err(Error::ImageURLTooLong);
- }
if usize::from(width) * usize::from(height) > MAX_PIECES {
return Err(Error::TooManyPieces);
}
- let nib_count = 2 * usize::from(width) * usize::from(height)
- - usize::from(width) - usize::from(height);
- let mut nib_types: Vec<u16> = Vec::with_capacity(nib_count);
- let mut piece_positions: Vec<[f32; 2]> =
- Vec::with_capacity((width as usize) * (height as usize));
- {
- let mut rng = rand::thread_rng();
- // pick nib types
- for _ in 0..nib_count {
- nib_types.push(rng.gen());
- }
- // pick piece positions
- for y in 0..u16::from(height) {
- for x in 0..u16::from(width) {
- let dx: f32 = rng.gen_range(0.0..0.3);
- let dy: f32 = rng.gen_range(0.0..0.3);
- piece_positions.push([
- (f32::from(x) + dx) / (f32::from(width) + 1.0),
- (f32::from(y) + dy) / (f32::from(height) + 1.0),
- ]);
- }
- }
- piece_positions.shuffle(&mut rng);
+ let url: String = parts.next().ok_or(Error::BadSyntax)?.replace(';', " ");
+ if url.len() > 2048 {
+ return Err(Error::ImageURLTooLong);
}
+ let seed = parts
+ .next()
+ .ok_or(Error::BadSyntax)?
+ .parse()
+ .map_err(|_| Error::BadSyntax)?;
+ let piece_positions = vec![0.0f32; 2 * (width as usize) * (height as usize)];
let mut connectivity_data: Vec<u16> =
Vec::with_capacity(usize::from(width) * usize::from(height));
for i in 0..u16::from(width) * u16::from(height) {
@@ -364,17 +309,15 @@ async fn handle_websocket(
width,
height,
&url,
- nib_types,
- piece_positions.as_flattened(),
+ &piece_positions,
connectivity_data,
+ seed,
)
.await?;
server.player_counts.lock().await.insert(id, 1);
*puzzle_id = Some(id);
ws.send(Message::Text(format!("id: {}", std::str::from_utf8(&id)?)))
.await?;
- let info = get_puzzle_info(server, &id).await?;
- ws.send(Message::Binary(info)).await?;
} else if let Some(id) = text.strip_prefix("join ") {
let id = id.as_bytes().try_into().map_err(|_| Error::BadSyntax)?;
let mut player_counts = server.player_counts.lock().await;
@@ -387,48 +330,6 @@ async fn handle_websocket(
*puzzle_id = Some(id);
let info = get_puzzle_info(server, &id).await?;
ws.send(Message::Binary(info)).await?;
- } else if text.starts_with("move ") {
- let puzzle_id = puzzle_id.ok_or(Error::NotJoined)?;
- for line in text.split('\n') {
- let mut parts = line.split(' ');
- parts.next(); // skip "move"
- let piece: usize = parts
- .next()
- .ok_or(Error::BadSyntax)?
- .parse()
- .map_err(|_| Error::BadSyntax)?;
- let x: f32 = parts
- .next()
- .ok_or(Error::BadSyntax)?
- .parse()
- .map_err(|_| Error::BadSyntax)?;
- let y: f32 = parts
- .next()
- .ok_or(Error::BadSyntax)?
- .parse()
- .map_err(|_| Error::BadSyntax)?;
- for coord in [x, y] {
- if !coord.is_finite() || coord < 0.0 || coord > 2.0 {
- return Err(Error::BadSyntax);
- }
- }
- server.move_piece(puzzle_id, piece, x, y).await?;
- }
- ws.send(Message::Text("ack".to_string())).await?;
- } else if let Some(data) = text.strip_prefix("connect ") {
- let mut parts = data.split(' ');
- let puzzle_id = puzzle_id.ok_or(Error::NotJoined)?;
- let piece1: usize = parts
- .next()
- .ok_or(Error::BadSyntax)?
- .parse()
- .map_err(|_| Error::BadSyntax)?;
- let piece2: usize = parts
- .next()
- .ok_or(Error::BadSyntax)?
- .parse()
- .map_err(|_| Error::BadSyntax)?;
- server.connect_pieces(puzzle_id, piece1, piece2).await?;
} else if text == "poll" {
let puzzle_id = puzzle_id.ok_or(Error::NotJoined)?;
let PieceInfo {
@@ -464,6 +365,46 @@ async fn handle_websocket(
)))
.await?;
}
+ } else if let Message::Binary(data) = &message {
+ if data.len() % 4 != 0 {
+ return Err(Error::BadSyntax);
+ }
+ let puzzle_id = puzzle_id.ok_or(Error::NotJoined)?;
+ let mut reader_data = std::io::Cursor::new(data);
+ let reader = &mut reader_data;
+ fn read<const N: usize>(reader: &mut std::io::Cursor<&Vec<u8>>) -> Result<[u8; N]> {
+ let mut data = [0; N];
+ reader.read_exact(&mut data).map_err(|_| Error::BadSyntax)?;
+ Ok(data)
+ }
+ fn read_u32(reader: &mut std::io::Cursor<&Vec<u8>>) -> Result<u32> {
+ Ok(u32::from_le_bytes(read(reader)?))
+ }
+ fn read_f32(reader: &mut std::io::Cursor<&Vec<u8>>) -> Result<f32> {
+ Ok(f32::from_le_bytes(read(reader)?))
+ }
+ let message_id = read_u32(reader)?;
+ while !reader.get_ref().is_empty() {
+ let action = read_u32(reader)?;
+ if action == ACTION_MOVE {
+ let piece: usize = read_u32(reader)? as _;
+ let x: f32 = read_f32(reader)?;
+ let y: f32 = read_f32(reader)?;
+ for coord in [x, y] {
+ if !coord.is_finite() || coord < 0.0 || coord > 2.0 {
+ return Err(Error::BadSyntax);
+ }
+ }
+ server.move_piece(puzzle_id, piece, x, y).await?;
+ } else if action == ACTION_CONNECT {
+ let piece1: usize = read_u32(reader)? as _;
+ let piece2: usize = read_u32(reader)? as _;
+ server.connect_pieces(puzzle_id, piece1, piece2).await?;
+ } else {
+ return Err(Error::BadSyntax);
+ }
+ }
+ ws.send(Message::Text(format!("ack {message_id}"))).await?;
}
}
Ok(())
@@ -474,8 +415,8 @@ async fn handle_connection(server: &Server, conn: &mut tokio::net::TcpStream) ->
let mut ws = tokio_tungstenite::accept_async_with_config(
conn,
Some(tungstenite::protocol::WebSocketConfig {
- max_message_size: Some(65536),
- max_frame_size: Some(65536),
+ max_message_size: Some(128 << 10),
+ max_frame_size: Some(128 << 10),
..Default::default()
}),
)
@@ -525,6 +466,32 @@ async fn get_potd() -> String {
}
}
+async fn create_table_if_doesnt_exist(database: &tokio_postgres::Client) -> Result<()> {
+ if database.query("SELECT FROM puzzles", &[]).await.is_err() {
+ database
+ .execute(
+ &format!(
+ "CREATE TABLE puzzles (
+ id char({PUZZLE_ID_LEN}) PRIMARY KEY,
+ url text,
+ width int4,
+ height int4,
+ create_time timestamp DEFAULT CURRENT_TIMESTAMP,
+ seed int4,
+ connectivity int2[],
+ positions float4[]
+ )"
+ ),
+ &[],
+ )
+ .await?;
+ database
+ .execute("CREATE INDEX by_id ON puzzles (id)", &[])
+ .await?;
+ }
+ Ok(())
+}
+
#[tokio::main]
async fn main() {
let port = 54472;
@@ -554,6 +521,10 @@ async fn main() {
eprintln!("connection error: {}", e);
}
});
+ if let Err(e) = create_table_if_doesnt_exist(&client).await {
+ eprintln!("couldn't create table: {e}");
+ return;
+ };
use tokio_postgres::types::Type;
let create_puzzle = client
.prepare_typed("INSERT INTO puzzles (id) VALUES ($1)", &[Type::BPCHAR])
@@ -561,15 +532,15 @@ async fn main() {
.expect("couldn't prepare create_puzzle statement");
let set_puzzle_data = client
.prepare_typed(
- "UPDATE puzzles SET width = $1, height = $2, url = $3, nib_types = $4,
- connectivity = $5, positions = $6 WHERE id = $7",
+ "UPDATE puzzles SET width = $1, height = $2, url = $3,
+ connectivity = $4, positions = $5, seed = $6 WHERE id = $7",
&[
Type::INT4,
Type::INT4,
Type::TEXT,
Type::INT2_ARRAY,
- Type::INT2_ARRAY,
Type::FLOAT4_ARRAY,
+ Type::INT4,
Type::BPCHAR,
],
)
@@ -589,7 +560,7 @@ async fn main() {
)
.await
.expect("couldn't prepare get_piece_info statement");
- let get_puzzle_info = client.prepare_typed("SELECT width, height, url, positions, nib_types, connectivity FROM puzzles WHERE id = $1", &[Type::BPCHAR])
+ let get_puzzle_info = client.prepare_typed("SELECT width, height, url, positions, seed, connectivity FROM puzzles WHERE id = $1", &[Type::BPCHAR])
.await.expect("couldn't prepare get_puzzle_info statement");
Server {
player_counts: Mutex::new(HashMap::new()),
@@ -604,10 +575,6 @@ async fn main() {
wikimedia_featured,
}
}));
- server
- .create_table_if_not_exists()
- .await
- .expect("error creating table");
tokio::task::spawn(async move {
fn next_day(t: SystemTime) -> SystemTime {
let day = 60 * 60 * 24;