summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpommicket <pommicket@gmail.com>2024-08-13 23:02:01 -0400
committerpommicket <pommicket@gmail.com>2024-08-13 23:02:01 -0400
commitdb63e7ada26e2aacbbaa3bf9a44fdafed9032c9e (patch)
treece8916631afb589ded5dbee46c18383aa210c5f5
parent56c6670c66ff7db2e30378b098102578e75f98fb (diff)
postgres seems to be working!
-rw-r--r--TODO.txt3
-rw-r--r--game.js4
-rw-r--r--server/Cargo.lock8
-rw-r--r--server/Cargo.toml2
-rw-r--r--server/src/main.rs255
5 files changed, 222 insertions, 50 deletions
diff --git a/TODO.txt b/TODO.txt
index 9a72310..4fc37dc 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -1,5 +1,4 @@
- single player mode with no server
-- limit number of playres connected to a game (prevent contention)
- congratualations
- handle server errors on the client side
-- when server moves pieces bring their zIndex up
+- link to wikimedia File: page rather than image
diff --git a/game.js b/game.js
index dc7a418..aed0da3 100644
--- a/game.js
+++ b/game.js
@@ -332,6 +332,7 @@ window.addEventListener('load', function () {
let dx = e.clientX - draggingPieceLastPos.x;
let dy = e.clientY - draggingPieceLastPos.y;
for (const piece of draggingPiece.connectedComponent) {
+ piece.element.style.zIndex = pieceZIndexCounter;
piece.element.classList.add('no-animation');
piece.x += dx;
piece.y += dy;
@@ -371,7 +372,8 @@ window.addEventListener('load', function () {
console.assert(puzzleWidth === data[8]);
console.assert(puzzleHeight === data[9]);
}
- const nibTypesOffset = 10 + 8 /* timestamp - not relevant to us */;
+ console.log(data);
+ const nibTypesOffset = 10;
const nibTypeCount = 2 * puzzleWidth * puzzleHeight - puzzleWidth - puzzleHeight;
const nibTypes = new Uint16Array(payload, nibTypesOffset, nibTypeCount);
const imageUrlOffset = nibTypesOffset + nibTypeCount * 2;
diff --git a/server/Cargo.lock b/server/Cargo.lock
index 4cb5a9c..58e8261 100644
--- a/server/Cargo.lock
+++ b/server/Cargo.lock
@@ -285,11 +285,11 @@ version = "0.1.0"
dependencies = [
"futures-util",
"rand",
+ "safe-transmute",
"tokio",
"tokio-postgres",
"tokio-tungstenite",
"tungstenite",
- "zerocopy",
]
[[package]]
@@ -546,6 +546,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
+name = "safe-transmute"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3944826ff8fa8093089aba3acb4ef44b9446a99a16f3bf4e74af3f77d340ab7d"
+
+[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 8a848e9..b4e4499 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -6,8 +6,8 @@ edition = "2021"
[dependencies]
futures-util = "0.3"
rand = { version = "0.8.5", features = ["std", "std_rng"] }
+safe-transmute = "0.11.3"
tokio = { version = "1", features = ["rt", "macros", "rt-multi-thread", "net", "io-util", "sync", "time", "process"] }
tokio-postgres = { version = "0.7.11", features = ["array-impls"] }
tokio-tungstenite = "0.23.1"
tungstenite = "0.23.0"
-zerocopy = "0.7.35"
diff --git a/server/src/main.rs b/server/src/main.rs
index 1ec62a6..2c33174 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -1,9 +1,9 @@
-#![allow(dead_code)] // TODO : delete me
-#![allow(unused_variables)] // TODO : delete me
+#![allow(clippy::too_many_arguments)]
use futures_util::{SinkExt, StreamExt};
use rand::seq::SliceRandom;
use rand::Rng;
+use safe_transmute::{transmute_many_pedantic, transmute_to_bytes};
use std::collections::HashMap;
use std::io::prelude::*;
use std::net::SocketAddr;
@@ -12,11 +12,11 @@ use std::time::{Duration, SystemTime};
use tokio::io::AsyncWriteExt;
use tokio::sync::{Mutex, RwLock};
use tungstenite::protocol::Message;
-use zerocopy::AsBytes;
const PUZZLE_ID_CHARSET: &[u8] = b"23456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
const PUZZLE_ID_LEN: usize = 7;
const MAX_PLAYERS: u16 = 20;
+const MAX_PIECES: usize = 1000;
fn generate_puzzle_id() -> [u8; PUZZLE_ID_LEN] {
let mut rng = rand::thread_rng();
@@ -32,45 +32,179 @@ struct Server {
database: tokio_postgres::Client,
}
+struct PieceInfo {
+ positions: Vec<f32>,
+ connectivity: Vec<i16>,
+}
+
+struct PuzzleInfo {
+ width: u8,
+ height: u8,
+ url: String,
+ nib_types: Vec<i16>,
+ piece_info: PieceInfo,
+}
impl Server {
async fn create_table_if_not_exists(&self) -> Result<()> {
- if self.database.query("SELECT FROM puzzles", &[]).await.is_ok() {
- return Ok(());
- } else {
- self.database.execute("CREATE TABLE puzzles (
- id char($1) PRIMARY KEY,
+ 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 varchar(256),
- create_time timestamp,
+ width int4,
+ height int4,
+ create_time timestamp DEFAULT CURRENT_TIMESTAMP,
+ nib_types int2[],
connectivity int2[],
positions float4[]
- )", &[&(PUZZLE_ID_LEN as i32)]).await?;
- return Ok(());
+ )"
+ ),
+ &[],
+ )
+ .await?;
}
+ Ok(())
}
async fn try_register_id(&self, id: [u8; PUZZLE_ID_LEN]) -> Result<bool> {
- todo!()
- }
- async fn set_puzzle_data(&self, id: [u8; PUZZLE_ID_LEN], width: u8, height: u8, url: &str, nib_types: Vec<u16>, piece_positions: Vec<f32>, connectivity_data: Vec<u16>) -> Result<()> {
- todo!()
+ let id = std::str::from_utf8(&id)?;
+ Ok(self
+ .database
+ .execute("INSERT INTO puzzles (id) VALUES ($1)", &[&id])
+ .await
+ .is_ok())
}
- async fn move_piece(&self, piece: usize, x: f32, y: f32) -> Result<()> {
- todo!()
+ async fn set_puzzle_data(
+ &self,
+ id: [u8; PUZZLE_ID_LEN],
+ width: u8,
+ height: u8,
+ url: &str,
+ nib_types: Vec<u16>,
+ piece_positions: Vec<f32>,
+ connectivity: Vec<u16>,
+ ) -> Result<()> {
+ let id = std::str::from_utf8(&id)?;
+ let width = i32::from(width);
+ let height = i32::from(height);
+ // 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(
+ "UPDATE puzzles SET width = $1, height = $2, url = $3, nib_types = $4,
+ connectivity = $5, positions = $6 WHERE id = $7",
+ &[
+ &width,
+ &height,
+ &url,
+ &nib_types,
+ &connectivity,
+ &positions,
+ &id,
+ ],
+ )
+ .await?;
+ Ok(())
}
- async fn connect_pieces(&self, piece1: usize, piece2: usize) -> Result<()> {
- todo!()
+ async fn move_piece(
+ &self,
+ id: [u8; PUZZLE_ID_LEN],
+ piece: usize,
+ x: f32,
+ y: f32,
+ ) -> Result<()> {
+ let id = std::str::from_utf8(&id)?;
+ if piece > MAX_PIECES {
+ return Err(Error::BadPieceID);
+ }
+ let piece = piece as i32;
+ // NOTE: postgresql arrays start at index 1!
+ let i0 = piece * 2 + 1;
+ let i1 = piece * 2 + 2;
+ self.database.execute(
+ "UPDATE puzzles SET positions[$1] = $2, positions[$3] = $4 WHERE id = $5 AND $6 < width * height",
+ // the $6 < width * height protects against OOB access!
+ &[&i0, &x, &i1, &y, &id, &piece]
+ ).await?;
+ Ok(())
}
- async fn get_connectivity(&self, id: [u8; PUZZLE_ID_LEN]) -> Result<Vec<u16>> {
- todo!()
+ async fn connect_pieces(
+ &self,
+ id: [u8; PUZZLE_ID_LEN],
+ piece1: usize,
+ piece2: usize,
+ ) -> Result<()> {
+ let id = std::str::from_utf8(&id)?;
+ // NOTE: postgresql arrays start at index 1!
+ let piece1 = piece1 as i32 + 1;
+ let piece2 = piece2 as i32 + 1;
+ self.database.execute(
+ "UPDATE puzzles SET connectivity = array_replace(connectivity, connectivity[$1], connectivity[$2]) WHERE id = $3 AND $4 < width * height AND $5 < width * height",
+ // the $6 < width * height protects against OOB access!
+ &[&piece1, &piece2, &id, &piece1, &piece2]
+ ).await?;
+ Ok(())
}
- async fn get_positions(&self, id: [u8; PUZZLE_ID_LEN]) -> Result<Vec<f32>> {
- todo!()
+ async fn get_piece_info(&self, id: [u8; PUZZLE_ID_LEN]) -> Result<PieceInfo> {
+ let id = std::str::from_utf8(&id)?;
+ let rows = self
+ .database
+ .query(
+ "SELECT positions, connectivity FROM puzzles WHERE id = $1",
+ &[&id],
+ )
+ .await?;
+ let row = &rows[0];
+ let positions: Vec<f32> = row.try_get(0)?;
+ let connectivity: Vec<i16> = row.try_get(1)?;
+ Ok(PieceInfo {
+ positions,
+ connectivity,
+ })
}
- async fn get_details(&self, id: [u8; PUZZLE_ID_LEN]) -> Result<(u8, u8, String)> {
- todo!()
+ async fn get_puzzle_info(&self, id: [u8; PUZZLE_ID_LEN]) -> Result<PuzzleInfo> {
+ let id = std::str::from_utf8(&id)?;
+ let rows = self.database.query(
+ "SELECT width, height, url, positions, nib_types, connectivity FROM puzzles WHERE id = $1",
+ &[&id]
+ ).await?;
+ let row = &rows[0];
+ let width: i32 = row.try_get(0)?;
+ 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 connectivity: Vec<i16> = row.try_get(5)?;
+ Ok(PuzzleInfo {
+ width: width as u8,
+ height: height as u8,
+ url,
+ nib_types,
+ piece_info: PieceInfo {
+ positions,
+ connectivity,
+ },
+ })
}
async fn sweep(&self) -> Result<()> {
- todo!()
+ self.database
+ .execute(
+ "DELETE FROM puzzles WHERE create_time < current_timestamp - interval '1 week'",
+ &[],
+ )
+ .await?;
+ Ok(())
}
}
@@ -133,19 +267,28 @@ type Result<T> = std::result::Result<T, Error>;
async fn get_puzzle_info(server: &Server, id: &[u8]) -> Result<Vec<u8>> {
let id: [u8; PUZZLE_ID_LEN] = id.try_into().map_err(|_| Error::BadPuzzleID)?;
- let mut data = vec![1];
- let (width, height, url) = server.get_details(id).await?;
+ let mut data = vec![1, 0, 0, 0, 0, 0, 0, 0]; // opcode / version number and padding
+ let PuzzleInfo {
+ width,
+ height,
+ url,
+ nib_types,
+ piece_info: PieceInfo {
+ positions,
+ connectivity,
+ },
+ } = server.get_puzzle_info(id).await?;
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 {
// padding
data.push(0);
}
- let pieces = server.get_positions(id).await?;
- data.extend_from_slice(pieces.as_bytes());
- let connectivity = server.get_connectivity(id).await?;
- data.extend_from_slice(connectivity.as_bytes());
+ data.extend_from_slice(transmute_to_bytes(&positions[..]));
+ data.extend_from_slice(transmute_to_bytes(&connectivity[..]));
Ok(data)
}
@@ -177,12 +320,14 @@ async fn handle_websocket(
if url.len() > 255 {
return Err(Error::ImageURLTooLong);
}
- if (width as u16) * (height as u16) > 1000 {
+ if usize::from(width) * usize::from(height) > MAX_PIECES {
return Err(Error::TooManyPieces);
}
- let nib_count = 2 * (width as usize) * (height as usize) - (width as usize) - (height as usize);
+ let nib_count =
+ 2 * (width as usize) * (height as usize) - (width as usize) - (height as usize);
let mut nib_types: Vec<u16> = Vec::with_capacity(nib_count);
- let mut piece_positions: Vec<f32> = Vec::with_capacity((width as usize) * (height as usize) * 2);
+ let mut piece_positions: Vec<f32> =
+ Vec::with_capacity((width as usize) * (height as usize) * 2);
{
let mut rng = rand::thread_rng();
// pick nib types
@@ -212,7 +357,17 @@ async fn handle_websocket(
break;
}
}
- server.set_puzzle_data(id, width, height, url, nib_types, piece_positions, connectivity_data).await?;
+ server
+ .set_puzzle_data(
+ id,
+ width,
+ height,
+ url,
+ nib_types,
+ piece_positions,
+ connectivity_data,
+ )
+ .await?;
server.player_counts.lock().await.insert(id, 1);
ws.send(Message::Text(format!(
"id: {}",
@@ -253,7 +408,7 @@ async fn handle_websocket(
.ok_or(Error::BadSyntax)?
.parse()
.map_err(|_| Error::BadSyntax)?;
- server.move_piece(piece, x, y).await?;
+ 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 ") {
@@ -269,12 +424,16 @@ async fn handle_websocket(
.ok_or(Error::BadSyntax)?
.parse()
.map_err(|_| Error::BadSyntax)?;
- server.connect_pieces(piece1, piece2).await?;
+ server.connect_pieces(puzzle_id, piece1, piece2).await?;
} else if text == "poll" {
let puzzle_id = puzzle_id.ok_or(Error::NotJoined)?;
let mut data = vec![2, 0, 0, 0, 0, 0, 0, 0]; // opcode / version number + padding
- data.extend_from_slice(server.get_positions(puzzle_id).await?.as_bytes());
- data.extend_from_slice(server.get_connectivity(puzzle_id).await?.as_bytes());
+ let PieceInfo {
+ positions,
+ connectivity,
+ } = server.get_piece_info(puzzle_id).await?;
+ data.extend_from_slice(transmute_to_bytes(&positions[..]));
+ data.extend_from_slice(transmute_to_bytes(&connectivity[..]));
ws.send(Message::Binary(data)).await?;
} else if text == "randomFeaturedWikimedia" {
let choice = rand::thread_rng().gen_range(0..server.wikimedia_featured.len());
@@ -367,8 +526,13 @@ async fn main() {
let wikimedia_featured =
read_to_lines("featuredpictures.txt").expect("Couldn't read featuredpictures.txt");
let potd = get_potd().await;
- let (client, connection) = tokio_postgres::connect("host=/var/run/postgresql dbname=jigsaw", tokio_postgres::NoTls).await.expect("Couldn't connect to database");
-
+ let (client, connection) = tokio_postgres::connect(
+ "host=/var/run/postgresql dbname=jigsaw",
+ tokio_postgres::NoTls,
+ )
+ .await
+ .expect("Couldn't connect to database");
+
// docs say: "The connection object performs the actual communication with the database, so spawn it off to run on its own."
tokio::spawn(async move {
if let Err(e) = connection.await {
@@ -382,7 +546,10 @@ async fn main() {
wikimedia_featured,
}
});
- server_arc.create_table_if_not_exists().await.expect("error creating table");
+ server_arc
+ .create_table_if_not_exists()
+ .await
+ .expect("error creating table");
let server_arc_clone = server_arc.clone();
tokio::task::spawn(async move {
let server: &Server = server_arc_clone.as_ref();
@@ -404,8 +571,6 @@ async fn main() {
tokio::task::spawn(async move {
let server: &Server = server_arc_clone.as_ref();
loop {
- // TODO : sweep
- let now = SystemTime::now();
if let Err(e) = server.sweep().await {
eprintln!("error sweeping DB: {e}");
}