From 3d107ea4710f3dec0eedd91ed5bc1e52d8f15912 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Mon, 3 Jun 2024 21:27:52 +0200 Subject: code --- src/config.rs | 20 +++++ src/game/mod.rs | 71 +++++++++++++++++ src/game/protocol.rs | 204 ++++++++++++++++++++++++++++++++++++++++++++++++ src/game/server.rs | 46 +++++++++++ src/main.rs | 31 ++++++++ src/spectate/index.html | 11 +++ src/spectate/mod.rs | 9 +++ src/spectate/server.rs | 121 ++++++++++++++++++++++++++++ 8 files changed, 513 insertions(+) create mode 100644 src/config.rs create mode 100644 src/game/mod.rs create mode 100644 src/game/protocol.rs create mode 100644 src/game/server.rs create mode 100644 src/main.rs create mode 100644 src/spectate/index.html create mode 100644 src/spectate/mod.rs create mode 100644 src/spectate/server.rs (limited to 'src') diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..678074e --- /dev/null +++ b/src/config.rs @@ -0,0 +1,20 @@ +use crate::{game, spectate}; +use anyhow::{anyhow, Result}; +use serde::Deserialize; +use std::fs::read_to_string; + +#[derive(Deserialize)] +pub struct Config { + pub game: game::Config, + pub spectate: spectate::Config, +} + +impl Config { + pub fn load() -> Result { + Ok(toml::from_str(&read_to_string( + std::env::args() + .nth(1) + .ok_or(anyhow!("first arg is the config"))?, + )?)?) + } +} diff --git a/src/game/mod.rs b/src/game/mod.rs new file mode 100644 index 0000000..27faa27 --- /dev/null +++ b/src/game/mod.rs @@ -0,0 +1,71 @@ +use glam::IVec2; +use protocol::Direction; +use serde::Deserialize; +use std::{collections::HashMap, net::SocketAddr, ops::ControlFlow}; + +pub mod protocol; +pub mod server; + +#[derive(Deserialize)] +pub struct Config { + bind: SocketAddr, +} + +pub struct Game { + pub heads: HashMap, + pub map: HashMap, + pub size: IVec2, + pub dead: Vec, +} +impl Game { + pub fn new(players: Vec) -> Self { + let mut map = HashMap::new(); + let mut heads = HashMap::new(); + let plen = players.len(); + for (p, name) in players.into_iter().enumerate() { + let pos = IVec2::ONE * p as i32 * 2; + map.insert(pos, p as u32); + heads.insert(p as u32, (Direction::Up, pos, name)); + } + Self { + size: IVec2::ONE * plen as i32 * 2, + heads, + map, + dead: Vec::new(), + } + } + pub fn tick(&mut self) -> ControlFlow, ()> { + for (_player, (dir, head, _)) in &mut self.heads { + *head = (*head + dir.vector()).rem_euclid(self.size) + } + + self.dead.clear(); + let mut h = HashMap::>::new(); + for (player, (_, head, _)) in &self.heads { + h.entry(*head).or_default().push(*player); + if self.map.contains_key(head) { + self.dead.push(*player); + } + } + for (_, hp) in h { + if hp.len() > 1 { + self.dead.extend(hp) + } + } + + for (player, (_, head, _)) in &mut self.heads { + self.map.insert(*head, *player); + } + + for d in &self.dead { + self.map.retain(|_, p| *d != *p); + self.heads.remove(&d); + } + + if self.heads.len() <= 1 { + ControlFlow::Break(self.heads.keys().next().cloned()) + } else { + ControlFlow::Continue(()) + } + } +} diff --git a/src/game/protocol.rs b/src/game/protocol.rs new file mode 100644 index 0000000..c180282 --- /dev/null +++ b/src/game/protocol.rs @@ -0,0 +1,204 @@ +use anyhow::{anyhow, bail, Result}; +use glam::IVec2; +use log::debug; +use serde::Serialize; +use std::{ + fmt::Display, + io::{BufRead, BufReader, BufWriter, Lines, Write}, + net::TcpStream, + str::FromStr, +}; + +pub struct Client { + rx: Lines>, + tx: BufWriter, +} +impl Client { + pub fn new(host: &str) -> Result { + let sock = TcpStream::connect(host)?; + let rx = BufReader::new(sock.try_clone()?).lines(); + let tx = BufWriter::new(sock); + Ok(Self { rx, tx }) + } + pub fn read(&mut self) -> Result { + let line = self.rx.next().ok_or(anyhow!("eof"))??; + let packet = Packet::parse(&line)?; + debug!("<- {packet:?}"); + Ok(packet) + } + pub fn write(&mut self, packet: Packet) -> Result<()> { + debug!("-> {packet:?}"); + self.tx.write_all(packet.dump().as_bytes())?; + self.tx.flush()?; + Ok(()) + } +} + +impl Packet { + pub fn dump(&self) -> String { + match self { + Packet::Join { username, password } => { + format!("join|{username}|{password}") + } + Packet::Move(dir) => { + format!("move|{dir}") + } + Packet::Chat(message) => { + format!("chat|{message}") + } + Packet::Motd(motd) => format!("motd|{motd}"), + Packet::Error(message) => format!("erro|{message}"), + Packet::Game { + my_id, + width, + height, + } => format!("game|{width}|{height}|{my_id}"), + Packet::Pos { id, x, y } => format!("pos|{id}|{x}|{y}"), + Packet::Player { id, name } => format!("player|{id}|{name}"), + Packet::Tick => format!("tick"), + Packet::Die(players) => format!( + "die|{}", + players + .into_iter() + .map(|x| format!("{x}")) + .collect::>() + .join("|") + ), + Packet::Message { id, message } => format!("message|{id}|{message}"), + Packet::Win(x, y) => format!("win|{x}|{y}"), + Packet::Lose(x, y) => format!("lose|{x}|{y}"), + } + } + pub fn parse(line: &str) -> Result { + let mut toks = line.split("|"); + Ok(match toks.next().ok_or(anyhow!("packet type missing"))? { + "motd" => Packet::Motd(toks.next().ok_or(anyhow!("motd missing"))?.to_string()), + "error" => Packet::Error( + toks.next() + .ok_or(anyhow!("error message missing"))? + .to_string(), + ), + "pos" => Packet::Pos { + id: toks.next().ok_or(anyhow!("id missing"))?.parse()?, + x: toks.next().ok_or(anyhow!("x missing"))?.parse()?, + y: toks.next().ok_or(anyhow!("y missing"))?.parse()?, + }, + "game" => Packet::Game { + width: toks.next().ok_or(anyhow!("width missing"))?.parse()?, + height: toks.next().ok_or(anyhow!("height missing"))?.parse()?, + my_id: toks.next().ok_or(anyhow!("my id missing"))?.parse()?, + }, + "player" => Packet::Player { + id: toks.next().ok_or(anyhow!("id missing"))?.parse()?, + name: toks.next().ok_or(anyhow!("name missing"))?.to_string(), + }, + "tick" => Packet::Tick, + "die" => Packet::Die(toks.map(|e| e.parse()).try_collect()?), + "message" => Packet::Message { + id: toks.next().ok_or(anyhow!("id missing"))?.parse()?, + message: toks.next().ok_or(anyhow!("message missing"))?.to_string(), + }, + "win" => Packet::Win( + toks.next().ok_or(anyhow!("something missing"))?.parse()?, + toks.next().ok_or(anyhow!("something missing"))?.parse()?, + ), + "lose" => Packet::Lose( + toks.next().ok_or(anyhow!("something missing"))?.parse()?, + toks.next().ok_or(anyhow!("something missing"))?.parse()?, + ), + "move" => Packet::Move(Direction::from_str( + toks.next().ok_or(anyhow!("direction missing"))?, + )?), + "join" => Packet::Join { + username: toks.next().ok_or(anyhow!("username missing"))?.to_string(), + password: toks.next().ok_or(anyhow!("password missing"))?.to_string(), + }, + x => bail!("unknown command: {x:?}"), + }) + } +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "snake_case")] +pub enum Packet { + Motd(String), + Error(String), + Game { + my_id: u32, + width: usize, + height: usize, + }, + Pos { + id: u32, + x: i32, + y: i32, + }, + Player { + id: u32, + name: String, + }, + Tick, + Die(Vec), + Message { + id: u32, + message: String, + }, + Win(usize, usize), + Lose(usize, usize), + Join { + username: String, + password: String, + }, + Move(Direction), + Chat(String), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum Direction { + Up, + Down, + Left, + Right, +} + +impl Display for Direction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Direction::Up => "up", + Direction::Down => "down", + Direction::Left => "left", + Direction::Right => "right", + }) + } +} +impl FromStr for Direction { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + Ok(match s { + "up" => Direction::Up, + "down" => Direction::Down, + "left" => Direction::Left, + "right" => Direction::Right, + x => bail!("unknown direction: {x:?}"), + }) + } +} +impl Direction { + pub const ALL: [Direction; 4] = [Self::Up, Self::Down, Self::Left, Self::Right]; + pub fn vector(self) -> IVec2 { + match self { + Direction::Up => (0, -1).into(), + Direction::Down => (0, 1).into(), + Direction::Left => (-1, 0).into(), + Direction::Right => (1, 0).into(), + } + } + pub fn rotate_ccw(self) -> Self { + match self { + Direction::Up => Direction::Right, + Direction::Down => Direction::Left, + Direction::Left => Direction::Up, + Direction::Right => Direction::Down, + } + } +} diff --git a/src/game/server.rs b/src/game/server.rs new file mode 100644 index 0000000..1775bd1 --- /dev/null +++ b/src/game/server.rs @@ -0,0 +1,46 @@ +use super::{ + protocol::{Direction, Packet}, + Config, +}; +use crate::State; +use anyhow::{bail, Result}; +use glam::IVec2; +use log::{error, info}; +use std::sync::Arc; +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter}, + net::{TcpListener, TcpStream}, + spawn, +}; + +pub async fn game_server(config: Config, state: Arc) -> Result<()> { + let listener = TcpListener::bind(config.bind).await?; + info!("listening on {}", listener.local_addr()?); + while let Ok((sock, addr)) = listener.accept().await { + spawn(async move { + info!("connected {addr}"); + if let Err(e) = handle_client(sock).await { + error!("client error: {e}") + } + info!("disconnected {addr}"); + }); + } + bail!("accept failure") +} + +async fn handle_client(sock: TcpStream) -> Result<()> { + let (rx, tx) = sock.into_split(); + let rx = BufReader::new(rx); + let mut tx = BufWriter::new(tx); + + let mut lines = rx.lines(); + + while let Some(line) = lines.next_line().await? { + let packet = Packet::parse(&line)?; + + tx.write_all(packet.dump().as_bytes()).await?; + tx.flush().await?; + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..368f439 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,31 @@ +#![feature(iterator_try_collect)] +use config::Config; +use game::{server::game_server, Game}; +use spectate::server::spectate_server; +use std::sync::Arc; +use tokio::{ + spawn, + sync::{broadcast, RwLock}, +}; + +pub mod config; +pub mod game; +pub mod spectate; + +pub struct State { + tick: broadcast::Sender, // true for new game + game: RwLock, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init_from_env("LOG"); + let config = Config::load()?; + let state = Arc::new(State { + tick: broadcast::channel(16).0, + game: Game::new(vec![]).into(), + }); + spawn(spectate_server(config.spectate, state.clone())); + game_server(config.game, state.clone()).await?; + Ok(()) +} diff --git a/src/spectate/index.html b/src/spectate/index.html new file mode 100644 index 0000000..235a894 --- /dev/null +++ b/src/spectate/index.html @@ -0,0 +1,11 @@ + + + + + + GPN Tron + + + + + diff --git a/src/spectate/mod.rs b/src/spectate/mod.rs new file mode 100644 index 0000000..f80551e --- /dev/null +++ b/src/spectate/mod.rs @@ -0,0 +1,9 @@ +use serde::Deserialize; +use std::net::SocketAddr; + +pub mod server; + +#[derive(Deserialize)] +pub struct Config { + bind: SocketAddr, +} diff --git a/src/spectate/server.rs b/src/spectate/server.rs new file mode 100644 index 0000000..8fa1a3d --- /dev/null +++ b/src/spectate/server.rs @@ -0,0 +1,121 @@ +use crate::game::protocol::Packet; +use crate::State; + +use super::Config; +use anyhow::Result; +use axum::extract; +use axum::extract::connect_info::ConnectInfo; +use axum::extract::ws::Message; +use axum::response::Html; +use axum::{ + extract::ws::{WebSocket, WebSocketUpgrade}, + response::IntoResponse, + routing::get, + Router, +}; +use log::{info, warn}; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::spawn; +use tokio::sync::{broadcast, RwLock}; + +struct SpectateState { + past_events: RwLock>, + events: broadcast::Sender, +} + +pub async fn spectate_server(config: Config, state: Arc) -> Result<()> { + let sstate = Arc::new(SpectateState { + past_events: Default::default(), + events: broadcast::channel(16).0, + }); + spawn(broadcaster(sstate.clone(), state)); + let app = Router::new() + .route("/", get(index)) + .route("/events", get(ws_handler)) + .with_state(sstate); + let listener = tokio::net::TcpListener::bind(config.bind).await.unwrap(); + info!("listening on {}", listener.local_addr()?); + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .await?; + Ok(()) +} + +async fn index() -> Html<&'static str> { + Html(include_str!("index.html")) +} + +async fn broadcaster(sstate: Arc, state: Arc) { + let mut ticks = state.tick.subscribe(); + while let Ok(new_game) = ticks.recv().await { + let mut events = Vec::new(); + + { + let g = state.game.read().await; + if new_game { + sstate.past_events.write().await.clear(); + events.push(Packet::Game { + my_id: 0, + width: g.size.x as usize, + height: g.size.y as usize, + }); + for (player, (_, _, name)) in &g.heads { + events.push(Packet::Player { + id: *player, + name: name.to_owned(), + }) + } + } + for (player, (_, pos, _)) in &g.heads { + events.push(Packet::Pos { + id: *player, + x: pos.x, + y: pos.y, + }) + } + if !g.dead.is_empty() { + events.push(Packet::Die(g.dead.clone())); + } + } + + sstate.past_events.write().await.extend(events.clone()); + for ev in events { + let _ = sstate.events.send(ev); + } + } +} + +async fn ws_handler( + ws: WebSocketUpgrade, + ConnectInfo(addr): ConnectInfo, + extract::State(state): extract::State>, +) -> impl IntoResponse { + ws.on_upgrade(move |socket| async move { + if let Err(e) = handle_socket(socket, addr, state).await { + warn!("client error {e}") + } + }) +} + +async fn handle_socket( + mut socket: WebSocket, + _addr: SocketAddr, + state: Arc, +) -> anyhow::Result<()> { + let past = state.past_events.read().await.clone(); + for p in past { + socket + .send(Message::Text(serde_json::to_string(&p)?)) + .await?; + } + let mut live = state.events.subscribe(); + while let Ok(p) = live.recv().await { + socket + .send(Message::Text(serde_json::to_string(&p)?)) + .await?; + } + Ok(()) +} -- cgit v1.2.3-70-g09d2