use serde::{de::DeserializeOwned, Serialize};
use crate::Envelope;
#[derive(Debug, thiserror::Error)]
pub enum CodecError {
#[error("postcard encode failed: {0}")]
PostcardEncode(postcard::Error),
#[error("postcard decode failed: {0}")]
PostcardDecode(postcard::Error),
#[error("bincode encode failed: {0}")]
BincodeEncode(bincode::Error),
#[error("bincode decode failed: {0}")]
BincodeDecode(bincode::Error),
}
pub trait Codec {
fn encode<T: Serialize>(&self, value: &T) -> Result<Vec<u8>, CodecError>;
fn decode<T: DeserializeOwned>(&self, bytes: &[u8]) -> Result<T, CodecError>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PostcardCodec;
impl Codec for PostcardCodec {
fn encode<T: Serialize>(&self, value: &T) -> Result<Vec<u8>, CodecError> {
postcard::to_allocvec(value).map_err(CodecError::PostcardEncode)
}
fn decode<T: DeserializeOwned>(&self, bytes: &[u8]) -> Result<T, CodecError> {
postcard::from_bytes(bytes).map_err(CodecError::PostcardDecode)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct BincodeCodec;
impl Codec for BincodeCodec {
fn encode<T: Serialize>(&self, value: &T) -> Result<Vec<u8>, CodecError> {
bincode::serialize(value).map_err(CodecError::BincodeEncode)
}
fn decode<T: DeserializeOwned>(&self, bytes: &[u8]) -> Result<T, CodecError> {
bincode::deserialize(bytes).map_err(CodecError::BincodeDecode)
}
}
pub fn encode_envelope<T: Serialize>(codec: &impl Codec, payload: T) -> Result<Vec<u8>, CodecError> {
codec.encode(&Envelope::new(payload))
}
pub fn decode_envelope<T: DeserializeOwned>(
codec: &impl Codec,
bytes: &[u8],
) -> Result<Envelope<T>, CodecError> {
codec.decode(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{
BlueprintIngredientView, BlueprintView, EntityState, TickDelta, ToolRequirementView,
Transform, WorldClock, WorldCoord,
};
fn sample_tick_delta() -> TickDelta {
TickDelta {
tick: 42,
entities: (0..15)
.map(|i| EntityState {
id: i + 1,
label: String::new(),
transform: Transform {
position: WorldCoord::surface(i as f32, i as f32 * 0.5),
yaw: 1.25,
velocity: crate::types::Velocity2D { vx: 0.5, vy: -0.2 },
},
vitals: None,
attributes: None,
skills: None,
inside_building: None,
})
.collect(),
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,
}
}
#[test]
fn postcard_blueprint_snapshot_roundtrip() {
let codec = PostcardCodec;
let delta = TickDelta {
tick: 1,
entities: vec![],
resource_nodes: vec![],
buildings: vec![],
doors: vec![],
npcs: vec![],
inventory: vec![],
blueprints: vec![BlueprintView {
id: "oak_to_lumber".into(),
label: "Saw Planks".into(),
output: "lumber".into(),
output_qty: 1,
craft_ticks: 60,
station: Some("woodworking".into()),
category: Some("woodworking".into()),
required_tools: vec![ToolRequirementView {
item: "handsaw".into(),
consumed: false,
}],
skill: None,
failure_chance: 0.0,
inputs: vec![BlueprintIngredientView {
template_id: "oak_log".into(),
quantity: 2,
consumed: true,
}],
}],
world_clock: WorldClock::default(),
ground_drops: vec![],
placed_containers: vec![],
combat: None,
};
let bytes = codec.encode(&Envelope::new(delta.clone())).unwrap();
let decoded: Envelope<TickDelta> = codec.decode(&bytes).unwrap();
assert_eq!(decoded.payload, delta);
}
#[test]
fn postcard_roundtrip_tick_delta() {
let codec = PostcardCodec;
let original = Envelope::new(sample_tick_delta());
let bytes = codec.encode(&original).expect("encode");
let decoded: Envelope<TickDelta> = codec.decode(&bytes).expect("decode");
assert_eq!(original, decoded);
}
#[test]
fn bincode_roundtrip_tick_delta() {
let codec = BincodeCodec;
let original = Envelope::new(sample_tick_delta());
let bytes = codec.encode(&original).expect("encode");
let decoded: Envelope<TickDelta> = codec.decode(&bytes).expect("decode");
assert_eq!(original, decoded);
}
#[test]
fn postcard_many_entities_vitals_none() {
let delta = TickDelta {
tick: 42,
entities: (0..15)
.map(|i| EntityState {
id: i + 1,
label: String::new(),
transform: Transform {
position: WorldCoord::surface(i as f32, i as f32 * 0.5),
yaw: 1.25,
velocity: crate::types::Velocity2D { vx: 0.5, vy: -0.2 },
},
vitals: None,
attributes: None,
skills: None,
inside_building: None,
})
.collect(),
resource_nodes: vec![],
buildings: vec![],
doors: vec![],
npcs: vec![],
inventory: vec![],
blueprints: vec![],
world_clock: WorldClock::default(),
ground_drops: vec![],
placed_containers: vec![],
combat: None,
};
let codec = PostcardCodec;
let bytes = codec.encode(&Envelope::new(delta.clone())).unwrap();
let decoded: Envelope<TickDelta> = codec.decode(&bytes).unwrap();
assert_eq!(decoded.payload, delta);
}
}