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 tile_id: None,
86 presentation_state: None,
87 sprite_mode: None,
88 })
89 .collect(),
90 resource_nodes: Vec::new(),
91 buildings: Vec::new(),
92 doors: Vec::new(),
93 npcs: Vec::new(),
94 inventory: Vec::new(),
95 blueprints: Vec::new(),
96 world_clock: WorldClock::default(),
97 ground_drops: Vec::new(),
98 placed_containers: Vec::new(),
99 combat: None,
100 interior_map: None,
101 quest_log: vec![],
102 interactables: vec![],
103 }
104 }
105
106 #[test]
107 fn postcard_blueprint_snapshot_roundtrip() {
108 let codec = PostcardCodec;
109 let delta = TickDelta {
110 tick: 1,
111 entities: vec![],
112 resource_nodes: vec![],
113 buildings: vec![],
114 doors: vec![],
115 npcs: vec![],
116 inventory: vec![],
117 blueprints: vec![BlueprintView {
118 id: "oak_to_lumber".into(),
119 label: "Saw Planks".into(),
120 output: "lumber".into(),
121 output_qty: 1,
122 craft_ticks: 60,
123 station: Some("woodworking".into()),
124 category: Some("woodworking".into()),
125 required_tools: vec![ToolRequirementView {
126 item: "handsaw".into(),
127 consumed: false,
128 }],
129 skill: None,
130 failure_chance: 0.0,
131 inputs: vec![BlueprintIngredientView {
132 template_id: "oak_log".into(),
133 quantity: 2,
134 consumed: true,
135 }],
136 }],
137 world_clock: WorldClock::default(),
138 ground_drops: vec![],
139 placed_containers: vec![],
140 combat: None,
141 interior_map: None,
142 quest_log: vec![],
143 interactables: vec![],
144 };
145 let bytes = codec.encode(&Envelope::new(delta.clone())).unwrap();
146 let decoded: Envelope<TickDelta> = codec.decode(&bytes).unwrap();
147 assert_eq!(decoded.payload, delta);
148 }
149
150 #[test]
151 fn postcard_roundtrip_tick_delta() {
152 let codec = PostcardCodec;
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 bincode_roundtrip_tick_delta() {
161 let codec = BincodeCodec;
162 let original = Envelope::new(sample_tick_delta());
163 let bytes = codec.encode(&original).expect("encode");
164 let decoded: Envelope<TickDelta> = codec.decode(&bytes).expect("decode");
165 assert_eq!(original, decoded);
166 }
167
168 #[test]
169 fn postcard_many_entities_vitals_none() {
170 let delta = TickDelta {
171 tick: 42,
172 entities: (0..15)
173 .map(|i| EntityState {
174 id: i + 1,
175 label: String::new(),
176 transform: Transform {
177 position: WorldCoord::surface(i as f32, i as f32 * 0.5),
178 yaw: 1.25,
179 velocity: crate::types::Velocity2D { vx: 0.5, vy: -0.2 },
180 },
181 vitals: None,
182 attributes: None,
183 skills: None,
184 inside_building: None,
185 tile_id: None,
186 presentation_state: None,
187 sprite_mode: None,
188 })
189 .collect(),
190 resource_nodes: vec![],
191 buildings: vec![],
192 doors: vec![],
193 npcs: vec![],
194 inventory: vec![],
195 blueprints: vec![],
196 world_clock: WorldClock::default(),
197 ground_drops: vec![],
198 placed_containers: vec![],
199 combat: None,
200 interior_map: None,
201 quest_log: vec![],
202 interactables: vec![],
203 };
204 let codec = PostcardCodec;
205 let bytes = codec.encode(&Envelope::new(delta.clone())).unwrap();
206 let decoded: Envelope<TickDelta> = codec.decode(&bytes).unwrap();
207 assert_eq!(decoded.payload, delta);
208 }
209}