M1c: Gleichschritt im Netz #9
@@ -0,0 +1,296 @@
|
|||||||
|
//! Host and guest driven by hand, message by message. These tests produce
|
||||||
|
//! what the in-memory network of `session.rs` never does, because it lets
|
||||||
|
//! every guest run each tick before the next one arrives: a guest that lags
|
||||||
|
//! behind, a tick that gets lost, and joins that wait at the limits.
|
||||||
|
|
||||||
|
use grovewright_core::command::Command;
|
||||||
|
use grovewright_core::guest::{Arrival, Guest};
|
||||||
|
use grovewright_core::host::{Host, Peer, MAX_ACTIONS_PER_TICK};
|
||||||
|
use grovewright_core::map::Pos;
|
||||||
|
use grovewright_core::mapgen::outcrop_origin;
|
||||||
|
use grovewright_core::player::{PlayerId, PlayerKey};
|
||||||
|
use grovewright_core::protocol::{Action, Refusal, ToGuest, ToHost, CHECK_INTERVAL_TICKS};
|
||||||
|
use grovewright_core::world::{Event, World};
|
||||||
|
|
||||||
|
const HOST_KEY: PlayerKey = [100; 16];
|
||||||
|
|
||||||
|
/// A host whose own player is in a new world.
|
||||||
|
fn started_host() -> Host {
|
||||||
|
let mut host = Host::new(World::new(5));
|
||||||
|
host.join_local(HOST_KEY, "Host");
|
||||||
|
host.step();
|
||||||
|
host
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connects a guest with a key made of `key` as `peer` and hands its
|
||||||
|
/// greeting to the host.
|
||||||
|
fn greet(host: &mut Host, peer: Peer, key: u8) -> Guest {
|
||||||
|
host.connect(peer);
|
||||||
|
let mut guest = Guest::new([key; 16], "Guest");
|
||||||
|
for bytes in guest.take_outbox() {
|
||||||
|
host.receive(peer, &bytes);
|
||||||
|
}
|
||||||
|
guest
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes the host's whole outbox and keeps what is for `peer`.
|
||||||
|
fn messages_for(host: &mut Host, peer: Peer) -> Vec<Vec<u8>> {
|
||||||
|
host.take_outbox()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(to, _)| *to == peer)
|
||||||
|
.map(|(_, bytes)| bytes)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The host's answer to a greeting from `peer`: `None` for its world, or
|
||||||
|
/// the reason it turned the guest away.
|
||||||
|
fn answer_to(host: &mut Host, peer: Peer) -> Option<Refusal> {
|
||||||
|
let messages = messages_for(host, peer);
|
||||||
|
match ToGuest::decode(&messages[0]) {
|
||||||
|
Ok(ToGuest::World { .. }) => None,
|
||||||
|
Ok(ToGuest::Refused(refusal)) => Some(refusal),
|
||||||
|
other => panic!("unexpected answer {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A host and the guest Ada at peer 2, in the same state.
|
||||||
|
fn host_with_ada() -> (Host, Guest) {
|
||||||
|
let mut host = started_host();
|
||||||
|
let mut ada = greet(&mut host, 2, 1);
|
||||||
|
for bytes in messages_for(&mut host, 2) {
|
||||||
|
ada.receive(&bytes).unwrap();
|
||||||
|
}
|
||||||
|
host.step();
|
||||||
|
for bytes in messages_for(&mut host, 2) {
|
||||||
|
ada.receive(&bytes).unwrap();
|
||||||
|
}
|
||||||
|
while ada.step().is_some() {}
|
||||||
|
assert!(ada.local_player().is_some());
|
||||||
|
assert_eq!(
|
||||||
|
ada.world().map(World::state_hash),
|
||||||
|
Some(host.world().state_hash())
|
||||||
|
);
|
||||||
|
(host, ada)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A world that knows players with keys made of 1 to `count`, all offline.
|
||||||
|
fn world_with_offline_players(count: u8) -> World {
|
||||||
|
let mut world = World::new(5);
|
||||||
|
for key in 1..=count {
|
||||||
|
world.step(&[Command::Join {
|
||||||
|
key: [key; 16],
|
||||||
|
name: "Visitor".to_owned(),
|
||||||
|
}]);
|
||||||
|
world.step(&[Command::Leave {
|
||||||
|
player: PlayerId(key - 1),
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
world
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_hosts_own_queued_join_counts_for_the_online_limit() {
|
||||||
|
let mut host = Host::new(World::new(5));
|
||||||
|
host.join_local(HOST_KEY, "Host");
|
||||||
|
// No tick has run: the host's join is still queued.
|
||||||
|
let mut answers = Vec::new();
|
||||||
|
for key in 1..=4 {
|
||||||
|
let peer = Peer::from(key) + 1;
|
||||||
|
greet(&mut host, peer, key);
|
||||||
|
answers.push(answer_to(&mut host, peer));
|
||||||
|
}
|
||||||
|
assert_eq!(answers, [None, None, None, Some(Refusal::WorldFull)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn queued_new_keys_count_for_the_limit_of_known_players_but_known_ones_do_not() {
|
||||||
|
let mut host = Host::new(world_with_offline_players(15));
|
||||||
|
greet(&mut host, 2, 3);
|
||||||
|
assert_eq!(answer_to(&mut host, 2), None, "a returning player");
|
||||||
|
greet(&mut host, 3, 200);
|
||||||
|
assert_eq!(
|
||||||
|
answer_to(&mut host, 3),
|
||||||
|
None,
|
||||||
|
"a new player fits as the 16th"
|
||||||
|
);
|
||||||
|
greet(&mut host, 4, 201);
|
||||||
|
assert_eq!(
|
||||||
|
answer_to(&mut host, 4),
|
||||||
|
Some(Refusal::TooManyPlayers),
|
||||||
|
"a second new player does not"
|
||||||
|
);
|
||||||
|
|
||||||
|
let events = host.step();
|
||||||
|
assert!(!events
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, Event::JoinRefused { .. })));
|
||||||
|
assert_eq!(host.world().players().len(), 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_queued_join_of_a_player_who_is_online_already_counts_once() {
|
||||||
|
let mut world = World::new(5);
|
||||||
|
let joins = [HOST_KEY, [1; 16], [2; 16]].map(|key| Command::Join {
|
||||||
|
key,
|
||||||
|
name: "Online".to_owned(),
|
||||||
|
});
|
||||||
|
world.step(&joins);
|
||||||
|
let mut host = Host::new(world);
|
||||||
|
// The host's player is online already; its queued join changes nothing.
|
||||||
|
host.join_local(HOST_KEY, "Host");
|
||||||
|
greet(&mut host, 2, 3);
|
||||||
|
assert_eq!(answer_to(&mut host, 2), None, "the fourth player fits");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_count_of_a_guests_actions_starts_again_in_every_tick() {
|
||||||
|
let (mut host, mut ada) = host_with_ada();
|
||||||
|
for tick in 0..3 {
|
||||||
|
for x in 0..10 {
|
||||||
|
ada.act(Action::Dig {
|
||||||
|
pos: Pos::new(x, 1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for bytes in ada.take_outbox() {
|
||||||
|
host.receive(2, &bytes);
|
||||||
|
}
|
||||||
|
host.step();
|
||||||
|
let counts: Vec<usize> = messages_for(&mut host, 2)
|
||||||
|
.iter()
|
||||||
|
.filter_map(|bytes| match ToGuest::decode(bytes) {
|
||||||
|
Ok(ToGuest::Tick { commands, .. }) => Some(commands.len()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert_eq!(counts, [MAX_ACTIONS_PER_TICK as usize], "tick {tick}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_tick_carries_the_number_of_the_tick_before_it_runs() {
|
||||||
|
let (mut host, mut ada) = host_with_ada();
|
||||||
|
let before = host.world().tick();
|
||||||
|
host.step();
|
||||||
|
let messages = messages_for(&mut host, 2);
|
||||||
|
let Ok(ToGuest::Tick { tick, .. }) = ToGuest::decode(&messages[0]) else {
|
||||||
|
panic!("not a tick");
|
||||||
|
};
|
||||||
|
assert_eq!(tick, before);
|
||||||
|
for bytes in messages {
|
||||||
|
ada.receive(&bytes).unwrap();
|
||||||
|
}
|
||||||
|
ada.step().unwrap();
|
||||||
|
assert_eq!(ada.world().map(World::tick), Some(before + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_lagging_guest_that_diverges_runs_nothing_until_the_world_comes_and_drops_older_ticks() {
|
||||||
|
let (mut host, mut ada) = host_with_ada();
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
host.act_local(Action::Dig {
|
||||||
|
pos: outcrop_origin(host.world().map()),
|
||||||
|
});
|
||||||
|
host.step();
|
||||||
|
for bytes in messages_for(&mut host, 2) {
|
||||||
|
let bytes = match ToGuest::decode(&bytes) {
|
||||||
|
Ok(ToGuest::Tick { tick, check, .. }) => ToGuest::Tick {
|
||||||
|
tick,
|
||||||
|
commands: vec![],
|
||||||
|
check,
|
||||||
|
}
|
||||||
|
.encode(),
|
||||||
|
_ => bytes,
|
||||||
|
};
|
||||||
|
ada.receive(&bytes).unwrap();
|
||||||
|
}
|
||||||
|
while ada.step().is_some() {}
|
||||||
|
assert!(!ada.has_diverged());
|
||||||
|
|
||||||
|
// Thirty more ticks arrive before Ada runs any of them.
|
||||||
|
for _ in 0..30 {
|
||||||
|
host.step();
|
||||||
|
for bytes in messages_for(&mut host, 2) {
|
||||||
|
ada.receive(&bytes).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(ada.pending_ticks() >= 30);
|
||||||
|
|
||||||
|
// She catches up and trips over the next checksum.
|
||||||
|
while ada.step().is_some() && !ada.has_diverged() {}
|
||||||
|
assert!(ada.has_diverged());
|
||||||
|
let waiting = ada.pending_ticks();
|
||||||
|
assert!(waiting > 0, "newer ticks are still waiting");
|
||||||
|
assert!(ada.step().is_none(), "no tick runs after a divergence");
|
||||||
|
assert_eq!(ada.pending_ticks(), waiting, "and none is used up");
|
||||||
|
|
||||||
|
// Her report names her tick after the step that showed the divergence.
|
||||||
|
let reports = ada.take_outbox();
|
||||||
|
assert_eq!(reports.len(), 1);
|
||||||
|
let Ok(ToHost::Diverged { tick, hash }) = ToHost::decode(&reports[0]) else {
|
||||||
|
panic!("not a divergence report");
|
||||||
|
};
|
||||||
|
assert_eq!(Some(tick), ada.world().map(World::tick));
|
||||||
|
assert_eq!(tick % CHECK_INTERVAL_TICKS, 0);
|
||||||
|
host.receive(2, &reports[0]);
|
||||||
|
let divergences = host.take_divergences();
|
||||||
|
assert_eq!(divergences.len(), 1);
|
||||||
|
assert_eq!((divergences[0].tick, divergences[0].hash), (tick, hash));
|
||||||
|
|
||||||
|
// The host has gone on meanwhile. Its world comes first, then later ticks.
|
||||||
|
let world_tick = host.world().tick();
|
||||||
|
let answer = messages_for(&mut host, 2);
|
||||||
|
assert_eq!(answer.len(), 1);
|
||||||
|
host.step();
|
||||||
|
host.step();
|
||||||
|
let later = messages_for(&mut host, 2);
|
||||||
|
assert_eq!(later.len(), 2);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ada.receive(&answer[0]),
|
||||||
|
Ok(Arrival::World { replaced: true })
|
||||||
|
);
|
||||||
|
assert_eq!(ada.pending_ticks(), 0, "the older ticks are dropped");
|
||||||
|
assert_eq!(ada.world().map(World::tick), Some(world_tick));
|
||||||
|
assert!(!ada.has_diverged());
|
||||||
|
for bytes in later {
|
||||||
|
ada.receive(&bytes).unwrap();
|
||||||
|
}
|
||||||
|
while ada.step().is_some() {}
|
||||||
|
assert_eq!(
|
||||||
|
ada.world().map(World::state_hash),
|
||||||
|
Some(host.world().state_hash())
|
||||||
|
);
|
||||||
|
assert!(ada.take_diverged_recording().is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_missing_tick_makes_the_guest_ask_for_the_world() {
|
||||||
|
let (mut host, mut ada) = host_with_ada();
|
||||||
|
host.step();
|
||||||
|
// This tick never arrives.
|
||||||
|
messages_for(&mut host, 2);
|
||||||
|
host.step();
|
||||||
|
for bytes in messages_for(&mut host, 2) {
|
||||||
|
ada.receive(&bytes).unwrap();
|
||||||
|
}
|
||||||
|
assert!(ada.step().is_none());
|
||||||
|
assert!(ada.has_diverged());
|
||||||
|
let reports = ada.take_outbox();
|
||||||
|
assert_eq!(reports.len(), 1);
|
||||||
|
|
||||||
|
host.receive(2, &reports[0]);
|
||||||
|
for bytes in messages_for(&mut host, 2) {
|
||||||
|
assert_eq!(ada.receive(&bytes), Ok(Arrival::World { replaced: true }));
|
||||||
|
}
|
||||||
|
host.step();
|
||||||
|
for bytes in messages_for(&mut host, 2) {
|
||||||
|
ada.receive(&bytes).unwrap();
|
||||||
|
}
|
||||||
|
while ada.step().is_some() {}
|
||||||
|
assert_eq!(
|
||||||
|
ada.world().map(World::state_hash),
|
||||||
|
Some(host.world().state_hash())
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user