Nachrichten zwischen Host und Gästen
Gäste grüßen mit Protokollversion, Regel-Kennung, Schlüssel und Name und schicken danach nur Aktionen ohne Spielernummer. Der Host schickt seine Welt, Ablehnungen mit Grund und jeden Takt mit seinen Befehlen, jede Sekunde mit einer Prüfsumme. Ein Gruß einer anderen Version bleibt erkennbar, und das Format ist in den festgeschriebenen Werten verankert. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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