M1c: Gleichschritt im Netz #9
@@ -13,6 +13,7 @@ pub mod inventory;
|
|||||||
pub mod map;
|
pub mod map;
|
||||||
pub mod mapgen;
|
pub mod mapgen;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
|
pub mod protocol;
|
||||||
pub mod replay;
|
pub mod replay;
|
||||||
pub mod rng;
|
pub mod rng;
|
||||||
pub mod rules;
|
pub mod rules;
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
//! Messages between the host of a shared world and its guests.
|
||||||
|
//!
|
||||||
|
//! Every machine simulates the whole world. The host decides which commands
|
||||||
|
//! run in which tick and sends every tick to every guest; a guest sends the
|
||||||
|
//! host only what its player wants to do. The shell carries these messages
|
||||||
|
//! as they are, and it must deliver the host's messages to a guest reliably
|
||||||
|
//! and in the order they were sent.
|
||||||
|
//!
|
||||||
|
//! The tag and the version at the start of a greeting, and the whole refusal
|
||||||
|
//! message, never change. That way games of every version can still tell
|
||||||
|
//! each other that they do not fit together.
|
||||||
|
|
||||||
|
use crate::codec::{DecodeError, Reader, Writer};
|
||||||
|
use crate::command::Command;
|
||||||
|
use crate::map::Pos;
|
||||||
|
use crate::player::{clamp_name, PlayerId, PlayerKey, MAX_NAME_BYTES};
|
||||||
|
use crate::world::TICKS_PER_SECOND;
|
||||||
|
|
||||||
|
/// Version of the messages. Raise it whenever a message changes.
|
||||||
|
pub const PROTOCOL_VERSION: u32 = 1;
|
||||||
|
/// Longest message a guest may send, in bytes. The longest one is a greeting
|
||||||
|
/// with a name of full length, which needs 65 bytes.
|
||||||
|
pub const MAX_GUEST_MESSAGE_BYTES: usize = 128;
|
||||||
|
/// Ticks between two state checksums in the stream of ticks: one second.
|
||||||
|
pub const CHECK_INTERVAL_TICKS: u64 = TICKS_PER_SECOND as u64;
|
||||||
|
|
||||||
|
const TO_HOST_HELLO: u8 = 1;
|
||||||
|
const TO_HOST_ACT: u8 = 2;
|
||||||
|
const TO_HOST_DIVERGED: u8 = 3;
|
||||||
|
|
||||||
|
const TO_GUEST_WORLD: u8 = 1;
|
||||||
|
const TO_GUEST_REFUSED: u8 = 2;
|
||||||
|
const TO_GUEST_TICK: u8 = 3;
|
||||||
|
|
||||||
|
const ACTION_DIG: u8 = 1;
|
||||||
|
|
||||||
|
/// Something a player wants to do. The host turns it into a command for the
|
||||||
|
/// player of the connection it came from, so that no guest can act for
|
||||||
|
/// another player.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Action {
|
||||||
|
/// Hit the tile at `pos` with the pickaxe.
|
||||||
|
Dig { pos: Pos },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Action {
|
||||||
|
/// The command that carries out this action for `player`.
|
||||||
|
pub fn command(self, player: PlayerId) -> Command {
|
||||||
|
match self {
|
||||||
|
Action::Dig { pos } => Command::Dig { player, pos },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode(&self, w: &mut Writer) {
|
||||||
|
match self {
|
||||||
|
Action::Dig { pos } => {
|
||||||
|
w.u8(ACTION_DIG);
|
||||||
|
w.i32(pos.x);
|
||||||
|
w.i32(pos.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
|
||||||
|
match r.u8()? {
|
||||||
|
ACTION_DIG => {
|
||||||
|
let x = r.i32()?;
|
||||||
|
let y = r.i32()?;
|
||||||
|
Ok(Action::Dig {
|
||||||
|
pos: Pos::new(x, y),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Err(DecodeError::InvalidValue("action tag")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Why a host turned a guest away.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Refusal {
|
||||||
|
/// The guest's game speaks another version of these messages.
|
||||||
|
OtherProtocol = 1,
|
||||||
|
/// The guest's game has other rules than the host's.
|
||||||
|
OtherRules = 2,
|
||||||
|
/// Four players are online already.
|
||||||
|
WorldFull = 3,
|
||||||
|
/// The world remembers as many players as it can and does not know this
|
||||||
|
/// one.
|
||||||
|
TooManyPlayers = 4,
|
||||||
|
/// A player with the same key is in the world already.
|
||||||
|
AlreadyPlaying = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Refusal {
|
||||||
|
/// The number that stands for this refusal in a message.
|
||||||
|
pub fn code(self) -> u8 {
|
||||||
|
self as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_code(code: u8) -> Option<Self> {
|
||||||
|
match code {
|
||||||
|
1 => Some(Refusal::OtherProtocol),
|
||||||
|
2 => Some(Refusal::OtherRules),
|
||||||
|
3 => Some(Refusal::WorldFull),
|
||||||
|
4 => Some(Refusal::TooManyPlayers),
|
||||||
|
5 => Some(Refusal::AlreadyPlaying),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A message from a guest to the host.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum ToHost {
|
||||||
|
/// The first message of every guest.
|
||||||
|
Hello {
|
||||||
|
rules: u64,
|
||||||
|
key: PlayerKey,
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
/// A greeting in another version of the messages. Nothing after the
|
||||||
|
/// version can be read.
|
||||||
|
ForeignHello { protocol: u32 },
|
||||||
|
/// The guest's player wants to do something.
|
||||||
|
Act(Action),
|
||||||
|
/// After `tick` ticks the guest's state had the hash `hash`, which
|
||||||
|
/// differs from the host's checksum. The host answers with its world.
|
||||||
|
Diverged { tick: u64, hash: u64 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToHost {
|
||||||
|
pub fn encode(&self) -> Vec<u8> {
|
||||||
|
let mut w = Writer::new();
|
||||||
|
match self {
|
||||||
|
ToHost::Hello { rules, key, name } => {
|
||||||
|
w.u8(TO_HOST_HELLO);
|
||||||
|
w.u32(PROTOCOL_VERSION);
|
||||||
|
w.u64(*rules);
|
||||||
|
w.raw(key);
|
||||||
|
w.str(&clamp_name(name));
|
||||||
|
}
|
||||||
|
ToHost::ForeignHello { protocol } => {
|
||||||
|
w.u8(TO_HOST_HELLO);
|
||||||
|
w.u32(*protocol);
|
||||||
|
}
|
||||||
|
ToHost::Act(action) => {
|
||||||
|
w.u8(TO_HOST_ACT);
|
||||||
|
action.encode(&mut w);
|
||||||
|
}
|
||||||
|
ToHost::Diverged { tick, hash } => {
|
||||||
|
w.u8(TO_HOST_DIVERGED);
|
||||||
|
w.u64(*tick);
|
||||||
|
w.u64(*hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.into_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes a whole message. Messages longer than
|
||||||
|
/// [`MAX_GUEST_MESSAGE_BYTES`] are rejected unread.
|
||||||
|
pub fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
|
||||||
|
if bytes.len() > MAX_GUEST_MESSAGE_BYTES {
|
||||||
|
return Err(DecodeError::InvalidValue("message length"));
|
||||||
|
}
|
||||||
|
let mut r = Reader::new(bytes);
|
||||||
|
let message = match r.u8()? {
|
||||||
|
TO_HOST_HELLO => {
|
||||||
|
let protocol = r.u32()?;
|
||||||
|
if protocol != PROTOCOL_VERSION {
|
||||||
|
return Ok(ToHost::ForeignHello { protocol });
|
||||||
|
}
|
||||||
|
let rules = r.u64()?;
|
||||||
|
let mut key = [0u8; 16];
|
||||||
|
key.copy_from_slice(r.raw(16)?);
|
||||||
|
let name = r.string()?;
|
||||||
|
if name.len() > MAX_NAME_BYTES {
|
||||||
|
return Err(DecodeError::InvalidValue("player name"));
|
||||||
|
}
|
||||||
|
ToHost::Hello { rules, key, name }
|
||||||
|
}
|
||||||
|
TO_HOST_ACT => ToHost::Act(Action::decode(&mut r)?),
|
||||||
|
TO_HOST_DIVERGED => ToHost::Diverged {
|
||||||
|
tick: r.u64()?,
|
||||||
|
hash: r.u64()?,
|
||||||
|
},
|
||||||
|
_ => return Err(DecodeError::InvalidValue("message tag")),
|
||||||
|
};
|
||||||
|
r.finish()?;
|
||||||
|
Ok(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A message from the host to a guest.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum ToGuest {
|
||||||
|
/// The world as it is now. The host sends it when it accepts a guest and
|
||||||
|
/// again after the guest reported a divergence. The ticks that follow
|
||||||
|
/// start at this world's tick.
|
||||||
|
World { snapshot: Vec<u8> },
|
||||||
|
/// The host turned the guest away and closes the connection.
|
||||||
|
Refused(Refusal),
|
||||||
|
/// The commands of tick `tick` in the order the host ran them. Every
|
||||||
|
/// [`CHECK_INTERVAL_TICKS`] ticks, `check` holds the hash of the host's
|
||||||
|
/// state after the tick.
|
||||||
|
Tick {
|
||||||
|
tick: u64,
|
||||||
|
commands: Vec<Command>,
|
||||||
|
check: Option<u64>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToGuest {
|
||||||
|
pub fn encode(&self) -> Vec<u8> {
|
||||||
|
let mut w = Writer::new();
|
||||||
|
match self {
|
||||||
|
ToGuest::World { snapshot } => {
|
||||||
|
w.u8(TO_GUEST_WORLD);
|
||||||
|
w.bytes(snapshot);
|
||||||
|
}
|
||||||
|
ToGuest::Refused(refusal) => {
|
||||||
|
w.u8(TO_GUEST_REFUSED);
|
||||||
|
w.u8(refusal.code());
|
||||||
|
}
|
||||||
|
ToGuest::Tick {
|
||||||
|
tick,
|
||||||
|
commands,
|
||||||
|
check,
|
||||||
|
} => {
|
||||||
|
w.u8(TO_GUEST_TICK);
|
||||||
|
w.u64(*tick);
|
||||||
|
w.u16(u16::try_from(commands.len()).expect("command count fits in u16"));
|
||||||
|
for command in commands {
|
||||||
|
command.encode(&mut w);
|
||||||
|
}
|
||||||
|
match check {
|
||||||
|
Some(hash) => {
|
||||||
|
w.bool(true);
|
||||||
|
w.u64(*hash);
|
||||||
|
}
|
||||||
|
None => w.bool(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.into_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes a whole message.
|
||||||
|
pub fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
|
||||||
|
let mut r = Reader::new(bytes);
|
||||||
|
let message = match r.u8()? {
|
||||||
|
TO_GUEST_WORLD => ToGuest::World {
|
||||||
|
snapshot: r.bytes()?.to_vec(),
|
||||||
|
},
|
||||||
|
TO_GUEST_REFUSED => ToGuest::Refused(
|
||||||
|
Refusal::from_code(r.u8()?).ok_or(DecodeError::InvalidValue("refusal"))?,
|
||||||
|
),
|
||||||
|
TO_GUEST_TICK => {
|
||||||
|
let tick = r.u64()?;
|
||||||
|
let count = r.u16()?;
|
||||||
|
let mut commands = Vec::new();
|
||||||
|
for _ in 0..count {
|
||||||
|
commands.push(Command::decode(&mut r)?);
|
||||||
|
}
|
||||||
|
let check = if r.bool()? { Some(r.u64()?) } else { None };
|
||||||
|
ToGuest::Tick {
|
||||||
|
tick,
|
||||||
|
commands,
|
||||||
|
check,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return Err(DecodeError::InvalidValue("message tag")),
|
||||||
|
};
|
||||||
|
r.finish()?;
|
||||||
|
Ok(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,11 @@
|
|||||||
//! value here deliberately.
|
//! value here deliberately.
|
||||||
|
|
||||||
use grovewright_core::codec::Writer;
|
use grovewright_core::codec::Writer;
|
||||||
|
use grovewright_core::command::Command;
|
||||||
use grovewright_core::hash::fnv1a64;
|
use grovewright_core::hash::fnv1a64;
|
||||||
use grovewright_core::map::{Map, Pos, Terrain, Tile};
|
use grovewright_core::map::{Map, Pos, Terrain, Tile};
|
||||||
|
use grovewright_core::player::PlayerId;
|
||||||
|
use grovewright_core::protocol::{Action, Refusal, ToGuest, ToHost};
|
||||||
use grovewright_core::replay::Recording;
|
use grovewright_core::replay::Recording;
|
||||||
use grovewright_core::rules::{
|
use grovewright_core::rules::{
|
||||||
end_hash, frozen_offline_script, frozen_script, rules_id, RULES_SEED,
|
end_hash, frozen_offline_script, frozen_script, rules_id, RULES_SEED,
|
||||||
@@ -114,3 +117,51 @@ fn the_map_encoding_is_unchanged() {
|
|||||||
core/src/snapshot.rs; then update these bytes"
|
core/src/snapshot.rs; then update these bytes"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Covers every message between host and guests, built from literal values.
|
||||||
|
#[test]
|
||||||
|
fn the_protocol_format_is_unchanged() {
|
||||||
|
let to_host = [
|
||||||
|
ToHost::Hello {
|
||||||
|
rules: 0x0102_0304_0506_0708,
|
||||||
|
key: [3; 16],
|
||||||
|
name: "Ada".to_owned(),
|
||||||
|
},
|
||||||
|
ToHost::Act(Action::Dig {
|
||||||
|
pos: Pos::new(-2, 9),
|
||||||
|
}),
|
||||||
|
ToHost::Diverged { tick: 40, hash: 77 },
|
||||||
|
];
|
||||||
|
let to_guest = [
|
||||||
|
ToGuest::World {
|
||||||
|
snapshot: vec![5, 6, 7],
|
||||||
|
},
|
||||||
|
ToGuest::Refused(Refusal::WorldFull),
|
||||||
|
ToGuest::Tick {
|
||||||
|
tick: 12,
|
||||||
|
commands: vec![Command::Dig {
|
||||||
|
player: PlayerId(1),
|
||||||
|
pos: Pos::new(3, 4),
|
||||||
|
}],
|
||||||
|
check: Some(99),
|
||||||
|
},
|
||||||
|
ToGuest::Tick {
|
||||||
|
tick: 13,
|
||||||
|
commands: vec![],
|
||||||
|
check: None,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
for message in &to_host {
|
||||||
|
bytes.extend(message.encode());
|
||||||
|
}
|
||||||
|
for message in &to_guest {
|
||||||
|
bytes.extend(message.encode());
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
fnv1a64(&bytes),
|
||||||
|
0xc263_69c0_f892_247b,
|
||||||
|
"the messages between host and guests changed; raise \
|
||||||
|
PROTOCOL_VERSION in core/src/protocol.rs; then update this value"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
use grovewright_core::codec::{DecodeError, Writer};
|
||||||
|
use grovewright_core::command::Command;
|
||||||
|
use grovewright_core::map::Pos;
|
||||||
|
use grovewright_core::player::{PlayerId, MAX_NAME_BYTES};
|
||||||
|
use grovewright_core::protocol::{
|
||||||
|
Action, Refusal, ToGuest, ToHost, MAX_GUEST_MESSAGE_BYTES, PROTOCOL_VERSION,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_message_to_the_host_survives_encoding() {
|
||||||
|
let messages = [
|
||||||
|
ToHost::Hello {
|
||||||
|
rules: 0x0123_4567_89ab_cdef,
|
||||||
|
key: [9; 16],
|
||||||
|
name: "Ada".to_owned(),
|
||||||
|
},
|
||||||
|
ToHost::Act(Action::Dig {
|
||||||
|
pos: Pos::new(-3, 70),
|
||||||
|
}),
|
||||||
|
ToHost::Diverged {
|
||||||
|
tick: 1_234,
|
||||||
|
hash: u64::MAX,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for message in &messages {
|
||||||
|
assert_eq!(&ToHost::decode(&message.encode()).unwrap(), message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_message_to_a_guest_survives_encoding() {
|
||||||
|
let messages = [
|
||||||
|
ToGuest::World {
|
||||||
|
snapshot: vec![1, 2, 3],
|
||||||
|
},
|
||||||
|
ToGuest::Refused(Refusal::TooManyPlayers),
|
||||||
|
ToGuest::Tick {
|
||||||
|
tick: 99,
|
||||||
|
commands: vec![
|
||||||
|
Command::Leave {
|
||||||
|
player: PlayerId(2),
|
||||||
|
},
|
||||||
|
Command::Dig {
|
||||||
|
player: PlayerId(0),
|
||||||
|
pos: Pos::new(4, 5),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
check: Some(7),
|
||||||
|
},
|
||||||
|
ToGuest::Tick {
|
||||||
|
tick: 100,
|
||||||
|
commands: vec![],
|
||||||
|
check: None,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for message in &messages {
|
||||||
|
assert_eq!(&ToGuest::decode(&message.encode()).unwrap(), message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_action_becomes_a_command_for_the_given_player() {
|
||||||
|
let action = Action::Dig {
|
||||||
|
pos: Pos::new(1, 2),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
action.command(PlayerId(3)),
|
||||||
|
Command::Dig {
|
||||||
|
player: PlayerId(3),
|
||||||
|
pos: Pos::new(1, 2)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_greeting_in_another_protocol_version_can_still_be_recognised() {
|
||||||
|
let mut w = Writer::new();
|
||||||
|
w.u8(1); // the tag of a greeting, which never changes
|
||||||
|
w.u32(PROTOCOL_VERSION + 1);
|
||||||
|
w.raw(b"whatever the other version sends after its version");
|
||||||
|
assert_eq!(
|
||||||
|
ToHost::decode(&w.into_bytes()),
|
||||||
|
Ok(ToHost::ForeignHello {
|
||||||
|
protocol: PROTOCOL_VERSION + 1
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_refusal_keeps_its_code() {
|
||||||
|
let codes = [
|
||||||
|
(Refusal::OtherProtocol, 1),
|
||||||
|
(Refusal::OtherRules, 2),
|
||||||
|
(Refusal::WorldFull, 3),
|
||||||
|
(Refusal::TooManyPlayers, 4),
|
||||||
|
(Refusal::AlreadyPlaying, 5),
|
||||||
|
];
|
||||||
|
for (refusal, code) in codes {
|
||||||
|
assert_eq!(refusal.code(), code);
|
||||||
|
assert_eq!(Refusal::from_code(code), Some(refusal));
|
||||||
|
}
|
||||||
|
assert_eq!(Refusal::from_code(0), None);
|
||||||
|
assert_eq!(Refusal::from_code(6), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_greeting_shortens_an_overlong_name() {
|
||||||
|
let hello = ToHost::Hello {
|
||||||
|
rules: 1,
|
||||||
|
key: [1; 16],
|
||||||
|
name: "x".repeat(MAX_NAME_BYTES + 5),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
ToHost::decode(&hello.encode()),
|
||||||
|
Ok(ToHost::Hello {
|
||||||
|
rules: 1,
|
||||||
|
key: [1; 16],
|
||||||
|
name: "x".repeat(MAX_NAME_BYTES),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_greeting_with_an_overlong_name_written_by_hand_is_rejected() {
|
||||||
|
let mut w = Writer::new();
|
||||||
|
w.u8(1);
|
||||||
|
w.u32(PROTOCOL_VERSION);
|
||||||
|
w.u64(1);
|
||||||
|
w.raw(&[1; 16]);
|
||||||
|
w.str(&"x".repeat(MAX_NAME_BYTES + 1));
|
||||||
|
assert_eq!(
|
||||||
|
ToHost::decode(&w.into_bytes()),
|
||||||
|
Err(DecodeError::InvalidValue("player name"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn broken_messages_are_rejected() {
|
||||||
|
assert_eq!(ToHost::decode(&[]), Err(DecodeError::UnexpectedEnd));
|
||||||
|
assert_eq!(
|
||||||
|
ToHost::decode(&[0xEE]),
|
||||||
|
Err(DecodeError::InvalidValue("message tag"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ToHost::decode(&[2, 0xEE]),
|
||||||
|
Err(DecodeError::InvalidValue("action tag"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ToHost::decode(&[2; MAX_GUEST_MESSAGE_BYTES + 1]),
|
||||||
|
Err(DecodeError::InvalidValue("message length"))
|
||||||
|
);
|
||||||
|
let mut act = ToHost::Act(Action::Dig {
|
||||||
|
pos: Pos::new(0, 0),
|
||||||
|
})
|
||||||
|
.encode();
|
||||||
|
act.push(0);
|
||||||
|
assert_eq!(ToHost::decode(&act), Err(DecodeError::TrailingBytes));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ToGuest::decode(&[0xEE]),
|
||||||
|
Err(DecodeError::InvalidValue("message tag"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ToGuest::decode(&[2, 99]),
|
||||||
|
Err(DecodeError::InvalidValue("refusal"))
|
||||||
|
);
|
||||||
|
let tick = ToGuest::Tick {
|
||||||
|
tick: 1,
|
||||||
|
commands: vec![],
|
||||||
|
check: Some(5),
|
||||||
|
}
|
||||||
|
.encode();
|
||||||
|
assert_eq!(
|
||||||
|
ToGuest::decode(&tick[..tick.len() - 1]),
|
||||||
|
Err(DecodeError::UnexpectedEnd)
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user