aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.rs3
-rw-r--r--src/database.rs33
-rw-r--r--src/game/server.rs41
-rw-r--r--src/lib.rs10
-rw-r--r--src/main.rs3
-rw-r--r--src/spectate/index.html35
-rw-r--r--src/spectate/main.ts3
-rw-r--r--src/spectate/server.rs21
-rw-r--r--src/spectate/style.css43
9 files changed, 168 insertions, 24 deletions
diff --git a/src/config.rs b/src/config.rs
index eea7991..b489271 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,10 +1,11 @@
-use crate::{bot, game, spectate};
+use crate::{bot, database, game, spectate};
use anyhow::{anyhow, Result};
use serde::Deserialize;
use std::fs::read_to_string;
#[derive(Deserialize)]
pub struct Config {
+ pub database: database::Config,
pub game: game::Config,
pub spectate: spectate::Config,
pub bot: bot::Config,
diff --git a/src/database.rs b/src/database.rs
new file mode 100644
index 0000000..ac67857
--- /dev/null
+++ b/src/database.rs
@@ -0,0 +1,33 @@
+use anyhow::Result;
+use redb::{Database, ReadableTable, TableDefinition};
+use serde::Deserialize;
+use std::path::PathBuf;
+
+#[derive(Deserialize)]
+pub struct Config {
+ path: PathBuf,
+}
+
+pub fn open_db(config: Config) -> Result<Database> {
+ let db = Database::create(config.path)?;
+
+ Ok(db)
+}
+
+static T_CREDS: TableDefinition<&str, &str> = TableDefinition::new("creds");
+
+pub trait DatabaseExt {
+ fn check_or_insert_creds(&self, username: &str, password: &str) -> anyhow::Result<bool>;
+}
+
+impl DatabaseExt for Database {
+ fn check_or_insert_creds(&self, username: &str, password: &str) -> anyhow::Result<bool> {
+ let txn = self.begin_write()?;
+ let mut table = txn.open_table(T_CREDS)?;
+ if let Some(pw) = table.get(username)? {
+ return Ok(pw.value() == password);
+ }
+ table.insert(username, password)?;
+ Ok(true)
+ }
+}
diff --git a/src/game/server.rs b/src/game/server.rs
index f5de1bf..0e983b9 100644
--- a/src/game/server.rs
+++ b/src/game/server.rs
@@ -1,5 +1,5 @@
use super::{protocol::Packet, Config, Game};
-use crate::State;
+use crate::{database::DatabaseExt, State};
use anyhow::{anyhow, bail, Result};
use log::{debug, error, info};
use std::{ops::ControlFlow, sync::Arc, time::Duration};
@@ -29,7 +29,7 @@ pub async fn game_server(config: Config, state: Arc<State>) -> Result<()> {
}
async fn game_loop(config: Config, state: Arc<State>) {
- let mut speed = config.tickrate;
+ let mut speed = config.tickrate;
loop {
sleep(Duration::from_secs_f32(1. / speed)).await;
@@ -43,9 +43,18 @@ async fn game_loop(config: Config, state: Arc<State>) {
}
ControlFlow::Break(winner) => {
info!("winner: {winner:?}");
+ if let Some(winner) = winner {
+ if let Some(winner) = state.players.write().await.get(&winner).cloned() {
+ let mut h = state.win_history.write().await;
+ h.push_front(winner);
+ while h.len() > 64 {
+ h.pop_back();
+ }
+ }
+ }
let p = state.players.read().await;
*g = Game::new(p.clone().into_iter().collect());
- speed = config.tickrate;
+ speed = config.tickrate;
let _ = state.tick.send(true);
}
}
@@ -81,7 +90,7 @@ async fn handle_client_inner(
let mut lines = rx.lines();
let mut ticks = state.tick.subscribe();
let mut chat = state.chat.subscribe();
- tx.send_packet(Packet::Motd("This is the GPN-Tron Rust rewrite. The protocol should be compatible with the original: https://github.com/freehuntx/gpn-tron/blob/master/PROTOCOL.md".to_string())).await?;
+ tx.send_packet(Packet::Motd("This is the GPN-Tron Rust rewrite. It should be compatible with the original protocol: https://github.com/freehuntx/gpn-tron/blob/master/PROTOCOL.md".to_string())).await?;
loop {
tokio::select! {
message = chat.recv() => {
@@ -127,6 +136,9 @@ async fn handle_tick(
}
cstate.alive = true;
}
+ if !cstate.alive {
+ return Ok(());
+ }
{
let g = state.game.read().await;
if new_game {
@@ -153,9 +165,11 @@ async fn handle_tick(
events.push(Packet::Die(g.dead.clone()));
}
if g.dead.contains(&pid) {
+ cstate.alive = false;
events.push(Packet::Lose(0, 0)); // TODO implement stats
+ } else {
+ events.push(Packet::Tick);
}
- events.push(Packet::Tick);
}
for e in events {
tx.send_packet(e).await?;
@@ -179,10 +193,19 @@ async fn handle_packet(
};
debug!("<- {packet:?}");
match packet {
- Packet::Join {
- username,
- password: _,
- } => {
+ Packet::Join { username, password } => {
+ if username.len() > 64 || password.len() > 64 {
+ tx.send_packet(Packet::Error(
+ "password or username too long (> 64)".to_string(),
+ ))
+ .await?;
+ return Ok(());
+ }
+ if !state.db.check_or_insert_creds(&username, &password)? {
+ tx.send_packet(Packet::Error("incorrect password".to_string()))
+ .await?;
+ return Ok(());
+ }
if cstate.pid.is_some() {
tx.send_packet(Packet::Error("already joined".to_string()))
.await?
diff --git a/src/lib.rs b/src/lib.rs
index b3f62fe..757fb13 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,17 +1,21 @@
#![feature(async_closure)]
#![feature(iterator_try_collect)]
-use std::collections::HashMap;
-
use game::Game;
+use redb::Database;
+use std::collections::{HashMap, VecDeque};
use tokio::sync::{broadcast, RwLock};
+
+pub mod bot;
pub mod config;
+pub mod database;
pub mod game;
pub mod spectate;
-pub mod bot;
pub struct State {
pub tick: broadcast::Sender<bool>, // true for new game
pub game: RwLock<Game>,
pub players: RwLock<HashMap<u32, String>>,
+ pub win_history: RwLock<VecDeque<String>>,
pub chat: broadcast::Sender<(String, String)>,
+ pub db: Database,
}
diff --git a/src/main.rs b/src/main.rs
index 74d784b..fb0f0d2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,7 @@
use gpn_tron2::{
bot::spawn_bots,
config::Config,
+ database::open_db,
game::{server::game_server, Game},
spectate::server::spectate_server,
State,
@@ -13,9 +14,11 @@ async fn main() -> anyhow::Result<()> {
env_logger::init_from_env("LOG");
let config = Config::load()?;
let state = Arc::new(State {
+ db: open_db(config.database)?,
tick: broadcast::channel(512).0,
game: Game::new(vec![]).into(),
players: Default::default(),
+ win_history: Default::default(),
chat: broadcast::channel(512).0,
});
spawn(spectate_server(config.spectate, state.clone()));
diff --git a/src/spectate/index.html b/src/spectate/index.html
index 394b780..5b9dc5a 100644
--- a/src/spectate/index.html
+++ b/src/spectate/index.html
@@ -12,5 +12,40 @@
This is the GPN-Tron spectator application. You need JavaScript to
run it.
</noscript>
+ <div id="side">
+ <div id="info">
+ <h1>GPN-Tron</h1>
+ <p>
+ This is the GPN-Tron Rust rewrite. It should be compatible
+ with the original. Connect via TCP and join the fun!
+ </p>
+ <ul>
+ <li>
+ Spectator:
+ <script>
+ document.write(`HTTP ${window.location.host}`);
+ </script>
+ </li>
+ <li>
+ Player:
+ <script>
+ document.write(
+ `TCP ${window.location.hostname}:4000`
+ );
+ </script>
+ </li>
+ </ul>
+ <a href="https://codeberg.org/metamuffin/gpn-tron-rust">
+ Source code
+ </a>
+ </div>
+ <div id="scoreboard">
+ <h2>Scoreboard</h2>
+ </div>
+ <div id="chat">
+ <h2>Chat</h2>
+ </div>
+ </div>
+ <div id="board"></div>
</body>
</html>
diff --git a/src/spectate/main.ts b/src/spectate/main.ts
index ea84924..508ca30 100644
--- a/src/spectate/main.ts
+++ b/src/spectate/main.ts
@@ -35,7 +35,7 @@ let ctx: CanvasRenderingContext2D
document.addEventListener("DOMContentLoaded", () => {
canvas = document.createElement("canvas")
ctx = canvas.getContext("2d")!
- document.body.append(canvas)
+ document.getElementById("board")?.append(canvas)
canvas.width = 1000;
canvas.height = 1000;
redraw()
@@ -122,7 +122,6 @@ function name_color(name: string): string {
ws.onerror = console.error
ws.onmessage = message => {
const p = JSON.parse(message.data) as Packet
- console.log(p);
if (p == "tick") {
tick_anim = 0
const d = []
diff --git a/src/spectate/server.rs b/src/spectate/server.rs
index da18a77..11c4d5a 100644
--- a/src/spectate/server.rs
+++ b/src/spectate/server.rs
@@ -47,6 +47,19 @@ pub async fn spectate_server(config: Config, state: Arc<State>) -> Result<()> {
Ok(())
}
+#[cfg(debug_assertions)]
+async fn index() -> Html<String> {
+ use tokio::fs::read_to_string;
+ Html(
+ read_to_string(concat!(
+ env!("CARGO_MANIFEST_DIR"),
+ "/src/spectate/index.html"
+ ))
+ .await
+ .unwrap(),
+ )
+}
+#[cfg(not(debug_assertions))]
async fn index() -> Html<&'static str> {
Html(include_str!("index.html"))
}
@@ -91,13 +104,7 @@ async fn css() -> (HeaderMap, &'static str) {
use headers::HeaderMapExt;
let mut hm = HeaderMap::new();
hm.typed_insert(ContentType::from_str("text/css").unwrap());
- (
- hm,
- include_str!(concat!(
- env!("CARGO_MANIFEST_DIR"),
- "/src/spectate/style.css"
- )),
- )
+ (hm, include_str!("style.css"))
}
async fn broadcaster(sstate: Arc<SpectateState>, state: Arc<State>) {
diff --git a/src/spectate/style.css b/src/spectate/style.css
index 52711d0..c391020 100644
--- a/src/spectate/style.css
+++ b/src/spectate/style.css
@@ -1,4 +1,43 @@
+* {
+ color: white;
+}
+body {
+ padding: 0px;
+ margin: 0px;
+ background-color: black;
+
+ width: 100dvw;
+ height: 100dvh;
+ display: flex;
+ flex-direction: row;
+ overflow-y: hidden;
+}
+
canvas {
- height: min(100dvh, 70dvw);
- float: left;
+ object-fit: contain;
+ width: 100%;
+ height: 100%;
+}
+#board {
+ width: 60%;
+ height: 100%;
+}
+#side {
+ width: 30%;
+ height: 100%;
+}
+#info {
+ width: 100%;
+ height: 30%;
+ overflow-y: scroll;
+}
+#scoreboard {
+ width: 100%;
+ height: 30%;
+ overflow-y: scroll;
+}
+#chat {
+ width: 100%;
+ height: 40%;
+ overflow-y: scroll;
}