use flatland_protocol::frame::{
read_client_message, read_server_message, write_client_message, write_server_message,
};
use flatland_protocol::{
AuthCredential, ChatChannel, ChatMessage, ClientMessage, CraftResult, DeathNotice, EntityState,
Hello, InteractionNotice, Intent, ItemStack, ServerMessage, Snapshot, Transform, UseResult,
Velocity2D, Welcome, WorldClock, WorldCoord, PROTOCOL_VERSION,
};
use flatland_protocol::types::{HarvestResult, NpcView, TickDelta};
use uuid::Uuid;
async fn assert_server_roundtrip(msg: ServerMessage) {
let mut buf = Vec::new();
write_server_message(&mut buf, &msg)
.await
.unwrap_or_else(|e| panic!("encode failed for {msg:?}: {e}"));
let decoded = read_server_message(&mut buf.as_slice())
.await
.unwrap_or_else(|e| panic!("decode failed for {msg:?}: {e}"));
assert_eq!(msg, decoded, "round-trip mismatch");
}
async fn assert_client_roundtrip(msg: ClientMessage) {
let mut buf = Vec::new();
write_client_message(&mut buf, &msg)
.await
.unwrap_or_else(|e| panic!("encode failed for {msg:?}: {e}"));
let decoded = read_client_message(&mut buf.as_slice())
.await
.unwrap_or_else(|e| panic!("decode failed for {msg:?}: {e}"));
assert_eq!(msg, decoded, "round-trip mismatch");
}
fn empty_snapshot() -> Snapshot {
Snapshot {
tick: 1,
chunk_rev: 0,
content_rev: 0,
entities: Vec::new(),
resource_nodes: Vec::new(),
world_width_m: 0.0,
world_height_m: 0.0,
buildings: Vec::new(),
doors: Vec::new(),
npcs: Vec::new(),
inventory: Vec::new(),
blueprints: Vec::new(),
world_clock: WorldClock::default(),
terrain_zones: Vec::new(),
ground_drops: Vec::new(),
placed_containers: Vec::new(),
combat: None,
}
}
fn empty_tick_delta() -> TickDelta {
TickDelta {
tick: 1,
entities: Vec::new(),
resource_nodes: Vec::new(),
buildings: Vec::new(),
doors: Vec::new(),
npcs: Vec::new(),
inventory: Vec::new(),
blueprints: Vec::new(),
world_clock: WorldClock::default(),
ground_drops: Vec::new(),
placed_containers: Vec::new(),
combat: None,
}
}
fn bare_entity_state() -> EntityState {
EntityState {
id: 1,
transform: Transform {
position: WorldCoord::surface(0.0, 0.0),
yaw: 0.0,
velocity: Velocity2D { vx: 0.0, vy: 0.0 },
},
label: String::new(),
vitals: None,
attributes: None,
skills: None,
inside_building: None,
}
}
#[tokio::test]
async fn server_welcome_roundtrips() {
assert_server_roundtrip(ServerMessage::Welcome(Welcome {
session_id: 1,
entity_id: 1,
snapshot: empty_snapshot(),
}))
.await;
}
#[tokio::test]
async fn server_content_updated_roundtrips() {
assert_server_roundtrip(ServerMessage::ContentUpdated(empty_snapshot())).await;
}
#[tokio::test]
async fn server_tick_roundtrips() {
assert_server_roundtrip(ServerMessage::Tick(empty_tick_delta())).await;
}
#[tokio::test]
async fn server_intent_ack_roundtrips() {
assert_server_roundtrip(ServerMessage::IntentAck {
entity_id: 1,
seq: 1,
tick: 1,
})
.await;
}
#[tokio::test]
async fn server_chat_roundtrips() {
assert_server_roundtrip(ServerMessage::Chat(ChatMessage {
channel: ChatChannel::Nearby,
from_entity: 1,
from_name: "traveler".into(),
text: "hi".into(),
tick: 1,
}))
.await;
}
#[tokio::test]
async fn server_harvest_result_with_none_instance_id_roundtrips() {
assert_server_roundtrip(ServerMessage::HarvestResult(HarvestResult {
node_id: "crop-carrot-1".into(),
quantity: 1,
item_template: "carrot".into(),
item_instance_id: None,
}))
.await;
}
#[tokio::test]
async fn server_harvest_result_with_some_instance_id_roundtrips() {
assert_server_roundtrip(ServerMessage::HarvestResult(HarvestResult {
node_id: "crop-carrot-1".into(),
quantity: 1,
item_template: "carrot".into(),
item_instance_id: Some(Uuid::nil()),
}))
.await;
}
#[tokio::test]
async fn server_use_result_roundtrips() {
assert_server_roundtrip(ServerMessage::UseResult(UseResult {
template_id: "waterskin".into(),
hunger_restored: 0.0,
thirst_restored: 10.0,
}))
.await;
}
#[tokio::test]
async fn server_craft_result_roundtrips_with_empty_vecs() {
assert_server_roundtrip(ServerMessage::CraftResult(CraftResult {
blueprint_id: "wooden_stool".into(),
outputs: Vec::new(),
consumed: Vec::new(),
batch_index: 1,
batch_total: 1,
}))
.await;
}
#[tokio::test]
async fn server_craft_result_roundtrips_with_populated_vecs() {
assert_server_roundtrip(ServerMessage::CraftResult(CraftResult {
blueprint_id: "wooden_stool".into(),
outputs: vec![ItemStack::simple("wooden_stool", 1)],
consumed: vec![ItemStack::simple("lumber", 2)],
batch_index: 1,
batch_total: 1,
}))
.await;
}
#[tokio::test]
async fn server_craft_result_vegetable_soup_roundtrips() {
assert_server_roundtrip(ServerMessage::CraftResult(CraftResult {
blueprint_id: "vegetable_soup".into(),
outputs: vec![ItemStack::simple("vegetable_soup", 1)],
consumed: vec![
ItemStack::simple("carrot", 1),
ItemStack::simple("potato", 1),
ItemStack::simple("bottle_of_water", 1),
],
batch_index: 1,
batch_total: 1,
}))
.await;
}
#[tokio::test]
async fn craft_result_followed_by_interior_tick_roundtrips() {
use flatland_protocol::types::{DoorView, NpcView};
let craft = ServerMessage::CraftResult(CraftResult {
blueprint_id: "vegetable_soup".into(),
outputs: vec![ItemStack::simple("vegetable_soup", 1)],
consumed: vec![
ItemStack::simple("carrot", 1),
ItemStack::simple("potato", 1),
ItemStack::simple("bottle_of_water", 1),
],
batch_index: 1,
batch_total: 1,
});
let mut delta = empty_tick_delta();
delta.doors.push(DoorView {
id: "cookhouse_exit".into(),
building_id: "cookhouse".into(),
x: 524.0,
y: 520.0,
open: false,
is_exit: true,
});
delta.npcs.push(NpcView {
id: "maris_cook".into(),
label: "Maris".into(),
role: "cook".into(),
x: 526.0,
y: 523.0,
building_id: Some("cookhouse".into()),
entity_id: None,
life_state: None,
hp_pct: None,
});
delta.inventory = vec![
ItemStack::simple("vegetable_soup", 1),
ItemStack::simple("cooking_pot", 1),
];
let tick = ServerMessage::Tick(delta);
let mut buf = Vec::new();
write_server_message(&mut buf, &craft).await.unwrap();
write_server_message(&mut buf, &tick).await.unwrap();
let mut cursor = buf.as_slice();
let decoded_craft = read_server_message(&mut cursor).await.unwrap();
assert!(matches!(decoded_craft, ServerMessage::CraftResult(_)));
let decoded_tick = read_server_message(&mut cursor).await.unwrap();
assert!(matches!(decoded_tick, ServerMessage::Tick(_)));
}
#[tokio::test]
async fn server_death_roundtrips() {
assert_server_roundtrip(ServerMessage::Death(DeathNotice {
entity_id: 1,
respawn_x: 128.0,
respawn_y: 128.0,
message: "you died".into(),
}))
.await;
}
#[tokio::test]
async fn server_interaction_roundtrips_with_empty_inventory_delta() {
assert_server_roundtrip(ServerMessage::Interaction(InteractionNotice {
target_id: "door-1".into(),
message: "opened".into(),
coins_delta: 0,
inventory_delta: Vec::new(),
}))
.await;
}
#[tokio::test]
async fn server_interaction_roundtrips_with_populated_inventory_delta() {
assert_server_roundtrip(ServerMessage::Interaction(InteractionNotice {
target_id: "ada-broker".into(),
message: "sold".into(),
coins_delta: -5,
inventory_delta: vec![ItemStack::simple("oak_log", 3)],
}))
.await;
}
#[tokio::test]
async fn tick_delta_with_npc_view_none_building_id_roundtrips() {
let mut delta = empty_tick_delta();
delta.npcs.push(NpcView {
id: "eli_farmer".into(),
label: "Eli".into(),
role: "farmer".into(),
x: 10.0,
y: 10.0,
building_id: None,
entity_id: None,
life_state: None,
hp_pct: None,
});
assert_server_roundtrip(ServerMessage::Tick(delta)).await;
}
#[tokio::test]
async fn tick_delta_with_npc_view_some_building_id_roundtrips() {
let mut delta = empty_tick_delta();
delta.npcs.push(NpcView {
id: "ada_broker".into(),
label: "Ada".into(),
role: "broker".into(),
x: 148.0,
y: 118.0,
building_id: Some("broker-hut".into()),
entity_id: None,
life_state: None,
hp_pct: None,
});
assert_server_roundtrip(ServerMessage::Tick(delta)).await;
}
#[tokio::test]
async fn tick_delta_with_wildlife_npc_view_roundtrips() {
use flatland_protocol::LifeState;
let mut delta = empty_tick_delta();
delta.npcs.push(NpcView {
id: "north-meadow-rabbits-0".into(),
label: "Rabbit".into(),
role: "critter".into(),
x: 95.0,
y: 195.0,
building_id: None,
entity_id: Some(42),
life_state: Some(LifeState::Alive),
hp_pct: Some(1.0),
});
assert_server_roundtrip(ServerMessage::Tick(delta)).await;
}
#[tokio::test]
async fn tick_delta_with_bare_entity_state_roundtrips() {
let mut delta = empty_tick_delta();
delta.entities.push(bare_entity_state());
assert_server_roundtrip(ServerMessage::Tick(delta)).await;
}
#[tokio::test]
async fn client_hello_dev_local_roundtrips() {
assert_client_roundtrip(ClientMessage::Hello(Hello {
client_name: "traveler".into(),
protocol_version: PROTOCOL_VERSION,
auth: AuthCredential::DevLocal,
character_id: None,
}))
.await;
}
#[tokio::test]
async fn client_disconnect_roundtrips() {
assert_client_roundtrip(ClientMessage::Disconnect).await;
}
#[tokio::test]
async fn client_intent_move_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::Move {
entity_id: 1,
forward: 1.0,
strafe: 0.0,
vertical: 0.0,
sprint: false,
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_intent_stop_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::Stop {
entity_id: 1,
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_intent_harvest_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::Harvest {
entity_id: 1,
node_id: "crop-carrot-1".into(),
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_intent_use_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::Use {
entity_id: 1,
template_id: "waterskin".into(),
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_intent_say_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::Say {
entity_id: 1,
channel: ChatChannel::WhisperStone,
text: "hello".into(),
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_intent_craft_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::Craft {
entity_id: 1,
blueprint_id: "wooden_stool".into(),
count: Some(5),
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_intent_interact_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::Interact {
entity_id: 1,
target_id: "door-1".into(),
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_intent_test_damage_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::TestDamage {
entity_id: 1,
amount: 25.0,
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_intent_set_target_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::SetTarget {
entity_id: 1,
target_id: 42,
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_intent_clear_target_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::ClearTarget {
entity_id: 1,
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_intent_attack_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::Attack {
entity_id: 1,
target_id: Some(42),
weapon_slot: None,
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_intent_pickup_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::Pickup {
entity_id: 1,
drop_id: Some("drop-1-rabbit_pelt".into()),
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_intent_cast_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::Cast {
entity_id: 1,
ability_id: "fireball".into(),
target_id: 42,
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_intent_bind_action_slot_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::BindActionSlot {
entity_id: 1,
slot_index: 2,
ability_id: "fireball".into(),
auto_enabled: false,
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_intent_use_action_slot_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::UseActionSlot {
entity_id: 1,
slot_index: 2,
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_intent_dodge_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::Dodge {
entity_id: 1,
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_intent_lunge_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::Lunge {
entity_id: 1,
forward: -1.0,
strafe: 0.5,
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_intent_block_roundtrips() {
assert_client_roundtrip(ClientMessage::Intent(Intent::Block {
entity_id: 1,
enabled: true,
seq: 1,
}))
.await;
}
#[tokio::test]
async fn client_hello_api_token_roundtrips() {
assert_client_roundtrip(ClientMessage::Hello(Hello {
client_name: "bot-000".into(),
protocol_version: PROTOCOL_VERSION,
auth: AuthCredential::ApiToken {
token: "flat_tok_test".into(),
character_id: Uuid::nil(),
},
character_id: Some(Uuid::nil()),
}))
.await;
}