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>(
51 codec: &impl Codec,
52 payload: T,
53) -> Result<Vec<u8>, CodecError> {
54 codec.encode(&Envelope::new(payload))
55}
56
57pub fn decode_envelope<T: DeserializeOwned>(
58 codec: &impl Codec,
59 bytes: &[u8],
60) -> Result<Envelope<T>, CodecError> {
61 codec.decode(bytes)
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use crate::types::{
68 BlueprintIngredientView, BlueprintView, EntityState, TickDelta, ToolRequirementView,
69 Transform, WorldClock, WorldCoord,
70 };
71
72 fn sample_tick_delta() -> TickDelta {
73 TickDelta {
74 tick: 42,
75 entities: (0..15)
76 .map(|i| EntityState {
77 id: i + 1,
78 label: String::new(),
79 transform: Transform {
80 position: WorldCoord::surface(i as f32, i as f32 * 0.5),
81 yaw: 1.25,
82 velocity: crate::types::Velocity2D { vx: 0.5, vy: -0.2 },
83 },
84 vitals: None,
85 attributes: None,
86 skills: None,
87 inside_building: None,
88 tile_id: None,
89 paperdoll_ref: None,
90 presentation_state: None,
91 sprite_mode: None,
92 progression_xp: None,
93 combat_cues: Vec::new(),
94 })
95 .collect(),
96 resource_nodes: Vec::new(),
97 buildings: Vec::new(),
98 doors: Vec::new(),
99 npcs: Vec::new(),
100 inventory: Vec::new(),
101 blueprints: Vec::new(),
102 world_clock: WorldClock::default(),
103 ground_drops: Vec::new(),
104 placed_containers: Vec::new(),
105 combat: None,
106 interior_map: None,
107 quest_log: vec![],
108 hired_workers: Vec::new(),
109 interactables: vec![],
110 ledger: None,
111 career: None,
112 combat_fx: Vec::new(),
113 property_plots: Vec::new(),
114 terrain_overlays: Vec::new(),
115 }
116 }
117
118 #[test]
119 fn postcard_blueprint_snapshot_roundtrip() {
120 let codec = PostcardCodec;
121 let delta = TickDelta {
122 tick: 1,
123 entities: vec![],
124 resource_nodes: vec![],
125 buildings: vec![],
126 doors: vec![],
127 npcs: vec![],
128 inventory: vec![],
129 blueprints: vec![BlueprintView {
130 id: "oak_to_lumber".into(),
131 label: "Saw Planks".into(),
132 output: "lumber".into(),
133 output_qty: 1,
134 craft_ticks: 60,
135 station: Some("woodworking".into()),
136 category: Some("woodworking".into()),
137 required_tools: vec![ToolRequirementView {
138 item: "handsaw".into(),
139 consumed: false,
140 }],
141 skill: None,
142 failure_chance: 0.0,
143 worker_train_copper: 0,
144 inputs: vec![BlueprintIngredientView {
145 template_id: "oak_log".into(),
146 quantity: 2,
147 consumed: true,
148 }],
149 }],
150 world_clock: WorldClock::default(),
151 ground_drops: vec![],
152 placed_containers: vec![],
153 combat: None,
154 interior_map: None,
155 quest_log: vec![],
156 hired_workers: Vec::new(),
157 interactables: vec![],
158 ledger: None,
159 career: None,
160 combat_fx: Vec::new(),
161 property_plots: Vec::new(),
162 terrain_overlays: Vec::new(),
163 };
164 let bytes = codec.encode(&Envelope::new(delta.clone())).unwrap();
165 let decoded: Envelope<TickDelta> = codec.decode(&bytes).unwrap();
166 assert_eq!(decoded.payload, delta);
167 }
168
169 #[test]
170 fn postcard_roundtrip_tick_delta() {
171 let codec = PostcardCodec;
172 let original = Envelope::new(sample_tick_delta());
173 let bytes = codec.encode(&original).expect("encode");
174 let decoded: Envelope<TickDelta> = codec.decode(&bytes).expect("decode");
175 assert_eq!(original, decoded);
176 }
177
178 #[test]
179 fn bincode_roundtrip_tick_delta() {
180 let codec = BincodeCodec;
181 let original = Envelope::new(sample_tick_delta());
182 let bytes = codec.encode(&original).expect("encode");
183 let decoded: Envelope<TickDelta> = codec.decode(&bytes).expect("decode");
184 assert_eq!(original, decoded);
185 }
186
187 #[test]
188 fn postcard_many_entities_vitals_none() {
189 let delta = TickDelta {
190 tick: 42,
191 entities: (0..15)
192 .map(|i| EntityState {
193 id: i + 1,
194 label: String::new(),
195 transform: Transform {
196 position: WorldCoord::surface(i as f32, i as f32 * 0.5),
197 yaw: 1.25,
198 velocity: crate::types::Velocity2D { vx: 0.5, vy: -0.2 },
199 },
200 vitals: None,
201 attributes: None,
202 skills: None,
203 inside_building: None,
204 tile_id: None,
205 paperdoll_ref: None,
206 presentation_state: None,
207 sprite_mode: None,
208 progression_xp: None,
209 combat_cues: Vec::new(),
210 })
211 .collect(),
212 resource_nodes: vec![],
213 buildings: vec![],
214 doors: vec![],
215 npcs: vec![],
216 inventory: vec![],
217 blueprints: vec![],
218 world_clock: WorldClock::default(),
219 ground_drops: vec![],
220 placed_containers: vec![],
221 combat: None,
222 interior_map: None,
223 quest_log: vec![],
224 hired_workers: Vec::new(),
225 interactables: vec![],
226 ledger: None,
227 career: None,
228 combat_fx: Vec::new(),
229 property_plots: Vec::new(),
230 terrain_overlays: Vec::new(),
231 };
232 let codec = PostcardCodec;
233 let bytes = codec.encode(&Envelope::new(delta.clone())).unwrap();
234 let decoded: Envelope<TickDelta> = codec.decode(&bytes).unwrap();
235 assert_eq!(decoded.payload, delta);
236 }
237}