1use std::io::{self, Read, Seek};
2
3use crate::reader::BigEndianReader;
4
5pub const OBJ_TYPE_ITEM: i32 = 0;
7pub const OBJ_TYPE_CRITTER: i32 = 1;
8pub const OBJ_TYPE_MISC: i32 = 5;
9
10pub fn obj_type_from_pid(pid: i32) -> i32 {
11 (pid >> 24) & 0x0F
12}
13
14#[derive(Debug)]
15pub struct GameObject {
16 pub id: i32,
17 pub tile: i32,
18 pub x: i32,
19 pub y: i32,
20 pub sx: i32,
21 pub sy: i32,
22 pub frame: i32,
23 pub rotation: i32,
24 pub fid: i32,
25 pub flags: i32,
26 pub elevation: i32,
27 pub pid: i32,
28 pub cid: i32,
29 pub light_distance: i32,
30 pub light_intensity: i32,
31 pub outline: i32,
32 pub sid: i32,
33 pub script_index: i32,
34 pub inventory_length: i32,
35 pub inventory_capacity: i32,
36 pub object_data: ObjectData,
37 pub inventory: Vec<InventoryItem>,
38}
39
40#[derive(Debug)]
41pub enum ObjectData {
42 Critter(CritterObjectData),
43 Item(ItemObjectData),
44 Scenery(SceneryObjectData),
45 Misc(MiscObjectData),
46 Other,
47}
48
49#[derive(Debug)]
50pub struct CritterObjectData {
51 pub field_0: i32,
52 pub damage_last_turn: i32,
53 pub maneuver: i32,
54 pub ap: i32,
55 pub results: i32,
56 pub ai_packet: i32,
57 pub team: i32,
58 pub who_hit_me_cid: i32,
59 pub hp: i32,
60 pub radiation: i32,
61 pub poison: i32,
62}
63
64#[derive(Debug)]
65pub struct ItemObjectData {
66 pub flags: i32,
67 pub extra_bytes: u8, pub extra_data: Vec<u8>,
69}
70
71#[derive(Debug)]
72pub struct SceneryObjectData {
73 pub flags: i32,
74}
75
76#[derive(Debug)]
77pub struct MiscObjectData {
78 pub map: i32,
79 pub tile: i32,
80 pub elevation: i32,
81 pub rotation: i32,
82}
83
84#[derive(Debug)]
85pub struct InventoryItem {
86 pub quantity: i32,
87 pub object: GameObject,
88}
89
90impl GameObject {
91 pub fn parse<R: Read + Seek>(r: &mut BigEndianReader<R>) -> io::Result<Self> {
92 let id = r.read_i32()?;
94 let tile = r.read_i32()?;
95 let x = r.read_i32()?;
96 let y = r.read_i32()?;
97 let sx = r.read_i32()?;
98 let sy = r.read_i32()?;
99 let frame = r.read_i32()?;
100 let rotation = r.read_i32()?;
101 let fid = r.read_i32()?;
102 let flags = r.read_i32()?;
103 let elevation = r.read_i32()?;
104 let pid = r.read_i32()?;
105 let cid = r.read_i32()?;
106 let light_distance = r.read_i32()?;
107 let light_intensity = r.read_i32()?;
108 let outline = r.read_i32()?;
109 let sid = r.read_i32()?;
110 let script_index = r.read_i32()?;
111
112 let inventory_length = r.read_i32()?;
114 let inventory_capacity = r.read_i32()?;
115 let _placeholder = r.read_i32()?;
116
117 if !(-1..=1000).contains(&inventory_length) {
118 return Err(io::Error::new(
119 io::ErrorKind::InvalidData,
120 format!(
121 "invalid inventory length {} for object pid=0x{:08x} at pos={}",
122 inventory_length,
123 pid,
124 r.position().unwrap_or(0)
125 ),
126 ));
127 }
128
129 let obj_type = obj_type_from_pid(pid);
131 let object_data = match obj_type {
132 OBJ_TYPE_CRITTER => ObjectData::Critter(parse_critter_object_data(r)?),
133 OBJ_TYPE_ITEM => ObjectData::Item(parse_item_object_data(r)?),
134 OBJ_TYPE_MISC => {
135 if (0x500_0010..=0x500_0017).contains(&pid) {
137 ObjectData::Misc(parse_misc_object_data(r)?)
138 } else {
139 ObjectData::Other
140 }
141 }
142 _ => {
143 let scenery_flags = r.read_i32()?;
147 ObjectData::Scenery(SceneryObjectData {
148 flags: scenery_flags,
149 })
150 }
151 };
152
153 let normalized_inventory_length = inventory_length.max(0);
155 let mut inventory = Vec::with_capacity(normalized_inventory_length as usize);
156 for _ in 0..normalized_inventory_length {
157 let quantity = r.read_i32()?;
158 let object = GameObject::parse(r)?;
159 inventory.push(InventoryItem { quantity, object });
160 }
161
162 Ok(Self {
163 id,
164 tile,
165 x,
166 y,
167 sx,
168 sy,
169 frame,
170 rotation,
171 fid,
172 flags,
173 elevation,
174 pid,
175 cid,
176 light_distance,
177 light_intensity,
178 outline,
179 sid,
180 script_index,
181 inventory_length,
182 inventory_capacity,
183 object_data,
184 inventory,
185 })
186 }
187
188 pub fn emit_to_vec(&self, out: &mut Vec<u8>) -> io::Result<()> {
189 out.extend_from_slice(&self.id.to_be_bytes());
190 out.extend_from_slice(&self.tile.to_be_bytes());
191 out.extend_from_slice(&self.x.to_be_bytes());
192 out.extend_from_slice(&self.y.to_be_bytes());
193 out.extend_from_slice(&self.sx.to_be_bytes());
194 out.extend_from_slice(&self.sy.to_be_bytes());
195 out.extend_from_slice(&self.frame.to_be_bytes());
196 out.extend_from_slice(&self.rotation.to_be_bytes());
197 out.extend_from_slice(&self.fid.to_be_bytes());
198 out.extend_from_slice(&self.flags.to_be_bytes());
199 out.extend_from_slice(&self.elevation.to_be_bytes());
200 out.extend_from_slice(&self.pid.to_be_bytes());
201 out.extend_from_slice(&self.cid.to_be_bytes());
202 out.extend_from_slice(&self.light_distance.to_be_bytes());
203 out.extend_from_slice(&self.light_intensity.to_be_bytes());
204 out.extend_from_slice(&self.outline.to_be_bytes());
205 out.extend_from_slice(&self.sid.to_be_bytes());
206 out.extend_from_slice(&self.script_index.to_be_bytes());
207
208 let inventory_length = if self.inventory_length < 0 && self.inventory.is_empty() {
209 -1
210 } else {
211 self.inventory.len() as i32
212 };
213 let inventory_capacity = self.inventory_capacity.max(inventory_length.max(0));
214 out.extend_from_slice(&inventory_length.to_be_bytes());
215 out.extend_from_slice(&inventory_capacity.to_be_bytes());
216 out.extend_from_slice(&0i32.to_be_bytes());
217
218 match &self.object_data {
219 ObjectData::Critter(data) => {
220 out.extend_from_slice(&data.field_0.to_be_bytes());
221 out.extend_from_slice(&data.damage_last_turn.to_be_bytes());
222 out.extend_from_slice(&data.maneuver.to_be_bytes());
223 out.extend_from_slice(&data.ap.to_be_bytes());
224 out.extend_from_slice(&data.results.to_be_bytes());
225 out.extend_from_slice(&data.ai_packet.to_be_bytes());
226 out.extend_from_slice(&data.team.to_be_bytes());
227 out.extend_from_slice(&data.who_hit_me_cid.to_be_bytes());
228 out.extend_from_slice(&data.hp.to_be_bytes());
229 out.extend_from_slice(&data.radiation.to_be_bytes());
230 out.extend_from_slice(&data.poison.to_be_bytes());
231 }
232 ObjectData::Item(data) => {
233 if data.extra_data.len() != data.extra_bytes as usize {
234 return Err(io::Error::new(
235 io::ErrorKind::InvalidData,
236 format!(
237 "item extra data length mismatch: extra_bytes={}, extra_data={}",
238 data.extra_bytes,
239 data.extra_data.len()
240 ),
241 ));
242 }
243 out.extend_from_slice(&data.flags.to_be_bytes());
244 out.extend_from_slice(&data.extra_data);
245 }
246 ObjectData::Scenery(data) => {
247 out.extend_from_slice(&data.flags.to_be_bytes());
248 }
249 ObjectData::Misc(data) => {
250 out.extend_from_slice(&data.map.to_be_bytes());
251 out.extend_from_slice(&data.tile.to_be_bytes());
252 out.extend_from_slice(&data.elevation.to_be_bytes());
253 out.extend_from_slice(&data.rotation.to_be_bytes());
254 }
255 ObjectData::Other => {}
256 }
257
258 for item in &self.inventory {
259 out.extend_from_slice(&item.quantity.to_be_bytes());
260 item.object.emit_to_vec(out)?;
261 }
262
263 Ok(())
264 }
265
266 pub fn emit_bytes(&self) -> io::Result<Vec<u8>> {
267 let mut out = Vec::new();
268 self.emit_to_vec(&mut out)?;
269 Ok(out)
270 }
271}
272
273fn parse_critter_object_data<R: Read + Seek>(
274 r: &mut BigEndianReader<R>,
275) -> io::Result<CritterObjectData> {
276 Ok(CritterObjectData {
277 field_0: r.read_i32()?,
278 damage_last_turn: r.read_i32()?,
279 maneuver: r.read_i32()?,
280 ap: r.read_i32()?,
281 results: r.read_i32()?,
282 ai_packet: r.read_i32()?,
283 team: r.read_i32()?,
284 who_hit_me_cid: r.read_i32()?,
285 hp: r.read_i32()?,
286 radiation: r.read_i32()?,
287 poison: r.read_i32()?,
288 })
289}
290
291fn parse_item_object_data<R: Read + Seek>(
301 r: &mut BigEndianReader<R>,
302) -> io::Result<ItemObjectData> {
303 let flags = r.read_i32()?;
304 let pos_after_flags = r.position()?;
305
306 let mut best_extra = 0u8;
308 let mut best_score = -1i32;
309
310 for extra in [0u8, 4, 8] {
311 r.seek_to(pos_after_flags + extra as u64)?;
312 let score = score_next_data(r)?;
313 if score > best_score {
314 best_score = score;
315 best_extra = extra;
316 }
317 }
318
319 r.seek_to(pos_after_flags)?;
320 let extra_data = r.read_bytes(best_extra as usize)?;
321 Ok(ItemObjectData {
322 flags,
323 extra_bytes: best_extra,
324 extra_data,
325 })
326}
327
328fn score_next_data<R: Read + Seek>(r: &mut BigEndianReader<R>) -> io::Result<i32> {
337 let peek_pos = r.position()?;
338
339 let next_qty = match r.read_i32() {
341 Ok(v) => v,
342 Err(_) => {
343 r.seek_to(peek_pos)?;
344 return Ok(1); }
346 };
347
348 if next_qty <= 0 || next_qty > 10_000 {
349 r.seek_to(peek_pos)?;
350 return Ok(0);
351 }
352
353 let mut score = 1;
354
355 let pid_pos = peek_pos + 4 + 44;
357 if r.seek_to(pid_pos).is_ok()
358 && let Ok(next_pid) = r.read_i32()
359 {
360 let next_type = obj_type_from_pid(next_pid);
361 if (0..=5).contains(&next_type) {
362 score = 2;
363
364 let inv_len_pos = peek_pos + 4 + 72;
366 if r.seek_to(inv_len_pos).is_ok()
367 && let Ok(inv_len) = r.read_i32()
368 && (0..1000).contains(&inv_len)
369 {
370 score = 3;
371 }
372 }
373 }
374
375 r.seek_to(peek_pos)?;
376 Ok(score)
377}
378
379fn parse_misc_object_data<R: Read + Seek>(
380 r: &mut BigEndianReader<R>,
381) -> io::Result<MiscObjectData> {
382 Ok(MiscObjectData {
383 map: r.read_i32()?,
384 tile: r.read_i32()?,
385 elevation: r.read_i32()?,
386 rotation: r.read_i32()?,
387 })
388}
389
390#[cfg(test)]
391mod tests {
392 use std::io::Cursor;
393
394 use super::GameObject;
395 use crate::reader::BigEndianReader;
396
397 #[test]
398 fn parse_object_allows_negative_one_inventory_length() {
399 let mut bytes = Vec::new();
400
401 for v in [
403 1i32, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x20000001, -1, 0, 0, 0, -1, -1,
405 ] {
406 bytes.extend_from_slice(&v.to_be_bytes());
407 }
408
409 bytes.extend_from_slice(&(-1i32).to_be_bytes());
411 bytes.extend_from_slice(&(0i32).to_be_bytes());
412 bytes.extend_from_slice(&(0i32).to_be_bytes());
413
414 bytes.extend_from_slice(&(0i32).to_be_bytes());
416
417 let mut reader = BigEndianReader::new(Cursor::new(bytes));
418 let parsed = GameObject::parse(&mut reader).expect("object should parse");
419
420 assert_eq!(parsed.inventory_length, -1);
421 assert!(parsed.inventory.is_empty());
422 }
423}