1use std::fmt;
10use tilewright_jsonc::{Document, Value};
11
12pub const CHUNK_ENCODING: &str = "twc1+zstd";
14pub const ZSTD_LEVEL: i32 = 19;
16pub const MAXIMUM_RECORD_BYTES: usize = 16 * 1024 * 1024;
18pub const MAXIMUM_CHUNK_BYTES: usize = MAXIMUM_RECORD_BYTES + (MAXIMUM_RECORD_BYTES >> 8);
21pub const MAXIMUM_DEPTH: usize = 16;
23pub const RECORD_VERSION: u16 = 2;
25
26const MAGIC: &[u8; 4] = b"TWC1";
27const NO_TILE: u16 = u16::MAX;
28const FLAG_PORTAL: u8 = 1;
29const FLAG_FIELDS: u8 = 2;
30const FLAG_CUSTOM: u8 = 4;
31const FLAG_INVENTORY: u8 = 8;
32const FLAG_UID: u8 = 16;
33
34#[derive(Clone, Debug, PartialEq)]
36pub struct ChunkData {
37 pub chunk_size: u16,
38 pub palette: Vec<u32>,
40 pub ground: Vec<Vec<u16>>,
43 pub heights: Vec<i32>,
44 pub zones: Vec<u16>,
46 pub objects: Vec<ObjectRecord>,
47}
48
49impl ChunkData {
50 #[must_use]
54 pub fn object(&self, index: usize) -> Option<&ObjectRecord> {
55 let mut remaining = index;
56 let mut levels = vec![self.objects.iter()];
57 while let Some(level) = levels.last_mut() {
58 let Some(object) = level.next() else {
59 levels.pop();
60 continue;
61 };
62 if remaining == 0 {
63 return Some(object);
64 }
65 remaining -= 1;
66 levels.push(object.inventory.iter());
67 }
68 None
69 }
70}
71
72#[derive(Clone, Debug, PartialEq)]
73pub struct ObjectRecord {
74 pub uid: Option<String>,
76 pub tile: Option<u16>,
78 pub proto: u32,
79 pub layer: u16,
81 pub sub_layer: i16,
82 pub z: i32,
83 pub portal: Option<PortalRecord>,
84 pub fields: Vec<(u32, FieldRecord)>,
85 pub custom: Option<String>,
87 pub inventory: Vec<ObjectRecord>,
88}
89
90#[derive(Clone, Debug, PartialEq)]
91pub enum PortalRecord {
92 Location {
94 plane: u32,
95 layer: i32,
96 x: i64,
97 y: i64,
98 },
99 Anchor {
100 plane: u32,
101 anchor: u32,
102 },
103}
104
105#[derive(Clone, Debug, PartialEq)]
106pub enum FieldRecord {
107 Null,
108 Bool(bool),
109 Integer(i64),
110 Number(f64),
111 String(u32),
113 Json(String),
115}
116
117#[derive(Clone, Debug, Eq, PartialEq)]
119pub struct DecodeError(pub String);
120
121impl fmt::Display for DecodeError {
122 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123 write!(formatter, "invalid chunk record: {}", self.0)
124 }
125}
126
127impl std::error::Error for DecodeError {}
128
129fn invalid<T>(message: impl Into<String>) -> Result<T, DecodeError> {
130 Err(DecodeError(message.into()))
131}
132
133pub fn encode_chunk(chunk: &ChunkData) -> Result<Vec<u8>, DecodeError> {
138 let record = encode_record(chunk)?;
139 zstd::bulk::compress(&record, ZSTD_LEVEL)
140 .or_else(|error| invalid(format!("zstd compression failed: {error}")))
141}
142
143pub fn encode_record(chunk: &ChunkData) -> Result<Vec<u8>, DecodeError> {
148 let cells = check_shape(chunk)?;
149 let grids = cells * (2 * chunk.ground.len() + 6);
152 room(
154 &[],
155 18_usize
156 .saturating_add(chunk.palette.len().saturating_mul(4))
157 .saturating_add(grids),
158 )?;
159 let mut output = Vec::with_capacity(16 + grids);
160 output.extend_from_slice(MAGIC);
161 output.extend_from_slice(&RECORD_VERSION.to_le_bytes());
162 output.extend_from_slice(&chunk.chunk_size.to_le_bytes());
163 output.push(u8::try_from(chunk.ground.len()).or_else(|_| invalid("too many ground layers"))?);
164 output.push(0);
165 push_u32(&mut output, chunk.palette.len())?;
166 for id in &chunk.palette {
167 output.extend_from_slice(&id.to_le_bytes());
168 }
169 for grid in &chunk.ground {
170 for cell in grid {
171 output.extend_from_slice(&cell.to_le_bytes());
172 }
173 }
174 for height in &chunk.heights {
175 output.extend_from_slice(&height.to_le_bytes());
176 }
177 for cell in &chunk.zones {
178 output.extend_from_slice(&cell.to_le_bytes());
179 }
180 push_u32(&mut output, chunk.objects.len())?;
181 for object in &chunk.objects {
182 encode_object(&mut output, object, cells, 0)?;
183 }
184 if output.len() > MAXIMUM_RECORD_BYTES {
185 return invalid("record exceeds 16 MiB");
186 }
187 Ok(output)
188}
189
190fn check_shape(chunk: &ChunkData) -> Result<usize, DecodeError> {
191 if chunk.chunk_size == 0 || chunk.chunk_size > 255 {
192 return invalid("chunk size must be 1..=255");
193 }
194 let cells = usize::from(chunk.chunk_size) * usize::from(chunk.chunk_size);
195 if chunk.ground.len() > 8
196 || chunk.ground.iter().any(|grid| grid.len() != cells)
197 || chunk.heights.len() != cells
198 || chunk.zones.len() != cells
199 {
200 return invalid("grid sizes do not match the chunk size");
201 }
202 if chunk.palette.first() == Some(&0) || chunk.palette.windows(2).any(|pair| pair[0] >= pair[1])
203 {
204 return invalid("palette string IDs must be nonzero and strictly increasing");
205 }
206 let limit = chunk.palette.len();
207 if chunk
208 .ground
209 .iter()
210 .flatten()
211 .chain(&chunk.zones)
212 .any(|cell| usize::from(*cell) > limit)
213 {
214 return invalid("a grid cell is outside the palette");
215 }
216 Ok(cells)
217}
218
219fn check_json(text: &str, array_allowed: bool) -> Result<(), DecodeError> {
222 let document = Document::parse(text, &crate::canonical_limits())
223 .or_else(|error| invalid(format!("embedded JSON: {error}")))?;
224 let shape_ok = match document.root() {
226 Value::Object { members, .. } => {
227 array_allowed
228 || members
229 .iter()
230 .all(|member| crate::is_namespace(&member.key))
231 }
232 Value::Array { .. } => array_allowed,
233 _ => false,
234 };
235 if !shape_ok {
236 return invalid(if array_allowed {
237 "JSON field values must be arrays or objects"
238 } else {
239 "custom data must be an object whose keys are namespaces"
240 });
241 }
242 if document.canonical(document.root()) != text {
243 return invalid("embedded JSON is not canonical");
244 }
245 Ok(())
246}
247
248fn room(output: &[u8], additional: usize) -> Result<(), DecodeError> {
251 if output.len().saturating_add(additional) > MAXIMUM_RECORD_BYTES {
252 invalid("record exceeds 16 MiB")
253 } else {
254 Ok(())
255 }
256}
257
258fn push_u32(output: &mut Vec<u8>, value: usize) -> Result<(), DecodeError> {
259 let value = u32::try_from(value).or_else(|_| invalid("count exceeds u32"))?;
260 output.extend_from_slice(&value.to_le_bytes());
261 Ok(())
262}
263
264fn field_block_bytes(fields: &[(u32, FieldRecord)]) -> usize {
266 fields.iter().fold(2_usize, |total, (_, value)| {
267 total.saturating_add(match value {
268 FieldRecord::Null | FieldRecord::Bool(_) => 5,
269 FieldRecord::Integer(_) | FieldRecord::Number(_) => 13,
270 FieldRecord::String(_) => 9,
271 FieldRecord::Json(text) => text.len().saturating_add(9),
272 })
273 })
274}
275
276fn flags(object: &ObjectRecord) -> u8 {
278 [
279 (object.uid.is_some(), FLAG_UID),
280 (object.portal.is_some(), FLAG_PORTAL),
281 (!object.fields.is_empty(), FLAG_FIELDS),
282 (object.custom.is_some(), FLAG_CUSTOM),
283 (!object.inventory.is_empty(), FLAG_INVENTORY),
284 ]
285 .iter()
286 .filter(|(present, _)| *present)
287 .fold(0, |flags, (_, flag)| flags | flag)
288}
289
290fn encode_object(
291 output: &mut Vec<u8>,
292 object: &ObjectRecord,
293 cells: usize,
294 depth: usize,
295) -> Result<(), DecodeError> {
296 if depth > MAXIMUM_DEPTH {
297 return invalid("inventory nesting too deep");
298 }
299 let uid = match &object.uid {
302 Some(uid) if crate::is_identifier(uid) => 1 + uid.len(),
303 Some(_) => return invalid("a uid is not an identifier"),
304 None => 0,
305 };
306 let portal = match object.portal {
307 Some(PortalRecord::Location { .. }) => 25,
308 Some(PortalRecord::Anchor { .. }) => 9,
309 None => 0,
310 };
311 room(output, 15 + uid + portal)?;
312 let tile = match (object.tile, depth) {
313 (Some(tile), 0) if usize::from(tile) < cells => tile,
314 (None, depth) if depth > 0 => NO_TILE,
315 _ => return invalid("top-level objects need a tile in the chunk; inventory entries none"),
316 };
317 output.extend_from_slice(&tile.to_le_bytes());
318 output.extend_from_slice(&object.proto.to_le_bytes());
319 output.extend_from_slice(&object.layer.to_le_bytes());
320 output.extend_from_slice(&object.sub_layer.to_le_bytes());
321 output.extend_from_slice(&object.z.to_le_bytes());
322 output.push(flags(object));
323 if let Some(uid) = &object.uid {
324 output.push(u8::try_from(uid.len()).or_else(|_| invalid("a uid is too long"))?);
326 output.extend_from_slice(uid.as_bytes());
327 }
328 match &object.portal {
329 Some(PortalRecord::Location { plane, layer, x, y }) => {
330 output.push(0);
331 output.extend_from_slice(&plane.to_le_bytes());
332 output.extend_from_slice(&layer.to_le_bytes());
333 output.extend_from_slice(&x.to_le_bytes());
334 output.extend_from_slice(&y.to_le_bytes());
335 }
336 Some(PortalRecord::Anchor { plane, anchor }) => {
337 output.push(1);
338 output.extend_from_slice(&plane.to_le_bytes());
339 output.extend_from_slice(&anchor.to_le_bytes());
340 }
341 None => {}
342 }
343 if !object.fields.is_empty() {
344 let count = u16::try_from(object.fields.len()).or_else(|_| invalid("too many fields"))?;
345 room(output, field_block_bytes(&object.fields))?;
346 output.extend_from_slice(&count.to_le_bytes());
347 for (name, value) in &object.fields {
348 output.extend_from_slice(&name.to_le_bytes());
349 match value {
350 FieldRecord::Null => output.push(0),
351 FieldRecord::Bool(false) => output.push(1),
352 FieldRecord::Bool(true) => output.push(2),
353 FieldRecord::Integer(number) => {
354 output.push(3);
355 output.extend_from_slice(&number.to_le_bytes());
356 }
357 FieldRecord::Number(number) => {
358 if !number.is_finite() {
359 return invalid("numbers must be finite");
360 }
361 output.push(4);
362 output.extend_from_slice(&number.to_le_bytes());
363 }
364 FieldRecord::String(id) => {
365 output.push(5);
366 output.extend_from_slice(&id.to_le_bytes());
367 }
368 FieldRecord::Json(text) => {
369 check_json(text, true)?;
370 output.push(6);
371 push_u32(output, text.len())?;
372 output.extend_from_slice(text.as_bytes());
373 }
374 }
375 }
376 }
377 if let Some(custom) = &object.custom {
378 room(output, 4_usize.saturating_add(custom.len()))?;
379 check_json(custom, false)?;
380 push_u32(output, custom.len())?;
381 output.extend_from_slice(custom.as_bytes());
382 }
383 if !object.inventory.is_empty() {
384 room(output, 4)?;
385 push_u32(output, object.inventory.len())?;
386 for item in &object.inventory {
387 encode_object(output, item, cells, depth + 1)?;
388 }
389 }
390 Ok(())
391}
392
393pub fn decode_chunk(data: &[u8]) -> Result<ChunkData, DecodeError> {
402 decode_chunk_with(data, None)
403}
404
405pub(crate) type Visit<'v> = &'v mut dyn FnMut(&ObjectRecord) -> Result<(), DecodeError>;
408
409pub(crate) fn decode_chunk_with(
413 data: &[u8],
414 visit: Option<Visit<'_>>,
415) -> Result<ChunkData, DecodeError> {
416 if data.len() > MAXIMUM_CHUNK_BYTES {
417 return invalid(format!("stored chunk exceeds {MAXIMUM_CHUNK_BYTES} bytes"));
418 }
419 if zstd::zstd_safe::find_frame_compressed_size(data).ok() != Some(data.len()) {
421 return invalid("not exactly one zstd frame");
422 }
423 match zstd::zstd_safe::get_frame_content_size(data) {
424 Ok(Some(size)) if size <= MAXIMUM_RECORD_BYTES as u64 => {}
425 Ok(Some(_)) => return invalid("record exceeds 16 MiB"),
426 _ => return invalid("not a zstd frame with a recorded content size"),
427 }
428 let record = zstd::bulk::decompress(data, MAXIMUM_RECORD_BYTES)
429 .or_else(|error| invalid(format!("zstd decompression failed: {error}")))?;
430 decode_record_with(&record, visit)
431}
432
433struct Cursor<'a> {
434 bytes: &'a [u8],
435 position: usize,
436}
437
438impl<'a> Cursor<'a> {
439 fn take(&mut self, count: usize) -> Result<&'a [u8], DecodeError> {
440 let end = self
441 .position
442 .checked_add(count)
443 .filter(|end| *end <= self.bytes.len())
444 .ok_or_else(|| DecodeError("record ends early".to_owned()))?;
445 let slice = &self.bytes[self.position..end];
446 self.position = end;
447 Ok(slice)
448 }
449
450 fn array<const N: usize>(&mut self) -> Result<[u8; N], DecodeError> {
451 let mut output = [0; N];
452 output.copy_from_slice(self.take(N)?);
453 Ok(output)
454 }
455
456 fn u8(&mut self) -> Result<u8, DecodeError> {
457 Ok(self.array::<1>()?[0])
458 }
459
460 fn u16(&mut self) -> Result<u16, DecodeError> {
461 Ok(u16::from_le_bytes(self.array()?))
462 }
463
464 fn i16(&mut self) -> Result<i16, DecodeError> {
465 Ok(i16::from_le_bytes(self.array()?))
466 }
467
468 fn u32(&mut self) -> Result<u32, DecodeError> {
469 Ok(u32::from_le_bytes(self.array()?))
470 }
471
472 fn i32(&mut self) -> Result<i32, DecodeError> {
473 Ok(i32::from_le_bytes(self.array()?))
474 }
475
476 fn i64(&mut self) -> Result<i64, DecodeError> {
477 Ok(i64::from_le_bytes(self.array()?))
478 }
479
480 fn f64(&mut self) -> Result<f64, DecodeError> {
481 Ok(f64::from_le_bytes(self.array()?))
482 }
483
484 fn remaining(&self) -> usize {
485 self.bytes.len() - self.position
486 }
487
488 fn count(&mut self, minimum: usize) -> Result<usize, DecodeError> {
490 let count = usize::try_from(self.u32()?).or_else(|_| invalid("count exceeds usize"))?;
491 if count.saturating_mul(minimum) > self.remaining() {
492 return invalid("count exceeds the record");
493 }
494 Ok(count)
495 }
496
497 fn uid(&mut self) -> Result<String, DecodeError> {
499 let length = usize::from(self.u8()?);
500 match std::str::from_utf8(self.take(length)?) {
501 Ok(uid) if crate::is_identifier(uid) => Ok(uid.to_owned()),
502 _ => invalid("a uid is not an identifier"),
503 }
504 }
505
506 fn text(&mut self) -> Result<String, DecodeError> {
507 let length = self.count(1)?;
508 String::from_utf8(self.take(length)?.to_vec()).or_else(|_| invalid("text is not UTF-8"))
509 }
510}
511
512pub fn decode_record(record: &[u8]) -> Result<ChunkData, DecodeError> {
517 decode_record_with(record, None)
518}
519
520fn decode_record_with(
522 record: &[u8],
523 mut visit: Option<Visit<'_>>,
524) -> Result<ChunkData, DecodeError> {
525 if record.len() > MAXIMUM_RECORD_BYTES {
526 return invalid("record exceeds 16 MiB");
527 }
528 let mut cursor = Cursor {
529 bytes: record,
530 position: 0,
531 };
532 if cursor.take(4)? != MAGIC {
533 return invalid("bad magic");
534 }
535 if cursor.u16()? != RECORD_VERSION {
536 return invalid("unsupported version");
537 }
538 let chunk_size = cursor.u16()?;
539 if chunk_size == 0 || chunk_size > 255 {
540 return invalid("chunk size must be 1..=255");
541 }
542 let ground_count = usize::from(cursor.u8()?);
543 if ground_count > 8 || cursor.u8()? != 0 {
544 return invalid("bad ground count or reserved byte");
545 }
546 let palette_length = cursor.count(4)?;
547 let mut palette = Vec::with_capacity(palette_length);
548 for _ in 0..palette_length {
549 palette.push(cursor.u32()?);
550 }
551 let cells = usize::from(chunk_size) * usize::from(chunk_size);
552 let mut ground = Vec::with_capacity(ground_count);
553 for _ in 0..ground_count {
554 if cursor.remaining() < cells * 2 {
555 return invalid("record ends early");
556 }
557 ground.push(
558 (0..cells)
559 .map(|_| cursor.u16())
560 .collect::<Result<Vec<_>, _>>()?,
561 );
562 }
563 if cursor.remaining() < cells * 6 {
564 return invalid("record ends early");
565 }
566 let heights = (0..cells)
567 .map(|_| cursor.i32())
568 .collect::<Result<Vec<_>, _>>()?;
569 let zones = (0..cells)
570 .map(|_| cursor.u16())
571 .collect::<Result<Vec<_>, _>>()?;
572 let mut chunk = ChunkData {
573 chunk_size,
574 palette,
575 ground,
576 heights,
577 zones,
578 objects: Vec::new(),
579 };
580 check_shape(&chunk)?;
581 let object_count = cursor.count(15)?;
583 for _ in 0..object_count {
584 let object = decode_object(&mut cursor, cells, 0, &mut visit)?;
585 if visit.is_none() {
586 chunk.objects.push(object);
587 }
588 }
589 if cursor.remaining() != 0 {
590 return invalid("trailing bytes");
591 }
592 Ok(chunk)
593}
594
595fn decode_fields(cursor: &mut Cursor<'_>) -> Result<Vec<(u32, FieldRecord)>, DecodeError> {
597 let count = usize::from(cursor.u16()?);
598 if count == 0 || count * 5 > cursor.remaining() {
599 return invalid("bad field count");
600 }
601 let mut fields = Vec::with_capacity(count);
602 for _ in 0..count {
603 let name = cursor.u32()?;
604 let value = match cursor.u8()? {
605 0 => FieldRecord::Null,
606 1 => FieldRecord::Bool(false),
607 2 => FieldRecord::Bool(true),
608 3 => FieldRecord::Integer(cursor.i64()?),
609 4 => {
610 let number = cursor.f64()?;
611 if !number.is_finite() {
612 return invalid("numbers must be finite");
613 }
614 FieldRecord::Number(number)
615 }
616 5 => FieldRecord::String(cursor.u32()?),
617 6 => {
618 let text = cursor.text()?;
619 check_json(&text, true)?;
620 FieldRecord::Json(text)
621 }
622 _ => return invalid("unknown field tag"),
623 };
624 fields.push((name, value));
625 }
626 Ok(fields)
627}
628
629fn decode_object(
630 cursor: &mut Cursor<'_>,
631 cells: usize,
632 depth: usize,
633 visit: &mut Option<Visit<'_>>,
634) -> Result<ObjectRecord, DecodeError> {
635 if depth > MAXIMUM_DEPTH {
636 return invalid("inventory nesting too deep");
637 }
638 let tile = match (cursor.u16()?, depth) {
639 (tile, 0) if usize::from(tile) < cells => Some(tile),
640 (NO_TILE, depth) if depth > 0 => None,
641 _ => return invalid("object tile out of range"),
642 };
643 let proto = cursor.u32()?;
644 let layer = cursor.u16()?;
645 let sub_layer = cursor.i16()?;
646 let z = cursor.i32()?;
647 let flags = cursor.u8()?;
648 if flags & !(FLAG_PORTAL | FLAG_FIELDS | FLAG_CUSTOM | FLAG_INVENTORY | FLAG_UID) != 0 {
649 return invalid("unknown object flags");
650 }
651 let uid = if flags & FLAG_UID == 0 {
652 None
653 } else {
654 Some(cursor.uid()?)
655 };
656 let portal = if flags & FLAG_PORTAL == 0 {
657 None
658 } else {
659 Some(match cursor.u8()? {
660 0 => PortalRecord::Location {
661 plane: cursor.u32()?,
662 layer: cursor.i32()?,
663 x: cursor.i64()?,
664 y: cursor.i64()?,
665 },
666 1 => PortalRecord::Anchor {
667 plane: cursor.u32()?,
668 anchor: cursor.u32()?,
669 },
670 _ => return invalid("unknown portal kind"),
671 })
672 };
673 let fields = if flags & FLAG_FIELDS == 0 {
674 Vec::new()
675 } else {
676 decode_fields(cursor)?
677 };
678 let custom = if flags & FLAG_CUSTOM == 0 {
679 None
680 } else {
681 let text = cursor.text()?;
682 check_json(&text, false)?;
683 Some(text)
684 };
685 let mut object = ObjectRecord {
686 uid,
687 tile,
688 proto,
689 layer,
690 sub_layer,
691 z,
692 portal,
693 fields,
694 custom,
695 inventory: Vec::new(),
696 };
697 if let Some(visit) = visit.as_mut() {
698 visit(&object)?;
699 }
700 if flags & FLAG_INVENTORY != 0 {
701 let count = cursor.count(15)?;
702 if count == 0 {
703 return invalid("empty inventory flagged");
704 }
705 for _ in 0..count {
706 let item = decode_object(cursor, cells, depth + 1, visit)?;
707 if visit.is_none() {
708 object.inventory.push(item);
709 }
710 }
711 }
712 Ok(object)
713}