M1c: Gleichschritt im Netz #9

Merged
steven merged 20 commits from m1c-gleichschritt-im-netz into main 2026-10-02 06:41:02 +00:00
4 changed files with 1072 additions and 0 deletions
Showing only changes of commit b48556d1f2 - Show all commits
+231
View File
@@ -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<Command>,
check: Option<u64>,
}
/// 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<Following>,
/// True after a divergence, until the host's world arrives again.
diverged: bool,
pending: VecDeque<PendingTick>,
outbox: Vec<Vec<u8>>,
diverged_recording: Option<Vec<u8>>,
}
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<PlayerId> {
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<Arrival, GuestError> {
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<Vec<Event>> {
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<Vec<u8>> {
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<Vec<u8>> {
self.diverged_recording.take()
}
}
+323
View File
@@ -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<PlayerKey>,
local_actions: u32,
/// Commands for the next tick, in the order they arrived.
inbox: Vec<Command>,
links: BTreeMap<Peer, Link>,
outbox: Vec<(Peer, Vec<u8>)>,
closing: Vec<Peer>,
divergences: Vec<Divergence>,
}
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<PlayerId> {
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<Event> {
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<PlayerId> {
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<u8>)> {
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<Peer> {
std::mem::take(&mut self.closing)
}
/// Divergences the guests reported since the last call.
pub fn take_divergences(&mut self) -> Vec<Divergence> {
std::mem::take(&mut self.divergences)
}
}
/// The player with `key`, if it is online in `world`.
fn online_player(world: &World, key: &PlayerKey) -> Option<PlayerId> {
let id = world.find_player(key)?;
world.player(id)?.online.then_some(id)
}
+2
View File
@@ -8,7 +8,9 @@
pub mod codec; pub mod codec;
pub mod command; pub mod command;
pub mod guest;
pub mod hash; pub mod hash;
pub mod host;
pub mod inventory; pub mod inventory;
pub mod map; pub mod map;
pub mod mapgen; pub mod mapgen;
+516
View File
@@ -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<Peer, Guest>,
/// Guests whose connection the host closed, with the refusal they got.
closed: BTreeMap<Peer, Option<Refusal>>,
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<u8>) -> Vec<u8>) {
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<Peer> = [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());
}
}