1use serde::{de::DeserializeOwned, Serialize};
2
3use crate::Envelope;
4
5#[derive(Debug, thiserror::Error)]
6pub enum CodecError {
7 #[error("postcard encode failed: {0}")]
8 PostcardEncode(postcard::Error),
9 #[error("postcard decode failed: {0}")]
10 PostcardDecode(postcard::Error),
11 #[error("bincode encode failed: {0}")]
12 BincodeEncode(bincode::Error),
13 #[error("bincode decode failed: {0}")]
14 BincodeDecode(bincode::Error),
15}
16
17pub trait Codec {
18 fn encode<T: Serialize>(&self, value: &T) -> Result<Vec<u8>, CodecError>;
19 fn decode<T: DeserializeOwned>(&self, bytes: &[u8]) -> Result<T, CodecError>;
20}
21
22#[derive(Debug, Clone, Copy, Default)]
24pub struct PostcardCodec;
25
26impl Codec for PostcardCodec {
27 fn encode<T: Serialize>(&self, value: &T) -> Result<Vec<u8>, CodecError> {
28 postcard::to_allocvec(value).map_err(CodecError::PostcardEncode)
29 }
30
31 fn decode<T: DeserializeOwned>(&self, bytes: &[u8]) -> Result<T, CodecError> {
32 postcard::from_bytes(bytes).map_err(CodecError::PostcardDecode)
33 }
34}
35
36#[derive(Debug, Clone, Copy, Default)]
38pub struct BincodeCodec;
39
40impl Codec for BincodeCodec {
41 fn encode<T: Serialize>(&self, value: &T) -> Result<Vec<u8>, CodecError> {
42 bincode::serialize(value).map_err(CodecError::BincodeEncode)
43 }
44
45 fn decode<T: DeserializeOwned>(&self, bytes: &[u8]) -> Result<T, CodecError> {
46 bincode::deserialize(bytes).map_err(CodecError::BincodeDecode)
47 }
48}
49
50pub fn encode_envelope<T: Serialize>(codec: &impl Codec, payload: T) -> Result<Vec<u8>, CodecError> {
51 codec.encode(&Envelope::new(payload))
52}
53
54pub fn decode_envelope<T: DeserializeOwned>(
55 codec: &impl Codec,
56 bytes: &[u8],
57) -> Result<Envelope<T>, CodecError> {
58 codec.decode(bytes)
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use crate::types::{
65 BlueprintIngredientView, BlueprintView, EntityState, TickDelta, ToolRequirementView,
66 Transform, WorldClock, WorldCoord,
67 };
68
69 fn sample_tick_delta() -> TickDelta {
70 TickDelta {
71 tick: 42,
72 entities: (0..15)
73 .map(|i| EntityState {
74 id: i + 1,
75 label: String::new(),
76 transform: Transform {
77 position: WorldCoord::surface(i as f32, i as f32 * 0.5),
78 yaw: 1.25,
79 velocity: crate::types::Velocity2D { vx: 0.5, vy: -0.2 },
80 },
81 vitals: None,
82 attributes: None,
83 skills: None,
84 inside_building: None,
85 })
86 .collect(),
87 resource_nodes: Vec::new(),
88 buildings: Vec::new(),
89 doors: Vec::new(),
90 npcs: Vec::new(),
91 inventory: Vec::new(),
92 blueprints: Vec::new(),
93 world_clock: WorldClock::default(),
94 ground_drops: Vec::new(),
95 placed_containers: Vec::new(),
96 combat: None,
97 }
98 }
99
100 #[test]
101 fn postcard_blueprint_snapshot_roundtrip() {
102 let codec = PostcardCodec;
103 let delta = TickDelta {
104 tick: 1,
105 entities: vec![],
106 resource_nodes: vec![],
107 buildings: vec![],
108 doors: vec![],
109 npcs: vec![],
110 inventory: vec![],
111 blueprints: vec![BlueprintView {
112 id: "oak_to_lumber".into(),
113 label: "Saw Planks".into(),
114 output: "lumber".into(),
115 output_qty: 1,
116 craft_ticks: 60,
117 station: Some("woodworking".into()),
118 category: Some("woodworking".into()),
119 required_tools: vec![ToolRequirementView {
120 item: "handsaw".into(),
121 consumed: false,
122 }],
123 skill: None,
124 failure_chance: 0.0,
125 inputs: vec![BlueprintIngredientView {
126 template_id: "oak_log".into(),
127 quantity: 2,
128 consumed: true,
129 }],
130 }],
131 world_clock: WorldClock::default(),
132 ground_drops: vec![],
133 placed_containers: vec![],
134 combat: None,
135 };
136 let bytes = codec.encode(&Envelope::new(delta.clone())).unwrap();
137 let decoded: Envelope<TickDelta> = codec.decode(&bytes).unwrap();
138 assert_eq!(decoded.payload, delta);
139 }
140
141 #[test]
142 fn postcard_roundtrip_tick_delta() {
143 let codec = PostcardCodec;
144 let original = Envelope::new(sample_tick_delta());
145 let bytes = codec.encode(&original).expect("encode");
146 let decoded: Envelope<TickDelta> = codec.decode(&bytes).expect("decode");
147 assert_eq!(original, decoded);
148 }
149
150 #[test]
151 fn bincode_roundtrip_tick_delta() {
152 let codec = BincodeCodec;
153 let original = Envelope::new(sample_tick_delta());
154 let bytes = codec.encode(&original).expect("encode");
155 let decoded: Envelope<TickDelta> = codec.decode(&bytes).expect("decode");
156 assert_eq!(original, decoded);
157 }
158
159 #[test]
160 fn postcard_many_entities_vitals_none() {
161 let delta = TickDelta {
162 tick: 42,
163 entities: (0..15)
164 .map(|i| EntityState {
165 id: i + 1,
166 label: String::new(),
167 transform: Transform {
168 position: WorldCoord::surface(i as f32, i as f32 * 0.5),
169 yaw: 1.25,
170 velocity: crate::types::Velocity2D { vx: 0.5, vy: -0.2 },
171 },
172 vitals: None,
173 attributes: None,
174 skills: None,
175 inside_building: None,
176 })
177 .collect(),
178 resource_nodes: vec![],
179 buildings: vec![],
180 doors: vec![],
181 npcs: vec![],
182 inventory: vec![],
183 blueprints: vec![],
184 world_clock: WorldClock::default(),
185 ground_drops: vec![],
186 placed_containers: vec![],
187 combat: None,
188 };
189 let codec = PostcardCodec;
190 let bytes = codec.encode(&Envelope::new(delta.clone())).unwrap();
191 let decoded: Envelope<TickDelta> = codec.decode(&bytes).unwrap();
192 assert_eq!(decoded.payload, delta);
193 }
194}