aboutsummaryrefslogtreecommitdiff
path: root/games/rstnode/rst-core
diff options
context:
space:
mode:
authorKatharina Fey <kookie@spacekookie.de>2021-02-06 19:40:53 +0100
committerKatharina Fey <kookie@spacekookie.de>2021-02-06 19:42:04 +0100
commitcf9392a33bb99ae581f818d3ddb8be1231521a02 (patch)
tree8295d8a4ed199c3263eadd8f1a508b98567a44f7 /games/rstnode/rst-core
parent56d96b2f22bf6a61ff992b000215dc3a2c2448ad (diff)
rstnode: restructure project into workspace and sub-crates
Diffstat (limited to 'games/rstnode/rst-core')
-rw-r--r--games/rstnode/rst-core/Cargo.toml29
-rw-r--r--games/rstnode/rst-core/src/_if.rs58
-rw-r--r--games/rstnode/rst-core/src/_loop.rs41
-rw-r--r--games/rstnode/rst-core/src/_match.rs72
-rw-r--r--games/rstnode/rst-core/src/bin/game.rs3
-rw-r--r--games/rstnode/rst-core/src/bin/server.rs3
-rw-r--r--games/rstnode/rst-core/src/config/io.rs45
-rw-r--r--games/rstnode/rst-core/src/config/mod.rs50
-rw-r--r--games/rstnode/rst-core/src/data.rs279
-rw-r--r--games/rstnode/rst-core/src/gens.rs92
-rw-r--r--games/rstnode/rst-core/src/io.rs22
-rw-r--r--games/rstnode/rst-core/src/lib.rs39
-rw-r--r--games/rstnode/rst-core/src/lobby.rs191
-rw-r--r--games/rstnode/rst-core/src/map.rs80
-rw-r--r--games/rstnode/rst-core/src/mapstore.rs17
-rw-r--r--games/rstnode/rst-core/src/runner.rs1
-rw-r--r--games/rstnode/rst-core/src/server.rs155
-rw-r--r--games/rstnode/rst-core/src/stats.rs183
-rw-r--r--games/rstnode/rst-core/src/users.rs69
-rw-r--r--games/rstnode/rst-core/src/wire/action.rs33
-rw-r--r--games/rstnode/rst-core/src/wire/mod.rs68
-rw-r--r--games/rstnode/rst-core/src/wire/req.rs38
-rw-r--r--games/rstnode/rst-core/src/wire/resp.rs94
-rw-r--r--games/rstnode/rst-core/src/wire/update.rs72
24 files changed, 1734 insertions, 0 deletions
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 <kookie@spacekookie.de>"]
+
+[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<Self>, name: String, pw: String) -> Result<UserId, RegErr>;
+
+ /// Login for an existing user
+ async fn login(self: Arc<Self>, name: String, pw: String) -> Result<User, AuthErr>;
+
+ /// End a user session (go offline)
+ async fn logout(self: Arc<Self>, user: User) -> Result<(), AuthErr>;
+
+ /// Register as an anonymous player
+ async fn anonymous(self: Arc<Self>, name: String) -> Result<User, AuthErr>;
+
+ /// Join a match-making lobby
+ async fn join(self: Arc<Self>, user: User, lobby: LobbyId) -> Result<Lobby, LobbyErr>;
+
+ /// Leave a match-making lobby
+ async fn leave(self: Arc<Self>, user: User, lobby: LobbyId) -> Result<(), LobbyErr>;
+
+ /// Set the player's ready state
+ async fn ready(
+ self: Arc<Self>,
+ user: User,
+ lobby: LobbyId,
+ ready: bool,
+ ) -> Result<LobbyUpdate, LobbyErr>;
+
+ /// Send a start request (as lobby admin)
+ async fn start_req(
+ self: Arc<Self>,
+ user: UserId,
+ lobby: LobbyId,
+ ) -> Result<DateTime<Utc>, LobbyErr>;
+
+ /// Perform a game action as a user
+ async fn perform_action(self: Arc<Self>, user: User, mtch: MatchId, act: Action)
+ -> UpdateState;
+
+ /// Leave a match
+ async fn leave_match(self: Arc<Self>, 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<F>(run: Arc<AtomicBool>, f: F)
+where
+ F: Future<Output = ()> + 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<F>(run: Arc<AtomicBool>, f: F)
+where
+ F: Future<Output = ()> + 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<Player>,
+ /// The active game map
+ pub map: Map,
+ /// Input inbox (handled in-order each game tick)
+ inbox: RwLock<VecDeque<Action>>,
+ /// The time the match was initialised
+ init_t: DateTime<Utc>,
+ /// The synced time the match was started
+ start_t: Option<DateTime<Utc>>,
+}
+
+impl From<MetaLobby> 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<Utc>) {
+ 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<io::Error> for Error {
+ fn from(e: io::Error) -> Self {
+ Self::Io(e.to_string())
+ }
+}
+
+impl From<serde_yaml::Error> 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<MapCfg, Error> {
+ 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<NodeCfg>,
+ /// Links connecting nodes
+ pub links: Vec<LinkCfg>,
+ /// Default spawn points (player count)
+ pub spawns: Vec<SpawnCfg>,
+}
+
+/// 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<Arc<Link>>,
+ /// Input buffer
+ #[serde(skip)]
+ pub buffer: Vec<Packet>,
+}
+
+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<Player>,
+ /// 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<u16>,
+}
+
+/// 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<Color> {
+ 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<PacketType>, 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>) -> 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<BTreeMap<LobbyId, MetaLobby>>,
+}
+
+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<MetaLobby, LobbyErr> {
+ 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<F, T>(&self, id: LobbyId, cb: F) -> Result<T, LobbyErr>
+ 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<Color>,
+ 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::<Vec<_>>()
+ .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<NodeId, (i64, i64)>,
+ /// Link IDs mapped to link objects
+ links: BTreeMap<u16, Arc<Link>>,
+ /// A coordinate map for the network
+ coord: Quadtree<i64, Arc<MapNode>>,
+}
+
+impl Map {
+ pub fn new() -> Self {
+ Self {
+ nodes: BTreeMap::new(),
+ links: BTreeMap::new(),
+ coord: Quadtree::new(2),
+ }
+ }
+
+ pub fn update<F>(&mut self, cb: F) -> ServerResult<Response>
+ where
+ F: Fn(&mut Map) -> ServerResult<Response>,
+ {
+ unimplemented!()
+ }
+
+ /// Get all objects that can be selected by a single point
+ pub fn get_by_point(&self, x: i64, y: i64) -> Option<Vec<NodeId>> {
+ 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<Vec<NodeId>> {
+ 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<String, MapCfg>,
+}
+
+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<T> = Result<T, ServerErr>;
+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<MatchId, Mutex<Match>>,
+ 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<Self>, 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<Self>) -> ServerResult<u64> {
+ 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<Self>) -> ServerResult<u64> {
+ Ok(0)
+ }
+
+ pub async fn update_map<F>(self: Arc<Self>, id: MatchId, cb: F) -> ServerResult<Response>
+ where
+ F: Fn(&mut Map) -> ServerResult<Response>,
+ {
+ match self.matches.get(&id) {
+ Some(ref m) => m.lock().await.map.update(cb),
+ None => Err(ServerErr::NoSuchMatch),
+ }
+ }
+
+ pub async fn update_players<F>(self: Arc<Self>, id: MatchId, cb: F) -> ServerResult<Response>
+ where
+ F: Fn(&mut Vec<Player>) -> ServerResult<Response>,
+ {
+ 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<Self>, name: String, pw: String) -> Result<UserId, RegErr> {
+ unimplemented!()
+ }
+
+ async fn login(self: Arc<Self>, name: String, pw: String) -> Result<User, AuthErr> {
+ unimplemented!()
+ }
+
+ async fn logout(self: Arc<Self>, user: User) -> Result<(), AuthErr> {
+ unimplemented!()
+ }
+
+ async fn anonymous(self: Arc<Self>, name: String) -> Result<User, AuthErr> {
+ let (_, auth) = self.users.add(name, None, true).await;
+ Ok(auth)
+ }
+
+ async fn join(self: Arc<Self>, user: User, lobby: LobbyId) -> Result<Lobby, LobbyErr> {
+ let mu = self.users.get(&user).await?;
+ self.lobbies.get_mut(lobby, |mut l| l.join(&mu)).await
+ }
+
+ async fn leave(self: Arc<Self>, 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<Self>,
+ user: User,
+ lobby: LobbyId,
+ ready: bool,
+ ) -> Result<LobbyUpdate, LobbyErr> {
+ self.lobbies
+ .get_mut(lobby, |mut l| l.ready(user, ready))
+ .await
+ }
+
+ /// A start request was received
+ async fn start_req(
+ self: Arc<Self>,
+ user: UserId,
+ lobby: LobbyId,
+ ) -> Result<DateTime<Utc>, 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<Self>,
+ user: User,
+ mtch: MatchId,
+ act: Action,
+ ) -> UpdateState {
+ unimplemented!()
+ }
+
+ async fn leave_match(self: Arc<Self>, 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<BTreeMap<UserId, Arc<MetaUser>>>,
+}
+
+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<Arc<MetaUser>, LobbyErr> {
+ match self.users.read().await.get(&user.id) {
+ Some(ref u) => Ok(Arc::clone(u)),
+ None => Err(LobbyErr::OtherError),
+ }
+ }
+
+ pub async fn add<S: Into<Option<String>>>(
+ &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<NodeId> },
+ /// 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<LobbyUser>,
+ /// The name of the map
+ pub map: String,
+ /// Settings
+ pub settings: Vec<String>,
+}
+
+/// 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<UserId, RegErr>),
+ /// Response to login request
+ Login(Result<User, AuthErr>),
+ /// Response to login request
+ Logout(Result<(), AuthErr>),
+ /// Get a list of available <name-room> pairs
+ Rooms(Vec<(String, LobbyId)>),
+ /// A user joins a game lobby
+ Join(Result<Lobby, LobbyErr>),
+ /// 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<Utc>),
+ /// 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<UserId>),
+}
+
+/// 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,
+}