diff --git a/core/src/guest.rs b/core/src/guest.rs new file mode 100644 index 0000000..a3b87bb --- /dev/null +++ b/core/src/guest.rs @@ -0,0 +1,231 @@ +//! A guest's side of a shared world. The guest gets the host's world once +//! and then runs every tick the host sends, with the same commands, so that +//! both worlds stay the same. Checksums from the host show whether they do. + +use std::collections::VecDeque; +use std::fmt; + +use crate::codec::DecodeError; +use crate::command::Command; +use crate::player::{clamp_name, PlayerId, PlayerKey}; +use crate::protocol::{Action, Refusal, ToGuest, ToHost}; +use crate::replay::Recording; +use crate::rules::rules_id; +use crate::snapshot::SnapshotError; +use crate::world::{Event, World}; + +/// Reason a guest cannot go on with the session. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum GuestError { + /// A message from the host could not be read. + Unreadable(DecodeError), + /// The world the host sent could not be restored. + BadWorld(SnapshotError), + /// The host sent a tick before it sent its world. + TickBeforeWorld, +} + +impl fmt::Display for GuestError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GuestError::Unreadable(error) => write!(f, "unreadable message from the host: {error}"), + GuestError::BadWorld(error) => write!(f, "the host's world is unusable: {error}"), + GuestError::TickBeforeWorld => write!(f, "the host sent a tick before its world"), + } + } +} + +impl std::error::Error for GuestError {} + +/// What a message from the host changed, apart from queued ticks. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Arrival { + /// Nothing the shell has to react to right away. + Nothing, + /// The host's world arrived. `replaced` is true when it replaced a world + /// that had diverged. + World { replaced: bool }, + /// The host turned the guest away. + Refused(Refusal), +} + +/// A tick from the host that has not run yet. +struct PendingTick { + tick: u64, + commands: Vec, + check: Option, +} + +/// The world the guest follows, with the recording of everything it ran. +struct Following { + world: World, + recording: Recording, +} + +/// The guest side of a shared world. +pub struct Guest { + key: PlayerKey, + following: Option, + /// True after a divergence, until the host's world arrives again. + diverged: bool, + pending: VecDeque, + outbox: Vec>, + diverged_recording: Option>, +} + +impl Guest { + /// A guest that greets the host as the player `key` named `name`. The + /// greeting waits in the outbox. + pub fn new(key: PlayerKey, name: &str) -> Self { + let hello = ToHost::Hello { + rules: rules_id(), + key, + name: clamp_name(name), + }; + Self { + key, + following: None, + diverged: false, + pending: VecDeque::new(), + outbox: vec![hello.encode()], + diverged_recording: None, + } + } + + /// The world, once the host has sent it. + pub fn world(&self) -> Option<&World> { + self.following.as_ref().map(|following| &following.world) + } + + /// The recording of every tick this guest ran since the host's world + /// last arrived. + pub fn recording(&self) -> Option<&Recording> { + self.following + .as_ref() + .map(|following| &following.recording) + } + + /// The guest's player, once its join has run. + pub fn local_player(&self) -> Option { + let world = self.world()?; + let id = world.find_player(&self.key)?; + world.player(id)?.online.then_some(id) + } + + /// Handles a message from the host. + pub fn receive(&mut self, bytes: &[u8]) -> Result { + match ToGuest::decode(bytes).map_err(GuestError::Unreadable)? { + ToGuest::World { snapshot } => { + let world = World::from_snapshot(&snapshot).map_err(GuestError::BadWorld)?; + let replaced = self.diverged; + if let Some(old) = self.following.take() { + if replaced { + self.diverged_recording = Some(old.recording.to_bytes(&old.world)); + } + } + // Ticks sent before this world are part of it already. + let tick = world.tick(); + self.pending.retain(|pending| pending.tick >= tick); + self.following = Some(Following { + recording: Recording::start(&world), + world, + }); + self.diverged = false; + Ok(Arrival::World { replaced }) + } + ToGuest::Refused(refusal) => Ok(Arrival::Refused(refusal)), + ToGuest::Tick { + tick, + commands, + check, + } => { + if self.following.is_none() { + return Err(GuestError::TickBeforeWorld); + } + self.pending.push_back(PendingTick { + tick, + commands, + check, + }); + Ok(Arrival::Nothing) + } + } + } + + /// Ticks that arrived and have not run yet. + pub fn pending_ticks(&self) -> usize { + self.pending.len() + } + + /// Runs the oldest tick that arrived and returns what happened, or + /// `None` if there is none to run. After a divergence no tick runs until + /// the host's world arrives again. + pub fn step(&mut self) -> Option> { + if self.diverged { + return None; + } + let following = self.following.as_mut()?; + while let Some(next) = self.pending.pop_front() { + let expected = following.world.tick(); + if next.tick < expected { + continue; + } + if next.tick > expected { + // A tick is missing: this world can no longer follow. + self.diverge(); + return None; + } + let events = following + .recording + .step(&mut following.world, &next.commands); + if next + .check + .is_some_and(|check| check != following.world.state_hash()) + { + self.diverge(); + } + return Some(events); + } + None + } + + /// Reports the current state to the host and stops running ticks until + /// the host's world arrives again. + fn diverge(&mut self) { + let Some(world) = self.world() else { + return; + }; + let report = ToHost::Diverged { + tick: world.tick(), + hash: world.state_hash(), + }; + self.outbox.push(report.encode()); + self.diverged = true; + } + + /// True after a divergence, until the host's world arrives again. + pub fn has_diverged(&self) -> bool { + self.diverged + } + + /// Asks the host to carry out an action of the guest's player. Does + /// nothing before the player is in the world or while the world is + /// diverged. + pub fn act(&mut self, action: Action) { + if self.diverged || self.local_player().is_none() { + return; + } + self.outbox.push(ToHost::Act(action).encode()); + } + + /// Messages for the host, in the order they were made. + pub fn take_outbox(&mut self) -> Vec> { + std::mem::take(&mut self.outbox) + } + + /// The encoded recording of the world that diverged last, kept so that + /// the divergence can be replayed. Empty after it has been taken. + pub fn take_diverged_recording(&mut self) -> Option> { + self.diverged_recording.take() + } +} diff --git a/core/src/host.rs b/core/src/host.rs new file mode 100644 index 0000000..6fbb1aa --- /dev/null +++ b/core/src/host.rs @@ -0,0 +1,323 @@ +//! The host's side of a shared world: the world, its recording and the +//! guests connected to it. +//! +//! The host runs the ticks. Its own player's actions and the actions its +//! guests send become commands of the next tick, and every tick goes out to +//! every accepted guest. A solo game is a host without guests. + +use std::collections::BTreeMap; + +use crate::command::Command; +use crate::player::{clamp_name, PlayerId, PlayerKey, MAX_KNOWN_PLAYERS, MAX_ONLINE_PLAYERS}; +use crate::protocol::{Action, Refusal, ToGuest, ToHost, CHECK_INTERVAL_TICKS}; +use crate::replay::Recording; +use crate::rules::rules_id; +use crate::world::{Event, World}; + +/// Identifies a connection. The shell chooses the numbers. +pub type Peer = u32; + +/// Most actions one player may take per tick. Further actions in the same +/// tick are dropped, so that no guest can flood the world with commands. +pub const MAX_ACTIONS_PER_TICK: u32 = 4; + +/// A guest reported that its state differs from the host's checksum. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Divergence { + pub peer: Peer, + /// The guest's tick when it noticed. + pub tick: u64, + /// The hash of the guest's state at that tick. + pub hash: u64, +} + +/// Where the connection of a guest stands. +#[derive(Clone, Debug, PartialEq, Eq)] +enum Link { + /// Connected, but the guest has not greeted yet. + Greeting, + /// Accepted. The guest has the world, and its join runs in the next tick. + Joining { key: PlayerKey }, + /// In the world as `player`. `actions` counts its actions in this tick. + Playing { player: PlayerId, actions: u32 }, +} + +/// The host of a shared world. +pub struct Host { + world: World, + recording: Recording, + rules: u64, + local_key: Option, + local_actions: u32, + /// Commands for the next tick, in the order they arrived. + inbox: Vec, + links: BTreeMap, + outbox: Vec<(Peer, Vec)>, + closing: Vec, + divergences: Vec, +} + +impl Host { + /// Hosts `world`, new or loaded. Nobody is in it yet. + pub fn new(world: World) -> Self { + Self { + recording: Recording::start(&world), + world, + rules: rules_id(), + local_key: None, + local_actions: 0, + inbox: Vec::new(), + links: BTreeMap::new(), + outbox: Vec::new(), + closing: Vec::new(), + divergences: Vec::new(), + } + } + + pub fn world(&self) -> &World { + &self.world + } + + /// The recording of everything that happened since the world was hosted. + pub fn recording(&self) -> &Recording { + &self.recording + } + + /// Queues the entry of the host's own player for the next tick. + pub fn join_local(&mut self, key: PlayerKey, name: &str) { + self.local_key = Some(key); + self.inbox.push(Command::Join { + key, + name: clamp_name(name), + }); + } + + /// The host's own player, once it is in the world. + pub fn local_player(&self) -> Option { + online_player(&self.world, &self.local_key?) + } + + /// Queues an action of the host's own player for the next tick. Does + /// nothing before the player is in the world. + pub fn act_local(&mut self, action: Action) { + let Some(player) = self.local_player() else { + return; + }; + if self.local_actions < MAX_ACTIONS_PER_TICK { + self.local_actions += 1; + self.inbox.push(action.command(player)); + } + } + + /// A guest connected as `peer`. Its first message must be a greeting. + pub fn connect(&mut self, peer: Peer) { + self.links.entry(peer).or_insert(Link::Greeting); + } + + /// The connection `peer` closed. A guest in the world leaves it in the + /// next tick; an accepted guest whose join has not run yet never enters. + pub fn disconnect(&mut self, peer: Peer) { + match self.links.remove(&peer) { + Some(Link::Joining { key }) => self.inbox.retain( + |command| !matches!(command, Command::Join { key: joining, .. } if *joining == key), + ), + Some(Link::Playing { player, .. }) => self.inbox.push(Command::Leave { player }), + Some(Link::Greeting) | None => {} + } + } + + /// Handles a message that arrived from `peer`. A guest that breaks the + /// protocol is closed; see [`Host::take_closing`]. + pub fn receive(&mut self, peer: Peer, bytes: &[u8]) { + let Some(link) = self.links.get(&peer).cloned() else { + return; + }; + match (link, ToHost::decode(bytes)) { + (Link::Greeting, Ok(ToHost::Hello { rules, key, name })) => { + self.greet(peer, rules, key, &name); + } + (Link::Greeting, Ok(ToHost::ForeignHello { .. })) => { + self.refuse(peer, Refusal::OtherProtocol); + } + (Link::Playing { player, actions }, Ok(ToHost::Act(action))) => { + if actions < MAX_ACTIONS_PER_TICK { + self.links.insert( + peer, + Link::Playing { + player, + actions: actions + 1, + }, + ); + self.inbox.push(action.command(player)); + } + } + // The guest acted before its join ran. It cannot know its player + // yet, so the action is dropped. + (Link::Joining { .. }, Ok(ToHost::Act(_))) => {} + (Link::Joining { .. } | Link::Playing { .. }, Ok(ToHost::Diverged { tick, hash })) => { + self.divergences.push(Divergence { peer, tick, hash }); + self.send( + peer, + &ToGuest::World { + snapshot: self.world.to_snapshot(), + }, + ); + } + _ => { + self.disconnect(peer); + self.closing.push(peer); + } + } + } + + fn greet(&mut self, peer: Peer, rules: u64, key: PlayerKey, name: &str) { + if let Err(refusal) = self.admission(rules, &key) { + self.refuse(peer, refusal); + return; + } + self.send( + peer, + &ToGuest::World { + snapshot: self.world.to_snapshot(), + }, + ); + self.inbox.push(Command::Join { + key, + name: clamp_name(name), + }); + self.links.insert(peer, Link::Joining { key }); + } + + /// Whether a guest with `rules` and `key` may join now. Joins that are + /// queued for the next tick count as if they had run. + fn admission(&self, rules: u64, key: &PlayerKey) -> Result<(), Refusal> { + if rules != self.rules { + return Err(Refusal::OtherRules); + } + let queued: Vec<&PlayerKey> = self + .inbox + .iter() + .filter_map(|command| match command { + Command::Join { key, .. } => Some(key), + _ => None, + }) + .collect(); + if online_player(&self.world, key).is_some() || queued.contains(&key) { + return Err(Refusal::AlreadyPlaying); + } + let online = self + .world + .players() + .iter() + .filter(|player| player.online) + .count(); + let entering = queued + .iter() + .filter(|queued| online_player(&self.world, queued).is_none()) + .count(); + if online + entering >= MAX_ONLINE_PLAYERS { + return Err(Refusal::WorldFull); + } + let new = queued + .iter() + .filter(|queued| self.world.find_player(queued).is_none()) + .count(); + if self.world.find_player(key).is_none() + && self.world.players().len() + new >= MAX_KNOWN_PLAYERS + { + return Err(Refusal::TooManyPlayers); + } + Ok(()) + } + + fn refuse(&mut self, peer: Peer, refusal: Refusal) { + self.send(peer, &ToGuest::Refused(refusal)); + self.links.remove(&peer); + self.closing.push(peer); + } + + fn send(&mut self, peer: Peer, message: &ToGuest) { + self.outbox.push((peer, message.encode())); + } + + /// Runs the next tick with every queued command, sends it to every + /// accepted guest and returns what happened. + pub fn step(&mut self) -> Vec { + let commands = std::mem::take(&mut self.inbox); + let tick = self.world.tick(); + let events = self.recording.step(&mut self.world, &commands); + let check = self + .world + .tick() + .is_multiple_of(CHECK_INTERVAL_TICKS) + .then(|| self.world.state_hash()); + let message = ToGuest::Tick { + tick, + commands, + check, + } + .encode(); + for (peer, link) in &self.links { + if *link != Link::Greeting { + self.outbox.push((*peer, message.clone())); + } + } + + self.local_actions = 0; + let mut refused = Vec::new(); + for (peer, link) in self.links.iter_mut() { + match link { + Link::Joining { key } => match online_player(&self.world, key) { + Some(player) => *link = Link::Playing { player, actions: 0 }, + None => refused.push(*peer), + }, + Link::Playing { actions, .. } => *actions = 0, + Link::Greeting => {} + } + } + // The admission check keeps the world from refusing an accepted + // guest. Should it happen anyway, the guest learns why. + for peer in refused { + self.refuse(peer, Refusal::WorldFull); + } + events + } + + /// Number of accepted guests, including those whose join has not run. + pub fn guest_count(&self) -> usize { + self.links + .values() + .filter(|link| **link != Link::Greeting) + .count() + } + + /// The player of the guest connected as `peer`, once it is in the world. + pub fn player_of(&self, peer: Peer) -> Option { + match self.links.get(&peer) { + Some(Link::Playing { player, .. }) => Some(*player), + _ => None, + } + } + + /// Messages to send, as (peer, bytes), in the order they were made. + pub fn take_outbox(&mut self) -> Vec<(Peer, Vec)> { + std::mem::take(&mut self.outbox) + } + + /// Connections the host has given up. The shell closes them after it + /// has sent the messages for them. + pub fn take_closing(&mut self) -> Vec { + std::mem::take(&mut self.closing) + } + + /// Divergences the guests reported since the last call. + pub fn take_divergences(&mut self) -> Vec { + std::mem::take(&mut self.divergences) + } +} + +/// The player with `key`, if it is online in `world`. +fn online_player(world: &World, key: &PlayerKey) -> Option { + let id = world.find_player(key)?; + world.player(id)?.online.then_some(id) +} diff --git a/core/src/lib.rs b/core/src/lib.rs index f1fd938..c1e3abb 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -8,7 +8,9 @@ pub mod codec; pub mod command; +pub mod guest; pub mod hash; +pub mod host; pub mod inventory; pub mod map; pub mod mapgen; diff --git a/core/tests/session.rs b/core/tests/session.rs new file mode 100644 index 0000000..1486a3b --- /dev/null +++ b/core/tests/session.rs @@ -0,0 +1,516 @@ +//! A host and its guests, connected in memory instead of over a network. +//! They exchange the same bytes the shell sends. + +use std::collections::BTreeMap; + +use grovewright_core::guest::{Arrival, Guest}; +use grovewright_core::host::{Host, Peer, MAX_ACTIONS_PER_TICK}; +use grovewright_core::inventory::ITEM_STONE; +use grovewright_core::map::{Pos, Terrain}; +use grovewright_core::mapgen::{outcrop_origin, TEST_MAP_SIZE}; +use grovewright_core::player::{PlayerKey, MAX_KNOWN_PLAYERS}; +use grovewright_core::protocol::{ + Action, Refusal, ToGuest, ToHost, CHECK_INTERVAL_TICKS, PROTOCOL_VERSION, +}; +use grovewright_core::replay::Recording; +use grovewright_core::rng::Rng; +use grovewright_core::rules::rules_id; +use grovewright_core::world::{World, DIG_COOLDOWN_TICKS}; + +const HOST_KEY: PlayerKey = [100; 16]; + +/// A host and its guests in one process. +struct Lan { + host: Host, + guests: BTreeMap, + /// Guests whose connection the host closed, with the refusal they got. + closed: BTreeMap>, + next_peer: Peer, +} + +impl Lan { + /// A host whose own player is in a new world from `seed`. + fn new(seed: u64) -> Self { + let mut host = Host::new(World::new(seed)); + host.join_local(HOST_KEY, "Host"); + host.step(); + Self { + host, + guests: BTreeMap::new(), + closed: BTreeMap::new(), + next_peer: 2, + } + } + + /// Connects a guest with a key made of `key` and lets it greet. + fn connect(&mut self, key: u8, name: &str) -> Peer { + let peer = self.next_peer; + self.next_peer += 1; + self.host.connect(peer); + self.guests.insert(peer, Guest::new([key; 16], name)); + self.deliver(); + peer + } + + /// Closes the connection of `peer`, as a guest that quits does. + fn disconnect(&mut self, peer: Peer) { + self.guests.remove(&peer); + self.host.disconnect(peer); + } + + /// Carries messages both ways until none are left. + fn deliver(&mut self) { + self.deliver_changed(|_, bytes| bytes); + } + + /// Like [`Lan::deliver`], but `change` may alter what the host sends. + fn deliver_changed(&mut self, mut change: impl FnMut(Peer, Vec) -> Vec) { + loop { + let mut moved = false; + for (peer, guest) in self.guests.iter_mut() { + for bytes in guest.take_outbox() { + self.host.receive(*peer, &bytes); + moved = true; + } + } + for (peer, bytes) in self.host.take_outbox() { + let bytes = change(peer, bytes); + if let Some(guest) = self.guests.get_mut(&peer) { + let arrival = guest + .receive(&bytes) + .expect("the host's message is readable"); + if let Arrival::Refused(refusal) = arrival { + self.closed.insert(peer, Some(refusal)); + } + } + moved = true; + } + for peer in self.host.take_closing() { + self.guests.remove(&peer); + self.closed.entry(peer).or_insert(None); + // The connection closes on both ends, and the host hears it. + self.host.disconnect(peer); + } + if !moved { + break; + } + } + } + + /// Runs one tick on the host and lets every guest run what arrived. + fn tick(&mut self) { + self.host.step(); + self.deliver(); + for guest in self.guests.values_mut() { + while guest.step().is_some() {} + } + self.deliver(); + } + + fn ticks(&mut self, count: u64) { + for _ in 0..count { + self.tick(); + } + } + + fn guest(&mut self, peer: Peer) -> &mut Guest { + self.guests.get_mut(&peer).expect("the guest is connected") + } + + /// Asserts that every guest is in exactly the state of the host. + fn assert_in_lockstep(&self) { + let expected = self.host.world().state_hash(); + for (peer, guest) in &self.guests { + assert_eq!( + guest.world().map(World::state_hash), + Some(expected), + "guest {peer}" + ); + } + } + + /// The rock right beside the spawn clearing. + fn rock(&self) -> Pos { + outcrop_origin(self.host.world().map()) + } +} + +/// The commands of the next tick as one guest receives them. Takes the +/// host's outbox, so the guests miss this tick. +fn commands_sent_to(host: &mut Host, peer: Peer) -> usize { + host.take_outbox() + .into_iter() + .filter(|(to, _)| *to == peer) + .find_map(|(_, bytes)| match ToGuest::decode(&bytes) { + Ok(ToGuest::Tick { commands, .. }) => Some(commands.len()), + _ => None, + }) + .expect("a tick was sent") +} + +#[test] +fn a_guest_gets_the_world_and_follows_every_tick() { + let mut lan = Lan::new(5); + let ada = lan.connect(1, "Ada"); + lan.ticks(3 * CHECK_INTERVAL_TICKS); + + lan.assert_in_lockstep(); + let guest = lan.guest(ada); + assert!(!guest.has_diverged()); + let me = guest.local_player().expect("the guest is in the world"); + let world = guest.world().unwrap(); + assert_eq!(world.player(me).unwrap().name, "Ada"); + assert_eq!( + world + .players() + .iter() + .filter(|player| player.online) + .count(), + 2 + ); +} + +#[test] +fn a_guest_joins_a_running_world_at_its_current_tick() { + let mut lan = Lan::new(5); + lan.ticks(100); + let ada = lan.connect(1, "Ada"); + let tick = lan.host.world().tick(); + assert_eq!(lan.guest(ada).world().unwrap().tick(), tick); + assert_eq!( + lan.guest(ada).local_player(), + None, + "its join has not run yet" + ); + + lan.tick(); + assert!(lan.guest(ada).local_player().is_some()); + assert_eq!(lan.host.player_of(ada), lan.guest(ada).local_player()); + lan.assert_in_lockstep(); +} + +#[test] +fn the_actions_of_a_guest_change_the_world_of_everyone() { + let mut lan = Lan::new(5); + let ada = lan.connect(1, "Ada"); + let grace = lan.connect(2, "Grace"); + lan.tick(); + let rock = lan.rock(); + for _ in 0..3 { + lan.guest(ada).act(Action::Dig { pos: rock }); + lan.ticks(DIG_COOLDOWN_TICKS + 1); + } + + lan.assert_in_lockstep(); + let ada_player = lan.host.player_of(ada).unwrap(); + let grace_player = lan.host.player_of(grace).unwrap(); + let seen_by_grace = lan.guest(grace).world().unwrap().clone(); + assert_eq!( + seen_by_grace.map().get(rock).unwrap().terrain, + Terrain::Ground + ); + assert_eq!( + seen_by_grace + .player(ada_player) + .unwrap() + .inventory + .count(ITEM_STONE), + 1 + ); + assert_eq!( + seen_by_grace + .player(grace_player) + .unwrap() + .inventory + .count(ITEM_STONE), + 0 + ); +} + +#[test] +fn the_host_takes_at_most_four_actions_per_player_and_tick() { + let mut lan = Lan::new(5); + let ada = lan.connect(1, "Ada"); + lan.tick(); + for x in 0..10 { + lan.guest(ada).act(Action::Dig { + pos: Pos::new(x, 1), + }); + lan.host.act_local(Action::Dig { + pos: Pos::new(x, 2), + }); + } + lan.deliver(); + lan.host.step(); + assert_eq!( + commands_sent_to(&mut lan.host, ada), + 2 * MAX_ACTIONS_PER_TICK as usize + ); + + // The count starts again in the next tick. + lan.host.act_local(Action::Dig { + pos: Pos::new(1, 3), + }); + lan.host.step(); + assert_eq!(commands_sent_to(&mut lan.host, ada), 1); +} + +#[test] +fn a_guest_of_another_protocol_version_is_told_so() { + let mut lan = Lan::new(5); + let peer = 9; + lan.host.connect(peer); + let foreign = ToHost::ForeignHello { + protocol: PROTOCOL_VERSION + 1, + }; + lan.host.receive(peer, &foreign.encode()); + assert_eq!( + lan.host.take_outbox(), + vec![(peer, ToGuest::Refused(Refusal::OtherProtocol).encode())] + ); + assert_eq!(lan.host.take_closing(), vec![peer]); +} + +#[test] +fn a_guest_with_other_rules_is_refused() { + let mut lan = Lan::new(5); + let peer = 9; + lan.host.connect(peer); + let hello = ToHost::Hello { + rules: rules_id() ^ 1, + key: [1; 16], + name: "Ada".to_owned(), + }; + lan.host.receive(peer, &hello.encode()); + assert_eq!( + lan.host.take_outbox(), + vec![(peer, ToGuest::Refused(Refusal::OtherRules).encode())] + ); + assert_eq!(lan.host.take_closing(), vec![peer]); +} + +#[test] +fn a_fifth_player_is_refused_even_before_the_others_have_joined() { + let mut lan = Lan::new(5); + let guests = [1, 2, 3].map(|key| lan.connect(key, "Guest")); + let fifth = lan.connect(4, "Fifth"); + assert_eq!(lan.closed.get(&fifth), Some(&Some(Refusal::WorldFull))); + + lan.tick(); + let late = lan.connect(5, "Late"); + assert_eq!(lan.closed.get(&late), Some(&Some(Refusal::WorldFull))); + assert_eq!(lan.host.guest_count(), 3); + + // Once a guest has left, there is room again. + lan.disconnect(guests[0]); + lan.tick(); + let next = lan.connect(6, "Next"); + lan.tick(); + assert!(lan.host.player_of(next).is_some()); + lan.assert_in_lockstep(); +} + +#[test] +fn a_world_that_knows_sixteen_players_refuses_a_new_one_but_not_an_old_one() { + let mut lan = Lan::new(5); + // The host and fifteen guests who come and go. + for key in 1..MAX_KNOWN_PLAYERS as u8 { + let peer = lan.connect(key, "Visitor"); + lan.tick(); + lan.disconnect(peer); + lan.tick(); + } + assert_eq!(lan.host.world().players().len(), MAX_KNOWN_PLAYERS); + + let stranger = lan.connect(200, "Stranger"); + assert_eq!( + lan.closed.get(&stranger), + Some(&Some(Refusal::TooManyPlayers)) + ); + let regular = lan.connect(3, "Visitor"); + lan.tick(); + assert!(lan.host.player_of(regular).is_some()); +} + +#[test] +fn a_key_that_is_in_the_world_already_is_refused() { + let mut lan = Lan::new(5); + let ada = lan.connect(1, "Ada"); + let twin = lan.connect(1, "Ada"); + assert_eq!(lan.closed.get(&twin), Some(&Some(Refusal::AlreadyPlaying))); + lan.tick(); + let impostor = lan.connect(HOST_KEY[0], "Host"); + assert_eq!( + lan.closed.get(&impostor), + Some(&Some(Refusal::AlreadyPlaying)) + ); + assert!(lan.host.player_of(ada).is_some()); +} + +#[test] +fn a_guest_that_leaves_is_offline_everywhere_and_keeps_its_bag() { + let mut lan = Lan::new(5); + let ada = lan.connect(1, "Ada"); + let grace = lan.connect(2, "Grace"); + lan.tick(); + let rock = lan.rock(); + for _ in 0..3 { + lan.guest(ada).act(Action::Dig { pos: rock }); + lan.ticks(DIG_COOLDOWN_TICKS + 1); + } + let player = lan.host.player_of(ada).unwrap(); + + lan.disconnect(ada); + lan.tick(); + assert!(!lan.host.world().player(player).unwrap().online); + assert!( + !lan.guest(grace) + .world() + .unwrap() + .player(player) + .unwrap() + .online + ); + lan.assert_in_lockstep(); + + let back = lan.connect(1, "Ada"); + lan.tick(); + assert_eq!(lan.host.player_of(back), Some(player)); + assert_eq!(lan.guest(back).local_player(), Some(player)); + assert_eq!( + lan.host + .world() + .player(player) + .unwrap() + .inventory + .count(ITEM_STONE), + 1 + ); + lan.assert_in_lockstep(); +} + +#[test] +fn a_guest_that_goes_before_its_join_ran_never_enters() { + let mut lan = Lan::new(5); + let ada = lan.connect(1, "Ada"); + lan.disconnect(ada); + lan.tick(); + assert_eq!(lan.host.world().find_player(&[1; 16]), None); + assert_eq!(lan.host.guest_count(), 0); +} + +#[test] +fn a_guest_that_breaks_the_protocol_is_closed() { + let mut lan = Lan::new(5); + let act = ToHost::Act(Action::Dig { + pos: Pos::new(1, 1), + }) + .encode(); + let broken = [vec![], vec![0xEE], vec![2; 200], act.clone()]; + for (index, bytes) in broken.iter().enumerate() { + let peer = 50 + index as Peer; + lan.host.connect(peer); + lan.host.receive(peer, bytes); + assert_eq!(lan.host.take_closing(), vec![peer], "message {index}"); + } + + // Messages from a connection the host does not know are ignored. + lan.host.receive(77, &act); + assert!(lan.host.take_outbox().is_empty()); + assert!(lan.host.take_closing().is_empty()); + + // A second greeting from a guest in the world closes it, and its + // player leaves. + let ada = lan.connect(1, "Ada"); + lan.tick(); + let player = lan.host.player_of(ada).unwrap(); + let hello = ToHost::Hello { + rules: rules_id(), + key: [1; 16], + name: "Ada".to_owned(), + }; + lan.host.receive(ada, &hello.encode()); + assert_eq!(lan.host.take_closing(), vec![ada]); + lan.host.step(); + assert!(!lan.host.world().player(player).unwrap().online); +} + +#[test] +fn a_guest_that_diverges_gets_the_world_again_and_keeps_the_evidence() { + let mut lan = Lan::new(5); + let ada = lan.connect(1, "Ada"); + lan.tick(); + // The host's player hits the rock, but Ada's copy of that tick loses the + // command, as if her machine had run it differently. + lan.host.act_local(Action::Dig { pos: lan.rock() }); + lan.host.step(); + lan.deliver_changed(|_, bytes| match ToGuest::decode(&bytes) { + Ok(ToGuest::Tick { tick, check, .. }) => ToGuest::Tick { + tick, + commands: vec![], + check, + } + .encode(), + _ => bytes, + }); + while lan.guest(ada).step().is_some() {} + assert!( + !lan.guest(ada).has_diverged(), + "nobody notices before the next checksum" + ); + + lan.ticks(CHECK_INTERVAL_TICKS); + lan.assert_in_lockstep(); + assert!(!lan.guest(ada).has_diverged()); + let divergences = lan.host.take_divergences(); + assert_eq!(divergences.len(), 1); + assert_eq!(divergences[0].peer, ada); + + let evidence = lan + .guest(ada) + .take_diverged_recording() + .expect("the recording of the diverged world is kept"); + let replayed = Recording::verify(&evidence).expect("the evidence replays"); + assert_eq!(replayed.state_hash(), divergences[0].hash); + assert_eq!(lan.guest(ada).take_diverged_recording(), None); +} + +#[test] +fn a_long_busy_session_stays_in_lockstep() { + let mut lan = Lan::new(77); + let mut rng = Rng::new(9); + let mut peers: Vec = [1, 2, 3].map(|key| lan.connect(key, "Busy")).to_vec(); + let area = TEST_MAP_SIZE / 3; + let random_pos = |rng: &mut Rng| { + Pos::new( + (area + rng.below(area)) as i32, + (area + rng.below(area)) as i32, + ) + }; + for tick in 0..2_000 { + for &peer in &peers { + if rng.below(3) == 0 { + let pos = random_pos(&mut rng); + lan.guest(peer).act(Action::Dig { pos }); + } + } + if rng.below(2) == 0 { + let pos = random_pos(&mut rng); + lan.host.act_local(Action::Dig { pos }); + } + if tick == 700 { + lan.disconnect(peers.remove(1)); + } + if tick == 1_100 { + peers.push(lan.connect(2, "Busy")); + } + lan.tick(); + } + + lan.assert_in_lockstep(); + assert!(lan.host.take_divergences().is_empty()); + for guest in lan.guests.values() { + let replayed = guest.recording().unwrap().replay().unwrap(); + assert_eq!(&replayed, guest.world().unwrap()); + } +}