From cf9392a33bb99ae581f818d3ddb8be1231521a02 Mon Sep 17 00:00:00 2001 From: Katharina Fey Date: Sat, 6 Feb 2021 19:40:53 +0100 Subject: rstnode: restructure project into workspace and sub-crates --- games/rstnode/rst-core/Cargo.toml | 29 ++++ games/rstnode/rst-core/src/_if.rs | 58 +++++++ games/rstnode/rst-core/src/_loop.rs | 41 +++++ games/rstnode/rst-core/src/_match.rs | 72 ++++++++ games/rstnode/rst-core/src/bin/game.rs | 3 + games/rstnode/rst-core/src/bin/server.rs | 3 + games/rstnode/rst-core/src/config/io.rs | 45 +++++ games/rstnode/rst-core/src/config/mod.rs | 50 ++++++ games/rstnode/rst-core/src/data.rs | 279 ++++++++++++++++++++++++++++++ games/rstnode/rst-core/src/gens.rs | 92 ++++++++++ games/rstnode/rst-core/src/io.rs | 22 +++ games/rstnode/rst-core/src/lib.rs | 39 +++++ games/rstnode/rst-core/src/lobby.rs | 191 ++++++++++++++++++++ games/rstnode/rst-core/src/map.rs | 80 +++++++++ games/rstnode/rst-core/src/mapstore.rs | 17 ++ games/rstnode/rst-core/src/runner.rs | 1 + games/rstnode/rst-core/src/server.rs | 155 +++++++++++++++++ games/rstnode/rst-core/src/stats.rs | 183 ++++++++++++++++++++ games/rstnode/rst-core/src/users.rs | 69 ++++++++ games/rstnode/rst-core/src/wire/action.rs | 33 ++++ games/rstnode/rst-core/src/wire/mod.rs | 68 ++++++++ games/rstnode/rst-core/src/wire/req.rs | 38 ++++ games/rstnode/rst-core/src/wire/resp.rs | 94 ++++++++++ games/rstnode/rst-core/src/wire/update.rs | 72 ++++++++ 24 files changed, 1734 insertions(+) create mode 100644 games/rstnode/rst-core/Cargo.toml create mode 100644 games/rstnode/rst-core/src/_if.rs create mode 100644 games/rstnode/rst-core/src/_loop.rs create mode 100644 games/rstnode/rst-core/src/_match.rs create mode 100644 games/rstnode/rst-core/src/bin/game.rs create mode 100644 games/rstnode/rst-core/src/bin/server.rs create mode 100644 games/rstnode/rst-core/src/config/io.rs create mode 100644 games/rstnode/rst-core/src/config/mod.rs create mode 100644 games/rstnode/rst-core/src/data.rs create mode 100644 games/rstnode/rst-core/src/gens.rs create mode 100644 games/rstnode/rst-core/src/io.rs create mode 100644 games/rstnode/rst-core/src/lib.rs create mode 100644 games/rstnode/rst-core/src/lobby.rs create mode 100644 games/rstnode/rst-core/src/map.rs create mode 100644 games/rstnode/rst-core/src/mapstore.rs create mode 100644 games/rstnode/rst-core/src/runner.rs create mode 100644 games/rstnode/rst-core/src/server.rs create mode 100644 games/rstnode/rst-core/src/stats.rs create mode 100644 games/rstnode/rst-core/src/users.rs create mode 100644 games/rstnode/rst-core/src/wire/action.rs create mode 100644 games/rstnode/rst-core/src/wire/mod.rs create mode 100644 games/rstnode/rst-core/src/wire/req.rs create mode 100644 games/rstnode/rst-core/src/wire/resp.rs create mode 100644 games/rstnode/rst-core/src/wire/update.rs (limited to 'games/rstnode/rst-core') diff --git a/games/rstnode/rst-core/Cargo.toml b/games/rstnode/rst-core/Cargo.toml new file mode 100644 index 000000000000..da0d3fa0ad55 --- /dev/null +++ b/games/rstnode/rst-core/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "rst-core" +description = "Shared logic and type state library for rstnode" +version = "0.0.0" +edition = "2018" +license = "AGPL-3.0-or-later" +authors = ["Bread Machine", "Katharina Fey "] + +[dependencies] + +# Serialisation +bincode = "1.0" +serde = { version = "1.0", features = ["derive", "rc"] } +serde_yaml = "0.8.15" + +# Async utils +async-std = { version = "1.0", features = ["unstable"] } +async-trait = "0.1" + +# Ratman networking utils +identity = { version = "0.4", features = ["random"], package = "ratman-identity"} +netmod-mem = "0.1.0" +ratman = "0.1" + +# Other dependencies +chrono = { version = "0.4", features = ["serde"] } +const_env = "0.1" +quadtree_rs = "0.1" +rand = "0.7" diff --git a/games/rstnode/rst-core/src/_if.rs b/games/rstnode/rst-core/src/_if.rs new file mode 100644 index 000000000000..1a842840708c --- /dev/null +++ b/games/rstnode/rst-core/src/_if.rs @@ -0,0 +1,58 @@ +//! A common trait interface between the server and the client + +use crate::wire::{ + Action, AuthErr, Lobby, LobbyErr, LobbyId, LobbyUpdate, MatchErr, MatchId, RegErr, UpdateState, + User, UserId, +}; +use async_std::sync::Arc; +use async_trait::async_trait; +use chrono::{DateTime, Utc}; + +/// Main game interface implemented by the server and client +/// +/// The client implementation simply translates requests to network +/// requests that are sent to the server. The server implementation +/// consists of two parts: the network layer listening loop, and the +/// game server state which then implements the actual game logic. +#[async_trait] +pub trait GameIf { + /// Register a new user on a game server + async fn register(self: Arc, name: String, pw: String) -> Result; + + /// Login for an existing user + async fn login(self: Arc, name: String, pw: String) -> Result; + + /// End a user session (go offline) + async fn logout(self: Arc, user: User) -> Result<(), AuthErr>; + + /// Register as an anonymous player + async fn anonymous(self: Arc, name: String) -> Result; + + /// Join a match-making lobby + async fn join(self: Arc, user: User, lobby: LobbyId) -> Result; + + /// Leave a match-making lobby + async fn leave(self: Arc, user: User, lobby: LobbyId) -> Result<(), LobbyErr>; + + /// Set the player's ready state + async fn ready( + self: Arc, + user: User, + lobby: LobbyId, + ready: bool, + ) -> Result; + + /// Send a start request (as lobby admin) + async fn start_req( + self: Arc, + user: UserId, + lobby: LobbyId, + ) -> Result, LobbyErr>; + + /// Perform a game action as a user + async fn perform_action(self: Arc, user: User, mtch: MatchId, act: Action) + -> UpdateState; + + /// Leave a match + async fn leave_match(self: Arc, user: User, mtch: MatchId) -> Result<(), MatchErr>; +} diff --git a/games/rstnode/rst-core/src/_loop.rs b/games/rstnode/rst-core/src/_loop.rs new file mode 100644 index 000000000000..ac4619622ed3 --- /dev/null +++ b/games/rstnode/rst-core/src/_loop.rs @@ -0,0 +1,41 @@ +//! A timed loop implementation +//! +//! This is a utility to be used in the server simulation to make sure +//! that a simulation step takes a certain amount of time to execute. + +use async_std::{future::Future, task}; +use chrono::{DateTime, Utc}; +use std::{ + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, +}; + +/// Number of ticks per second +#[from_env("RSTNODE_TICKS")] +const TICKS: u64 = 100; +const TICK_TIME: Duration = Duration::from_millis(1000 / TICKS); + +/// Run a timed loop until you no longer want to +pub(crate) fn block_loop(run: Arc, f: F) +where + F: Future + Send + Copy + 'static, +{ + while run.load(Ordering::Relaxed) { + let t1 = Utc::now(); + task::block_on(f); + let t2 = Utc::now(); + let t3 = (t2 - t1).to_std().unwrap(); + task::block_on(async { task::sleep(TICK_TIME - t3) }); + } +} + +/// Run a detached timed loop until you no longer want to +pub(crate) fn spawn_loop(run: Arc, f: F) +where + F: Future + Send + Copy + 'static, +{ + task::spawn(async move { block_loop(run, f) }); +} diff --git a/games/rstnode/rst-core/src/_match.rs b/games/rstnode/rst-core/src/_match.rs new file mode 100644 index 000000000000..ce75af5af393 --- /dev/null +++ b/games/rstnode/rst-core/src/_match.rs @@ -0,0 +1,72 @@ +use crate::{ + data::Player, + lobby::MetaLobby, + map::Map, + wire::{Action, LobbyUser, MatchId, UserId}, +}; +use async_std::sync::{Arc, RwLock}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; + +/// Describes a match for the server +/// +/// This type implements the partial [GameIf](crate::GameIf) API to +/// allow client to queue commands from players. The server +/// implementation runs a simulation for each match that is running on +/// it. +pub struct Match { + /// The match id + pub id: MatchId, + /// The list of active players + pub players: Vec, + /// The active game map + pub map: Map, + /// Input inbox (handled in-order each game tick) + inbox: RwLock>, + /// The time the match was initialised + init_t: DateTime, + /// The synced time the match was started + start_t: Option>, +} + +impl From for Match { + fn from(ml: MetaLobby) -> Self { + Self { + id: ml.inner.id, + players: ml + .inner + .players + .into_iter() + .map(|lu| Player { + id: lu.id, + name: lu.name, + color: lu.color, + money: 0.into(), + }) + .collect(), + map: Map::new(), + inbox: Default::default(), + init_t: Utc::now(), + start_t: None, + } + } +} + +impl Match { + /// Set the start time of the match, which may be in the future + pub fn set_start(&mut self, t: DateTime) { + self.start_t = Some(t); + } + + /// Queue a new game action + pub async fn queue(&self, cmd: Action) { + self.inbox.write().await.push_back(cmd); + } + + pub async fn handle_inbox(&mut self) { + // for act in self.inbox.write().await.drain() { + + // } + } +} diff --git a/games/rstnode/rst-core/src/bin/game.rs b/games/rstnode/rst-core/src/bin/game.rs new file mode 100644 index 000000000000..f826c64689db --- /dev/null +++ b/games/rstnode/rst-core/src/bin/game.rs @@ -0,0 +1,3 @@ +//! The main game UI client + +fn main() {} diff --git a/games/rstnode/rst-core/src/bin/server.rs b/games/rstnode/rst-core/src/bin/server.rs new file mode 100644 index 000000000000..357ddf944dc4 --- /dev/null +++ b/games/rstnode/rst-core/src/bin/server.rs @@ -0,0 +1,3 @@ +//! The dedicated server binary + +fn main() {} diff --git a/games/rstnode/rst-core/src/config/io.rs b/games/rstnode/rst-core/src/config/io.rs new file mode 100644 index 000000000000..07aa7e7b3d3f --- /dev/null +++ b/games/rstnode/rst-core/src/config/io.rs @@ -0,0 +1,45 @@ +//! I/O utilities to load configurations from disk + +use super::MapCfg; +use serde_yaml::from_str; +use std::{ + fs::File, + io::{self, Read}, + path::Path, +}; + +/// Encode error state while loading a map configuration +pub enum Error { + Io(String), + Parsing(String), +} + +impl From for Error { + fn from(e: io::Error) -> Self { + Self::Io(e.to_string()) + } +} + +impl From for Error { + fn from(e: serde_yaml::Error) -> Self { + Self::Parsing(e.to_string()) + } +} + +/// Load a map YAML configuration from disk +/// +/// This file format is structured according to the configuration +/// types in [config](crate::config). An example configuration is +/// provided below. +/// +/// ```yaml +/// nodes: +/// - +/// ``` +pub fn load_map<'p>(p: impl Into<&'p Path>) -> Result { + let mut f = File::open(p.into())?; + let mut s = String::new(); + f.read_to_string(&mut s)?; + + Ok(from_str(s.as_str())?) +} diff --git a/games/rstnode/rst-core/src/config/mod.rs b/games/rstnode/rst-core/src/config/mod.rs new file mode 100644 index 000000000000..447c0fcbcc35 --- /dev/null +++ b/games/rstnode/rst-core/src/config/mod.rs @@ -0,0 +1,50 @@ +//! The file formats backing maps and other configs +//! +//! When creating maps to share with other clients, or to load into a +//! server, these types can be constructed with the YML description +//! format, outlined in [load_map](config::io::load_map) + +mod io; +pub use io::*; + +use crate::data::{LinkId, NodeId}; +use serde::{Deserialize, Serialize}; + +/// A config tree that describes a map +#[derive(Serialize, Deserialize)] +pub struct MapCfg { + /// The set of nodes + pub nodes: Vec, + /// Links connecting nodes + pub links: Vec, + /// Default spawn points (player count) + pub spawns: Vec, +} + +/// A single node on a map, with an x and y coordinate +#[derive(Serialize, Deserialize)] +pub struct NodeCfg { + /// Node ID + pub id: NodeId, + /// Render/world position + pub x: f64, + pub y: f64, +} + +/// A link between two nodes +#[derive(Serialize, Deserialize)] +pub struct LinkCfg { + /// The link ID + id: LinkId, + /// List of connectioned nodes + con: (NodeId, NodeId), +} + +/// Special configuration for a node to act as a player spawn point +#[derive(Serialize, Deserialize)] +pub struct SpawnCfg { + /// The node of the spawn point + pub n_id: NodeId, + /// At what number of players is this spawn available? + pub max_players: usize, +} diff --git a/games/rstnode/rst-core/src/data.rs b/games/rstnode/rst-core/src/data.rs new file mode 100644 index 000000000000..5ce5517c2b07 --- /dev/null +++ b/games/rstnode/rst-core/src/data.rs @@ -0,0 +1,279 @@ +//! Data structures for the game + +use crate::io::Io; +use async_std::sync::Arc; +use rand::seq::SliceRandom; +use rand::thread_rng; +use serde::{Deserialize, Serialize}; +use std::{ + collections::BTreeMap, + sync::atomic::{AtomicBool, AtomicU16, AtomicU32}, +}; + +pub type NodeId = usize; + +/// A node is a computer on the network graph +/// +/// It's owned by a player, and has some upgrade state, as well as +/// base stats. +#[derive(Serialize, Deserialize)] +pub struct Node { + /// Each node has a unique ID by which it's addressed + pub id: NodeId, + /// The current health + pub health: AtomicU32, + /// The max health + pub max_health: AtomicU32, + /// The owner of this node + pub owner: Owner, + /// Upgrade state + pub type_: Upgrade, + /// Number of links on the map + pub links: u8, + /// Active link states + pub link_states: Vec>, + /// Input buffer + #[serde(skip)] + pub buffer: Vec, +} + +pub type LinkId = usize; + +/// A one-to-one link between two nodes +#[derive(Serialize, Deserialize)] +pub struct Link { + /// This link ID + id: LinkId, + /// Node 1 + a: NodeId, + /// Node 2 + b: NodeId, + /// The step length + length: usize, + /// Packets present on this link + #[serde(skip)] + pp: Vec<(Packet, AtomicU32)>, + /// Actual Rx, Tx pair + #[serde(skip)] + io: Io, +} + +pub type PacketId = usize; + +/// A packet going across the network +pub struct Packet { + /// The packet ID + id: PacketId, + /// Declare this packet to be removed + dead: AtomicBool, + /// Each packet is owned by a player + owner: Arc, + /// What type of packet this is + data_: PacketType, +} + +pub type PlayerId = usize; + +/// A player who's having fun +#[derive(Serialize, Deserialize)] +pub struct Player { + /// A unique player ID (per match) + pub id: PlayerId, + /// The player name + pub name: String, + /// Player color + pub color: Color, + /// The player's money + pub money: AtomicU16, +} + +/// Optionally, players can create teams +#[derive(Serialize, Deserialize)] +pub struct Team { + /// Name of the team + name: String, + /// Unified color of the team + color: Color, + /// All team members by their ID + roster: Vec, +} + +/// An RGB color without alpha +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Color(u8, u8, u8); + +impl Color { + pub fn black() -> Self { + Self(50, 50, 50) + } + + pub fn red() -> Self { + Self(250, 50, 50) + } + + pub fn green() -> Self { + Self(100, 250, 100) + } + + pub fn blue() -> Self { + Self(100, 100, 250) + } + + pub fn teal() -> Self { + Self(150, 250, 250) + } + + pub fn purple() -> Self { + Self(150, 100, 250) + } + + pub fn orange() -> Self { + Self(250, 200, 100) + } + + pub fn yellow() -> Self { + Self(250, 250, 100) + } + + pub fn white() -> Self { + Self(225, 225, 225) + } +} + +pub trait ColorPalette { + /// Create a new color palette + fn palette() -> Self; + /// Get a palette without a certain colour + fn without(&mut self, b: &Color); + /// Mix a color back into the available palette + fn remix(&mut self, new: Color); +} + +impl ColorPalette for Vec { + fn palette() -> Self { + let mut rng = thread_rng(); + let mut pal = vec![ + Color::black(), + Color::red(), + Color::green(), + Color::blue(), + Color::teal(), + Color::purple(), + Color::orange(), + Color::yellow(), + Color::white(), + ]; + pal.shuffle(&mut rng); + pal + } + + /// Drop a colour from the palette + fn without(&mut self, b: &Color) { + if let Some(pos) = self.into_iter().rposition(|a| a == b) { + self.remove(pos); + } + } + + fn remix(&mut self, new: Color) { + let mut rng = thread_rng(); + self.push(new); + self.shuffle(&mut rng); + } +} + +/// Describes ownership state +#[derive(Serialize, Deserialize)] +pub enum Owner { + /// Nobody owns this + Neutral, + /// A player owns this + Player(Player), +} + +/// Encodes upgrade level without numbers +#[derive(Copy, Clone, Serialize, Deserialize)] +pub enum Level { + /// 1 + One, + /// 2 + Two, + /// 3 (wow) + Three, +} + +/// Describes upgrade state +#[derive(Copy, Clone, Serialize, Deserialize)] +pub enum Upgrade { + /// A basic node + Base, + /// Battle (attack/defence) nodes + Guard(Level), + /// These nodes make money + Compute(Level), + /// Good at packet switching + Relay(Level), +} + +/// Possible types of packets +pub enum PacketType { + /// A keepalive packet + /// + /// These are sent by all nodes if their neighbours are either + /// friendly or neutral, and used to keep the network alive. + /// Sending them costs nothing, and when they are received, they + /// yield a small amount of funds, and restoring health of a node. + Ping, + /// A non exploit capture + /// + /// This is a packet that can be sent out by any node (except a + /// switch) to claim a neutral node on the network. The path to + /// the node needs to consist only of friendlies, and if an enemy + /// node processes this type, nothing happens. + Capture, + /// A compute packet + /// + /// The first value is the target compute value, which each + /// compute node adds on to. The second value is the current. If + /// a compute packet passes through a compromised or enemy node, + /// it might subtract from the second value, before palling it on. + Compute { + max: u16, + curr: AtomicU16, + step: u16, + }, + /// A special wrapper packet generated by guards + /// + /// Sometimes, when a hostily attack packet encounters a guard, it + /// manages to capture the attack, and forwards it to a random + /// compute node. If the node manages to handle the packet, + /// without it getting dropped in the meantime), it yields a + /// specified reward, like a computation would. + Payload { inner: Box, reward: u16 }, + /// A reset attack packet + /// + /// When encountering a hostile node, it will make that node drop + /// all packets in it's buffers. + Reset, + /// Cross-node-scripting attack + /// + /// Decreases the strength of a node, also having a small chance + /// of spawning a new packet into a random output buffer. When + /// applied to a neutral node, it makes capturing nodes go faster. + CNS, + /// Node-in-the-middle attack + /// + /// Infect the routing behaviour of a node to route all traffic to + /// a specified enemy node instead + Nitm, + /// Virus infection attack + /// + /// Infects a node to capture it's earnings, both active and + /// passive, for a short time, without taking on it's costs. + Virus, + /// A total control exploit + /// + /// This is very hard to do, and a node will basically always + /// resist it, but if successful, transforms the node into a guard + /// node and yields control to the attackinng player. + TakeOver, +} diff --git a/games/rstnode/rst-core/src/gens.rs b/games/rstnode/rst-core/src/gens.rs new file mode 100644 index 000000000000..e6859f04ec66 --- /dev/null +++ b/games/rstnode/rst-core/src/gens.rs @@ -0,0 +1,92 @@ +//! Helpers to determine if a node can send a particular packet. + +/// These functions are unfortunately all a bit stringly typed atm +pub mod can_send { + use crate::data::{Level, PacketType, Upgrade}; + use Level::*; + + #[inline] + pub fn base() -> Vec<&'static str> { + vec!["ping", "capture"] + } + + /// Guard nodes are the most versatile + #[inline] + pub fn guard(lvl: Level) -> Vec<&'static str> { + match lvl { + // This is just kinda gross + One => { + let mut p = base(); + p.append(&mut vec!["payload", "cns", "reset"]); + p + } + Two => { + let mut p = guard(One); + p.append(&mut vec!["nitm", "virus"]); + p + } + Three => { + let mut p = guard(Two); + p.append(&mut vec!["takeover"]); + p + } + } + } + + /// Compute nodes can ping and compute but not capture + pub fn compute() -> Vec<&'static str> { + vec!["ping", "compute"] + } + + /// Relays only relay! + pub fn relay() -> Vec<&'static str> { + vec![] + } +} + +pub mod send { + use crate::{ + data::{ + Level::*, + PacketType as Pkt, + Upgrade::{self, *}, + }, + stats::strengths, + }; + + /// Turn the string type identifier into a packet + /// + /// This function makes heavy use of the stats module, which is + /// responsible for balancing all of these values. + pub fn build_packet(node: Upgrade, type_: &'static str, prev: Option) -> Pkt { + match (prev, node, type_) { + // All pings are the same + (None, _, "ping") => Pkt::Ping, + + // TODO: captures should improve + (None, Base, "capture") | (None, Guard(_), "capture") => Pkt::Capture, + + (None, Compute(lvl), "compute") => Pkt::Compute { + curr: Default::default(), + max: strengths::compute_max(lvl), + step: strengths::compute_step(lvl), + }, + + (Some(prev), Guard(lvl), "payload") => Pkt::Payload { + inner: Box::new(prev), + reward: strengths::payload_reward(lvl), + }, + + (None, Guard(_), "cns") => Pkt::CNS, + (None, Guard(_), "reset") => Pkt::Reset, + (None, Guard(_), "nitm") => Pkt::Nitm, + (None, Guard(_), "virus") => Pkt::Virus, + + // Only level 3 guards can send takeovers + (None, Guard(Three), "takeover") => Pkt::TakeOver, + + // Can't touch this + (_, _, _) => unreachable!(), + } + } +} diff --git a/games/rstnode/rst-core/src/io.rs b/games/rstnode/rst-core/src/io.rs new file mode 100644 index 000000000000..3032c2698190 --- /dev/null +++ b/games/rstnode/rst-core/src/io.rs @@ -0,0 +1,22 @@ +//! A module that adapts the ratman layer to RstNode + +use netmod_mem::MemMod; + +/// A pair of exits, connected +pub struct Io { + a: MemMod, + b: MemMod, +} + +impl Io { + fn new() -> Self { + Self::default() + } +} + +impl Default for Io { + fn default() -> Self { + let (a, b) = MemMod::make_pair(); + Self { a, b } + } +} diff --git a/games/rstnode/rst-core/src/lib.rs b/games/rstnode/rst-core/src/lib.rs new file mode 100644 index 000000000000..36e232f4639c --- /dev/null +++ b/games/rstnode/rst-core/src/lib.rs @@ -0,0 +1,39 @@ +//! # RST Node +//! +//! RST Node is a real-time strategy game about computers on a +//! network, fighting for dominance against a set of other network +//! operators. To operate a successful network you need to build +//! infrastructure, compute clusters, and defences on edge nodes. +//! +//! The game architecture is split between the game client and game +//! server. This library implements all required types and functions +//! to manage this state over a network connection. +//! +//! The main game interface is provided by [GameIf](crate::GameIf), +//! which is them implemented by [Server](crate::server::Server), and +//! [MatchClient](crate::client::MatchClient). + +#[macro_use] +extern crate const_env; + +pub(crate) mod _loop; + +mod _if; +pub use _if::GameIf; + +mod _match; +pub use _match::Match; + +pub mod config; +pub mod data; +pub mod gens; +pub mod io; +pub mod lobby; +pub mod map; +pub mod mapstore; +pub mod server; +pub mod stats; +pub mod users; +pub mod wire; + +pub use identity::Identity as Id; diff --git a/games/rstnode/rst-core/src/lobby.rs b/games/rstnode/rst-core/src/lobby.rs new file mode 100644 index 000000000000..496f98bd7b6f --- /dev/null +++ b/games/rstnode/rst-core/src/lobby.rs @@ -0,0 +1,191 @@ +//! The code that handles the lobby logic + +use crate::{ + data::{Color, ColorPalette}, + users::MetaUser, + wire::{Lobby, LobbyErr, LobbyId, LobbyUpdate, LobbyUser, User, UserId}, +}; +use async_std::sync::{Arc, RwLock}; +use std::{ + collections::BTreeMap, + sync::atomic::{AtomicUsize, Ordering}, +}; + +/// A list of all the lobbies on the server +pub struct LobbyList { + max: AtomicUsize, + lobbies: RwLock>, +} + +impl LobbyList { + pub fn new() -> Self { + Self { + max: 0.into(), + lobbies: Default::default(), + } + } + + /// Create a new lobby + pub async fn create(&self, map: String) -> LobbyId { + let id = self.max.fetch_add(1, Ordering::Relaxed); + self.lobbies + .write() + .await + .insert(id, MetaLobby::create(id, map)); + id + } + + /// Remove a lobby by ID + pub async fn destroy(&self, id: LobbyId) -> Result<(), LobbyErr> { + self.consume(id).await.map(|_| ()) + } + + /// Remove and return the lobby + pub async fn consume(&self, id: LobbyId) -> Result { + self.lobbies + .write() + .await + .remove(&id) + .map_or(Err(LobbyErr::NoSuchRoom), |l| Ok(l)) + } + + /// Get mutable access to a lobby + pub async fn get_mut(&self, id: LobbyId, cb: F) -> Result + where + F: Fn(&mut MetaLobby) -> T, + { + self.lobbies + .write() + .await + .get_mut(&id) + .map_or(Err(LobbyErr::OtherError), |ref mut l| Ok(cb(l))) + } +} + +/// Additional state held by the server +/// +/// The meta lobby will also sync updates to all connected users, when updates are made to the lobby +pub struct MetaLobby { + pub palette: Vec, + pub inner: Lobby, +} + +impl MetaLobby { + pub fn create(id: LobbyId, map: String) -> Self { + Self { + palette: Vec::palette(), + inner: Lobby { + id, + map, + players: vec![], + settings: vec![], + }, + } + } + + pub fn join(&mut self, user: &MetaUser) -> Lobby { + let color = if &user.name == "spacekookie" { + let color = Color::blue(); + self.palette.without(&color); + + if let Some(user) = self + .inner + .players + .iter_mut() + .find(|u| u.color == Color::blue()) + { + user.color = self.palette.remove(0); + } + + color + } else { + self.palette.remove(0) + }; + + self.inner.players.push(LobbyUser { + admin: false, + id: user.id, + name: user.name.clone(), + ready: false, + color, + }); + + self.inner.clone() + } + + pub fn leave(&mut self, user: &MetaUser) { + let (pos, user) = self + .inner + .players + .iter() + .enumerate() + .find_map(|(num, u)| { + if u.id == user.id { + Some((num, u)) + } else { + None + } + }) + .unwrap(); + self.palette.remix(user.color); + self.inner.players.remove(pos); + } + + /// Check if a user is even present in a lobby + /// + /// Perform this prerequisite check before making other user-specific changes to the lobby + pub fn in_lobby(&self, user: UserId) -> bool { + self.inner + .players + .iter() + .find(|u| u.id == user) + .map(|_| true) + .unwrap_or(false) + } + + /// Set the ready state for a user + pub fn ready(&mut self, user: User, ready: bool) -> LobbyUpdate { + if let Some(user) = self + .inner + .players + .iter_mut() + .find(|u| u.id == user.id) + .as_mut() + { + user.ready = ready; + } + + LobbyUpdate::Ready( + self.inner + .players + .iter() + .filter_map(|u| if u.ready { Some(u.id) } else { None }) + .collect(), + ) + } + + /// Try to start a game, if the user can and everybody is ready + pub fn start(&mut self, user: UserId) -> Result<(), LobbyErr> { + if let Some(_) = self + .inner + .players + .iter() + .filter(|u| u.admin) + .find(|u| u.id == user) + { + return Err(LobbyErr::NotAuthorized); + }; + + match self + .inner + .players + .iter() + .filter(|u| !u.ready) + .collect::>() + .len() + { + 0 => Err(LobbyErr::NotAllReady), + _ => Ok(()), + } + } +} diff --git a/games/rstnode/rst-core/src/map.rs b/games/rstnode/rst-core/src/map.rs new file mode 100644 index 000000000000..37f758b4a433 --- /dev/null +++ b/games/rstnode/rst-core/src/map.rs @@ -0,0 +1,80 @@ +//! Implements a map graph and world logic + +use crate::{ + config::{LinkCfg, MapCfg, NodeCfg}, + data::{Link, Node, NodeId}, + server::{ServerErr, ServerResult}, + wire::Response, +}; +use async_std::sync::Arc; +use quadtree_rs::{ + area::{Area, AreaBuilder}, + point::Point, + Quadtree, +}; +use std::collections::BTreeMap; + +pub struct MapNode { + pub pos: (f64, f64), + pub inner: Node, +} + +/// A map that people fight on +/// +/// A map is defined by it's graph relationships, but also where on +/// the map nodes are placed, how much spacing there is, etc. All +/// this information is encoded in the same structs because it's +/// static, and just more convenient. +pub struct Map { + /// Node IDs mapped to coordinates + nodes: BTreeMap, + /// Link IDs mapped to link objects + links: BTreeMap>, + /// A coordinate map for the network + coord: Quadtree>, +} + +impl Map { + pub fn new() -> Self { + Self { + nodes: BTreeMap::new(), + links: BTreeMap::new(), + coord: Quadtree::new(2), + } + } + + pub fn update(&mut self, cb: F) -> ServerResult + where + F: Fn(&mut Map) -> ServerResult, + { + unimplemented!() + } + + /// Get all objects that can be selected by a single point + pub fn get_by_point(&self, x: i64, y: i64) -> Option> { + self.coord + .query( + AreaBuilder::default() + .anchor(Point::from((x, y))) + .dimensions((1, 1)) + .build() + .ok()?, + ) + .map(|entry| Some(entry.value_ref().inner.id)) + .collect() + } + + /// Get all objects that can be selected by a 2d area + pub fn get_by_area(&self, x: i64, y: i64, w: i64, h: i64) -> Option> { + self.coord + .query( + AreaBuilder::default() + .anchor(Point::from((x, y))) + .dimensions((w, h)) + .build() + .ok()?, + ) + .map(|entry| Some(entry.value_ref().inner.id)) + .collect() + } +} diff --git a/games/rstnode/rst-core/src/mapstore.rs b/games/rstnode/rst-core/src/mapstore.rs new file mode 100644 index 000000000000..85c5e36ef93b --- /dev/null +++ b/games/rstnode/rst-core/src/mapstore.rs @@ -0,0 +1,17 @@ +//! Map store + +use crate::config::MapCfg; +use std::{collections::BTreeMap, fs, path::Path}; + +pub struct MapStore { + configs: BTreeMap, +} + +impl MapStore { + /// Load a set of map configs + pub fn load_path(&mut self, path: &Path) { + fs::read_dir(&path).unwrap().for_each(|d| { + let name = d.unwrap().file_name().into_string().unwrap(); + }); + } +} diff --git a/games/rstnode/rst-core/src/runner.rs b/games/rstnode/rst-core/src/runner.rs new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/games/rstnode/rst-core/src/runner.rs @@ -0,0 +1 @@ + diff --git a/games/rstnode/rst-core/src/server.rs b/games/rstnode/rst-core/src/server.rs new file mode 100644 index 000000000000..3d95c3638c98 --- /dev/null +++ b/games/rstnode/rst-core/src/server.rs @@ -0,0 +1,155 @@ +//! Game server state handler +//! +//! A server can host many lobbies at the same time. It listens for +//! connections according to a address given to the initialiser. + +use crate::{ + _if::GameIf, + _match::Match, + data::Player, + lobby::LobbyList, + map::Map, + users::UserStore, + wire::{ + Action, AuthErr, Lobby, LobbyErr, LobbyId, LobbyUpdate, MatchErr, MatchId, RegErr, + Response, UpdateState, User, UserId, + }, +}; +use async_std::sync::{Arc, Mutex, RwLock}; +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use std::{collections::BTreeMap, path::Path}; + +/// A convenience result wrapper for server actions +pub type ServerResult = Result; +pub enum ServerErr { + /// The requested directory is corrupted + NoSuchDir, + /// Corrupted game state + Corrupted, + /// No such match found + NoSuchMatch, +} + +/// The game's server backend +pub struct Server { + matches: BTreeMap>, + users: UserStore, + lobbies: LobbyList, +} + +impl Server { + /// Create a new game server + fn new() -> Self { + Self { + matches: Default::default(), + users: UserStore::new(), + lobbies: LobbyList::new(), + } + } + + /// Open the state dir of a game server + pub async fn open(self: Arc, path: &Path) -> ServerResult<()> { + Ok(()) + } + + /// Stop accepting new game connections and shutdown gracefully + /// + /// Returns the number of matches still going on. + pub async fn shutdown(self: Arc) -> ServerResult { + Ok(0) + } + + /// Save and close the statedir and kicking all players + /// + /// Returns the number of players that were kicked off the server + /// prematurely. + pub async fn kill(self: Arc) -> ServerResult { + Ok(0) + } + + pub async fn update_map(self: Arc, id: MatchId, cb: F) -> ServerResult + where + F: Fn(&mut Map) -> ServerResult, + { + match self.matches.get(&id) { + Some(ref m) => m.lock().await.map.update(cb), + None => Err(ServerErr::NoSuchMatch), + } + } + + pub async fn update_players(self: Arc, id: MatchId, cb: F) -> ServerResult + where + F: Fn(&mut Vec) -> ServerResult, + { + match self.matches.get(&id) { + Some(ref mut m) => cb(&mut m.lock().await.players), + None => Err(ServerErr::NoSuchMatch), + } + } +} + +#[async_trait] +impl GameIf for Server { + async fn register(self: Arc, name: String, pw: String) -> Result { + unimplemented!() + } + + async fn login(self: Arc, name: String, pw: String) -> Result { + unimplemented!() + } + + async fn logout(self: Arc, user: User) -> Result<(), AuthErr> { + unimplemented!() + } + + async fn anonymous(self: Arc, name: String) -> Result { + let (_, auth) = self.users.add(name, None, true).await; + Ok(auth) + } + + async fn join(self: Arc, user: User, lobby: LobbyId) -> Result { + let mu = self.users.get(&user).await?; + self.lobbies.get_mut(lobby, |mut l| l.join(&mu)).await + } + + async fn leave(self: Arc, user: User, lobby: LobbyId) -> Result<(), LobbyErr> { + let mu = self.users.get(&user).await?; + self.lobbies.get_mut(lobby, |mut l| l.leave(&mu)).await + } + + async fn ready( + self: Arc, + user: User, + lobby: LobbyId, + ready: bool, + ) -> Result { + self.lobbies + .get_mut(lobby, |mut l| l.ready(user, ready)) + .await + } + + /// A start request was received + async fn start_req( + self: Arc, + user: UserId, + lobby: LobbyId, + ) -> Result, LobbyErr> { + self.lobbies.get_mut(lobby, |mut l| l.start(user)).await?; + let lob = self.lobbies.consume(lobby).await?; + Ok(Utc::now()) + } + + async fn perform_action( + self: Arc, + user: User, + mtch: MatchId, + act: Action, + ) -> UpdateState { + unimplemented!() + } + + async fn leave_match(self: Arc, user: User, mtch: MatchId) -> Result<(), MatchErr> { + unimplemented!() + } +} diff --git a/games/rstnode/rst-core/src/stats.rs b/games/rstnode/rst-core/src/stats.rs new file mode 100644 index 000000000000..1c651a98a0a8 --- /dev/null +++ b/games/rstnode/rst-core/src/stats.rs @@ -0,0 +1,183 @@ +//! This file contains balancing data +//! +//! Each type of node and packet is being balanced for different +//! factors. The stats are just returned by these functions. +//! Whenever there is some effect that can happen in the game, it's +//! value (strength) will be derived from here. + +pub type Money = usize; + +/// The cost of doing business +pub mod costs { + use super::Money; + use crate::data::{Level, PacketType, Upgrade}; + + /// Takes the current node and desired upgrade level + pub fn upgrade(curr: &Upgrade, want: &Upgrade) -> Money { + use self::{Level::*, Upgrade::*}; + match (curr, want) { + // Base upgrades + (Base, Guard(One)) => 30, + (Base, Compute(One)) => 25, + (Base, Relay(One)) => 20, + + // Guards + (Guard(One), Guard(Two)) => 50, + (Guard(Two), Guard(Three)) => 75, + + // Compute is expensive + (Compute(One), Compute(Two)) => 50, + (Compute(Two), Compute(Three)) => 95, + + // Relays + (Relay(One), Relay(Two)) => 35, + (Relay(Two), Relay(Three)) => 55, + + // Can't touch this + (_, _) => unreachable!(), + } + } + + /// Sending certain packets costs money, let's find out how much + pub fn packets(node: &Upgrade, packet: &PacketType) -> Money { + use { + Level::*, + PacketType::*, + Upgrade::{Base, Compute as Cc, Guard, Relay}, + }; + match (node, packet) { + // Sending pings is free forever + (_, Ping) => 0, + // Capture packets always cost the same + (_, Capture) => 15, + + // The cost of compute packets increases with levels + // because their efficiency increases more dramatically + (Cc(One), Compute { .. }) => 7, + (Cc(Two), Compute { .. }) => 14, + (Cc(Three), Compute { .. }) => 21, + + // Payloads can only be sent from guards + (Guard(_), Payload { .. }) => 12, + + // Resets are relatively cheap + (Guard(One), Reset) => 16, + (Guard(Two), Reset) => 32, + (Guard(Three), Reset) => 48, + + (Guard(One), CNS) => 22, + (Guard(Two), CNS) => 40, + (Guard(Three), CNS) => 82, + + (Guard(Two), Nitm) => 64, + (Guard(Three), Nitm) => 148, + + (Guard(Two), Virus) => 40, + (Guard(Three), Virus) => 60, + + // Only level 3 guards can send takeovers + (Guard(Three), TakeOver) => 256, + + // Can't touch this + (_, _) => unreachable!(), + } + } +} + +/// This is what capitalists are all the rage about +pub mod gains { + use super::Money; + use crate::data::{Level, PacketType, Upgrade}; + use std::sync::atomic::Ordering; + + /// This will tell you if you'll receive any money this week + pub fn parse_packet(node: &Upgrade, packet: &PacketType) -> Money { + use { + Level::*, + PacketType::*, + Upgrade::{Base, Compute as Cc, Guard, Relay}, + }; + + /// A utility function which increments the packet progress + /// + /// Depending on the node that is processing the incoming + /// packet, progress is either stepped by 1, 1.5, or 2 times + /// the advertised step rate, which is set by the spawning + /// node of the compute job. This means that stronger compute + /// nodes have a positive impact even in mostly low-level + /// systems. + /// + /// This function returns if it reached or overflowed the max + fn incr_compute(l: &Level, packet: &PacketType) -> bool { + match (l, packet) { + (lvl, Compute { max, curr, step }) => { + if curr.load(Ordering::Relaxed) < *max { + curr.fetch_add( + match lvl { + One => *step, + Two => (*step as f32 * 1.5) as u16, + Three => *step * 2, + }, + Ordering::Relaxed, + ); + } + + // Did we reach the target? + curr.load(Ordering::Relaxed) >= *max + } + (_, _) => unreachable!(), + } + } + + match (node, packet) { + // A basic income for all node and packets + (Base, Ping) => 4, + (Guard(One), Ping) | (Cc(One), Ping) | (Relay(One), Ping) => 6, + (Guard(Two), Ping) | (Cc(Two), Ping) | (Relay(Two), Ping) => 11, + (Guard(Three), Ping) | (Cc(Three), Ping) | (Relay(Three), Ping) => 17, + + // A compute node will always increment the packet + // reference. If it made it to "max", it will then also + // return an amount of money that can be gained + (Cc(ref lvl), Compute { ref max, .. }) if incr_compute(lvl, packet) => match lvl { + One => *max as usize, + Two => (max * 2) as usize, + Three => (max * 3) as usize, + }, + + // If in doubt, nada! + (_, _) => 0, + } + } +} + +pub mod strengths { + use crate::data::Level::{self, *}; + + /// Determine the maximum amount of resources for a compute packet + pub fn compute_max(lvl: Level) -> u16 { + match lvl { + One => 65, + Two => 120, + Three => 250, + } + } + + /// Determine the step size by which a computation advances + pub fn compute_step(lvl: Level) -> u16 { + match lvl { + One => 7, + Two => 20, + Three => 32, + } + } + + /// Determine the reward gained from processing a payload packet + pub fn payload_reward(lvl: Level) -> u16 { + match lvl { + One => 70, + Two => 150, + Three => 275, + } + } +} diff --git a/games/rstnode/rst-core/src/users.rs b/games/rstnode/rst-core/src/users.rs new file mode 100644 index 000000000000..0c93b83ec1da --- /dev/null +++ b/games/rstnode/rst-core/src/users.rs @@ -0,0 +1,69 @@ +//! A users abstraction module + +use crate::{ + wire::{LobbyErr, User, UserId}, + Id, +}; +use async_std::sync::{Arc, RwLock}; +use std::{ + collections::BTreeMap, + sync::atomic::{AtomicUsize, Ordering}, +}; + +pub struct MetaUser { + pub id: UserId, + pub name: String, + pub pw: String, + pub auth: User, +} + +pub struct UserStore { + max: AtomicUsize, + users: RwLock>>, +} + +impl UserStore { + /// Currently resuming a userstore isn't possible + pub fn new() -> Self { + UserStore { + max: 0.into(), + users: Default::default(), + } + } + + /// Get the metadata user for a login user + pub async fn get(&self, user: &User) -> Result, LobbyErr> { + match self.users.read().await.get(&user.id) { + Some(ref u) => Ok(Arc::clone(u)), + None => Err(LobbyErr::OtherError), + } + } + + pub async fn add>>( + &self, + name: String, + pw: S, + registered: bool, + ) -> (UserId, User) { + let id = self.max.fetch_add(1, Ordering::Relaxed); + let token = Id::random(); + let pw = pw.into().unwrap_or("".into()); + let auth = User { + id, + token, + registered, + }; + + self.users.write().await.insert( + id, + MetaUser { + id, + name, + pw, + auth: auth.clone(), + } + .into(), + ); + (id, auth.clone()) + } +} diff --git a/games/rstnode/rst-core/src/wire/action.rs b/games/rstnode/rst-core/src/wire/action.rs new file mode 100644 index 000000000000..22ab7ce6868e --- /dev/null +++ b/games/rstnode/rst-core/src/wire/action.rs @@ -0,0 +1,33 @@ +use crate::data::{NodeId, Upgrade}; +use serde::{Deserialize, Serialize}; + +/// All actions that a user can trigger via the UI +#[derive(Serialize, Deserialize)] +pub enum Action { + /// Cancel the running action + Cancel(NodeId), + /// Start a capture action + Capture { from: NodeId, to: NodeId }, + /// Set the compute targets + Compute { from: NodeId, to: Vec }, + /// Set to payload analysis mode + Payload(NodeId), + /// Send an exploit across the network + Reset { + from: NodeId, + to: NodeId, + exp: Exploit, + }, + /// Try to upgrade the node to a level + Upgrade { node: NodeId, level: Upgrade }, +} + +/// A type of exploit a node can start running +#[derive(Serialize, Deserialize)] +pub enum Exploit { + Reset, + CNS, + Nitm, + Virus, + TakeOver, +} diff --git a/games/rstnode/rst-core/src/wire/mod.rs b/games/rstnode/rst-core/src/wire/mod.rs new file mode 100644 index 000000000000..cd311383a83a --- /dev/null +++ b/games/rstnode/rst-core/src/wire/mod.rs @@ -0,0 +1,68 @@ +//! Network formats and container messages + +mod action; +pub use action::*; + +mod req; +pub use req::*; + +mod resp; +pub use resp::*; + +mod update; +pub use update::*; + +use crate::{ + data::{Color, Player}, + map::Map, + Id, +}; +use serde::{Deserialize, Serialize}; + +/// An alias for a User's ID +pub type UserId = usize; + +/// Represents a user payload +#[derive(Copy, Clone, Serialize, Deserialize)] +pub struct User { + /// The internal user ID + pub id: UserId, + /// The auth token provided by the client + pub token: Id, + /// Whether the scores will be tracked + pub registered: bool, +} + +/// A more lobby specific abstraction for a user +#[derive(Clone, Serialize, Deserialize)] +pub struct LobbyUser { + /// The user ID + pub id: UserId, + /// Their nick name + pub name: String, + /// Are they ready? + pub ready: bool, + /// Are they the lobby admin? + pub admin: bool, + /// The colour they will be in the match + pub color: Color, +} + +/// An alias for a Room ID +pub type LobbyId = usize; + +/// Represent a lobby +#[derive(Clone, Serialize, Deserialize)] +pub struct Lobby { + /// The ID of the lobby + pub id: LobbyId, + /// A set of user IDs + pub players: Vec, + /// The name of the map + pub map: String, + /// Settings + pub settings: Vec, +} + +/// An alias for a match ID +pub type MatchId = usize; diff --git a/games/rstnode/rst-core/src/wire/req.rs b/games/rstnode/rst-core/src/wire/req.rs new file mode 100644 index 000000000000..ffefcbdb6ac7 --- /dev/null +++ b/games/rstnode/rst-core/src/wire/req.rs @@ -0,0 +1,38 @@ +use super::{action::Action, LobbyId, MatchId, User}; +use serde::{Deserialize, Serialize}; + +/// A message sent from the game client to the server +#[derive(Serialize, Deserialize)] +pub enum Request { + /// Register yourself with the game server + Register(String, String), + + /// Login to your user session + /// + /// This user can't log into the system from another computer + Login(String, String), + + /// Close your user session + Logout(User), + + /// Start an anonymous session + Anonymous(String), + + /// A user joins a game lobby + Join(User, LobbyId), + + /// A user leaves a game lobby + Leave(User, LobbyId), + + /// Mark a user as ready + Ready(User, LobbyId, bool), + + /// Try to start the match + StartReq(User, LobbyId), + + /// Send a move in the game + GameAction(User, MatchId, Action), + + /// Leave the match (forfeit) + LeaveGame(User, MatchId), +} diff --git a/games/rstnode/rst-core/src/wire/resp.rs b/games/rstnode/rst-core/src/wire/resp.rs new file mode 100644 index 000000000000..ef2e192a6044 --- /dev/null +++ b/games/rstnode/rst-core/src/wire/resp.rs @@ -0,0 +1,94 @@ +//! Response values that the server can reply with + +use super::{Lobby, LobbyId, User, UserId}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// A response values from the server +#[derive(Serialize, Deserialize)] +pub enum Response { + /// Response to the register request + Register(Result), + /// Response to login request + Login(Result), + /// Response to login request + Logout(Result<(), AuthErr>), + /// Get a list of available pairs + Rooms(Vec<(String, LobbyId)>), + /// A user joins a game lobby + Join(Result), + /// A user leaves a game lobby + Leave(Result<(), LobbyErr>), + /// Get the new set of ready states + Ready(LobbyUpdate), + /// Receiving a start request time + StartReq(DateTime), + /// Response to the action with the state update + GameUpdate(UpdateState), + /// Leave the match (forfeit) + LeaveGame(Result<(), MatchErr>), +} + +#[derive(Serialize, Deserialize)] +pub enum RegErr { + /// The password is way too bad + BadPassword, + /// THe username is already taken + UsernameTaken, + /// Other internal error, try again? + OtherError, +} + +#[derive(Serialize, Deserialize)] +pub enum AuthErr { + /// Wrong password for the user + WrongPassword, + /// The requested user doesn't exist + UserNotFound, + /// No session currently exists + NoSossion, + /// Other internal error, try again? + OtherError, +} + +#[derive(Serialize, Deserialize)] +pub enum LobbyErr { + /// The requested room is already full + RoomFull, + /// The room id is unknown + NoSuchRoom, + /// Previously not in room + NotInRoom, + /// Not everybody was ready + NotAllReady, + /// A request was sent by someone who isn't authorised + NotAuthorized, + /// Other internal error, try again? + OtherError, +} + +#[derive(Serialize, Deserialize)] +pub enum LobbyUpdate { + /// The set of ready users + Ready(Vec), +} + +/// The way the update was applied +#[derive(Serialize, Deserialize)] +pub enum UpdateState { + /// The update was applied seamlessly + Success, + /// The update was inserted, but had to be re-ordered with another update + Reordered, + /// The sent request was invalid and was not applied + Invalid, +} + +/// An error that can occur in a match +#[derive(Serialize, Deserialize)] +pub enum MatchErr { + /// The provided player wasn't in the match (anymore?) + NotInMatch, + /// The requested match had already ended + MatchAlreadyEnded, +} diff --git a/games/rstnode/rst-core/src/wire/update.rs b/games/rstnode/rst-core/src/wire/update.rs new file mode 100644 index 000000000000..a1b47ff07e50 --- /dev/null +++ b/games/rstnode/rst-core/src/wire/update.rs @@ -0,0 +1,72 @@ +//! Update to the game state + +use super::UserId; +use crate::data::{NodeId, PacketId, Player, Upgrade}; +use serde::{Deserialize, Serialize}; + +/// An update provided by the game server +#[derive(Serialize, Deserialize)] +pub enum Update { + /// Update made to a node + Node(NodeUpdate), + /// Update made to a link + Link(LinkUpdate), + /// Update made to a packet + Packet(PacketUpdate), + /// Update made to the user set + User(UserUpdate), + /// An error occured, can be non-fatal + Error(UpdateError), +} + +/// Update made to a node +#[derive(Serialize, Deserialize)] +pub enum NodeUpdate { + /// The node owner changed + Owner(Player), + /// Represent a new upgrade state + Level { node: NodeId, new: Upgrade }, + /// A new packet was consumed from a link + NewPacket(PacketId), + /// Remove a packet from the node buffer + SentPacket(PacketId), + /// Dropped a packet + DropPacket(PacketId), +} + +/// Update made to a link +#[derive(Serialize, Deserialize)] +pub enum LinkUpdate { + /// Take a packet from a node's buffer + TakePacket(PacketId), + /// Give a packet to a node's buffer + GivePacket(PacketId), +} + +/// Update made to a packet +#[derive(Serialize, Deserialize)] +pub enum PacketUpdate { + /// Advance a packet along one step along the link + Increment(PacketId), +} + +/// Update made to the user set +#[derive(Serialize, Deserialize)] +pub enum UserUpdate { + UserLeft(UserId), +} + +/// An error occured, can be non-fatal +#[derive(Serialize, Deserialize)] +pub enum UpdateError { + /// You are the last user in the match + LastUser, + /// The game crashed, so kick all + GameCrashed, + /// The server's time was behind the client time + /// + /// This means that newer events will be dropped from the map + /// state. This should prompt the client to warn the user this + /// has happened, then resync the time of the game states. + TimeAheadServer, +} -- cgit v1.2.3