1mod access;
33mod chunk;
34mod defs;
35mod diagnostic;
36mod files;
37mod plane;
38mod profile;
39mod read;
40mod schema;
41mod stairwell;
42pub mod transitions;
43mod uid;
44
45use std::{collections::BTreeMap, path::Path, sync::Arc};
46use tilewright_jsonc::Limits;
47
48pub use access::{
49 Access, AccessError, Audience, LOCK_NAME, Mounts, SourceDirectory, SourceLock, SourceRoot,
50};
51use diagnostic::Diagnostics;
52pub use diagnostic::{Code, Diagnostic};
53pub use files::is_jsonc_path;
54pub use read::{is_identifier, is_namespace};
55pub use schema::{FieldSpec, FieldType, MAXIMUM_FIELD_DEPTH, MAXIMUM_LIST_ITEMS, Schema};
56pub use transitions::{ResolvedChunk, TerrainSource, Transitions};
57
58pub const CHUNK_SIZE: u16 = 32;
60
61pub const MAXIMUM_GROUND_LAYERS: usize = 8;
63
64pub const MAXIMUM_LAYER_HEIGHT: u32 = 65_535;
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub struct LoadOptions {
72 pub limits: Limits,
75 pub maximum_files: usize,
77 pub maximum_entries: usize,
80 pub maximum_total_bytes: u64,
82 pub maximum_diagnostics: usize,
85}
86
87impl Default for LoadOptions {
88 fn default() -> Self {
89 Self {
90 limits: Limits::default(),
91 maximum_files: 500_000,
92 maximum_entries: 1_000_000,
93 maximum_total_bytes: 8 * 1024 * 1024 * 1024,
94 maximum_diagnostics: 10_000,
95 }
96 }
97}
98
99#[derive(Clone, Debug, PartialEq)]
111pub struct World {
112 profile: Profile,
113 planes: BTreeMap<String, Plane>,
114 definitions: BTreeMap<(String, String), Definition>,
115 object_uids: BTreeMap<String, ObjectLocation>,
116 source_digest: [u8; 32],
117}
118
119impl World {
120 #[must_use]
121 pub const fn profile(&self) -> &Profile {
122 &self.profile
123 }
124
125 #[must_use]
127 pub const fn planes(&self) -> &BTreeMap<String, Plane> {
128 &self.planes
129 }
130
131 #[must_use]
133 pub const fn definitions(&self) -> &BTreeMap<(String, String), Definition> {
134 &self.definitions
135 }
136
137 #[must_use]
139 pub const fn source_digest(&self) -> [u8; 32] {
140 self.source_digest
141 }
142
143 #[must_use]
146 pub const fn object_uids(&self) -> &BTreeMap<String, ObjectLocation> {
147 &self.object_uids
148 }
149
150 #[must_use]
152 pub fn object_location(&self, uid: &str) -> Option<&ObjectLocation> {
153 self.object_uids.get(uid)
154 }
155
156 #[must_use]
158 pub fn object(&self, uid: &str) -> Option<&Object> {
159 let location = self.object_uids.get(uid)?;
160 self.planes
161 .get(&location.plane)?
162 .chunks
163 .get(&location.chunk)?
164 .object(&location.path)
165 }
166}
167
168#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
170pub struct ObjectLocation {
171 pub plane: String,
172 pub chunk: (i32, i64, i64),
174 pub path: Vec<usize>,
178}
179
180#[derive(Clone, Debug, PartialEq)]
182pub struct Profile {
183 pub id: String,
184 pub version: String,
185 pub projection: Projection,
186 pub render_layers: Vec<String>,
187 pub ground_layers: Vec<String>,
189 pub definition_types: BTreeMap<String, Arc<Schema>>,
192 pub object_fields: Arc<Schema>,
193 pub zone_properties: Arc<Schema>,
194 pub layer_height: Option<u32>,
198 pub movement: Option<Movement>,
199 pub transitions: Option<Arc<Transitions>>,
203 pub canonical: String,
205}
206
207impl Profile {
208 #[must_use]
210 pub fn render_layer_index(&self, name: &str) -> Option<usize> {
211 self.render_layers.iter().position(|layer| layer == name)
212 }
213}
214
215#[derive(Clone, Copy, Debug, Eq, PartialEq)]
216pub enum Projection {
217 Isometric { tile_width: u32, tile_height: u32 },
218 TopDown { tile_size: u32 },
219}
220
221#[derive(Clone, Copy, Debug, Eq, PartialEq)]
223pub struct Movement {
224 pub max_step: u32,
225 pub fall_unit: u32,
226}
227
228#[derive(Clone, Copy, Debug, Eq, PartialEq)]
229pub enum PlaneKind {
230 Shared,
231 Instanced,
232 PerPlayer,
233}
234
235impl PlaneKind {
236 #[must_use]
237 pub const fn as_str(self) -> &'static str {
238 match self {
239 Self::Shared => "shared",
240 Self::Instanced => "instanced",
241 Self::PerPlayer => "per_player",
242 }
243 }
244}
245
246#[derive(Clone, Debug, PartialEq)]
248pub struct Plane {
249 pub id: String,
250 pub title: Option<String>,
251 pub kind: PlaneKind,
252 pub chunk_size: u16,
253 pub default_zone: Option<String>,
254 pub canonical: String,
256 pub zones: BTreeMap<String, Zone>,
257 pub anchors: BTreeMap<String, Anchor>,
258 pub stairwells: BTreeMap<String, Stairwell>,
259 pub chunks: BTreeMap<(i32, i64, i64), Chunk>,
261}
262
263#[derive(Clone, Debug, PartialEq)]
264pub struct Zone {
265 pub id: String,
266 pub parent: Option<String>,
267 pub name: Option<String>,
268 pub canonical: String,
270}
271
272#[derive(Clone, Copy, Debug, Eq, PartialEq)]
273pub struct Anchor {
274 pub layer: i32,
275 pub at: [i64; 2],
276}
277
278#[derive(Clone, Debug, Eq, PartialEq)]
281pub struct Stairwell {
282 pub id: String,
283 pub steps: Vec<StairwellStep>,
287 pub direction: StairwellDirection,
288 pub hidden_on_other_layers: bool,
290 pub custom: Option<String>,
292 pub canonical: String,
294}
295
296impl Stairwell {
297 #[must_use]
301 pub fn connects(steps: &[StairwellStep]) -> bool {
302 let [first, second, ..] = steps else {
303 return false;
304 };
305 let up = second.layer > first.layer;
306 steps.windows(2).all(|pair| pair[0].leads_to(&pair[1], up))
307 }
308
309 #[must_use]
311 pub fn lower_end(&self) -> Option<&StairwellStep> {
312 self.steps.iter().min_by_key(|step| step.layer)
313 }
314
315 #[must_use]
317 pub fn upper_end(&self) -> Option<&StairwellStep> {
318 self.steps.iter().max_by_key(|step| step.layer)
319 }
320}
321
322#[derive(Clone, Debug, Eq, PartialEq)]
324pub struct StairwellStep {
325 pub layer: i32,
326 pub at: [i64; 2],
327 pub proto: String,
328}
329
330impl StairwellStep {
331 #[must_use]
335 pub fn leads_to(&self, next: &Self, up: bool) -> bool {
336 adjacent((self.layer, self.at), (next.layer, next.at), up)
337 }
338}
339
340pub(crate) fn adjacent(before: (i32, [i64; 2]), next: (i32, [i64; 2]), up: bool) -> bool {
343 let change = if up { 1 } else { -1 };
344 i64::from(next.0) - i64::from(before.0) == change
345 && before
346 .1
347 .iter()
348 .zip(next.1)
349 .all(|(before, after)| (i128::from(*before) - i128::from(after)).abs() <= 1)
350}
351
352#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
354pub enum StairwellDirection {
355 #[default]
357 Both,
358 Down,
360 Up,
362}
363
364impl StairwellDirection {
365 #[must_use]
366 pub const fn as_str(self) -> &'static str {
367 match self {
368 Self::Both => "both",
369 Self::Down => "down",
370 Self::Up => "up",
371 }
372 }
373
374 #[must_use]
376 pub fn parse(text: &str) -> Option<Self> {
377 match text {
378 "both" => Some(Self::Both),
379 "down" => Some(Self::Down),
380 "up" => Some(Self::Up),
381 _ => None,
382 }
383 }
384}
385
386#[derive(Clone, Debug, Default, Eq, PartialEq)]
388pub struct Grid {
389 pub palette: Vec<String>,
391 pub cells: Vec<u16>,
394}
395
396impl Grid {
397 #[must_use]
399 pub fn value(&self, index: usize) -> Option<&str> {
400 let cell = usize::from(*self.cells.get(index)?);
401 cell.checked_sub(1)
402 .and_then(|palette| self.palette.get(palette))
403 .map(String::as_str)
404 }
405}
406
407#[derive(Clone, Debug, PartialEq)]
409pub struct Chunk {
410 pub path: String,
412 pub ground: Vec<Grid>,
414 pub heights: Vec<i32>,
416 pub zones: Grid,
418 pub objects: Vec<Object>,
419}
420
421impl Chunk {
422 #[must_use]
425 pub fn object(&self, path: &[usize]) -> Option<&Object> {
426 let (first, rest) = path.split_first()?;
427 rest.iter()
428 .try_fold(self.objects.get(*first)?, |object, index| {
429 object.inventory.get(*index)
430 })
431 }
432
433 #[must_use]
438 pub fn depth_first(&self) -> DepthFirst<'_> {
439 DepthFirst {
440 levels: vec![self.objects.iter()],
441 }
442 }
443}
444
445#[derive(Clone, Debug)]
447pub struct DepthFirst<'c> {
448 levels: Vec<std::slice::Iter<'c, Object>>,
450}
451
452impl<'c> Iterator for DepthFirst<'c> {
453 type Item = &'c Object;
454
455 fn next(&mut self) -> Option<Self::Item> {
456 loop {
457 let level = self.levels.last_mut()?;
458 if let Some(object) = level.next() {
459 self.levels.push(object.inventory.iter());
460 return Some(object);
461 }
462 self.levels.pop();
463 }
464 }
465}
466
467#[derive(Clone, Debug, PartialEq)]
469pub struct Object {
470 pub uid: Option<String>,
472 pub at: Option<[u16; 2]>,
474 pub proto: String,
475 pub layer: Option<String>,
476 pub sub_layer: i16,
477 pub z: i32,
478 pub fields: Vec<(String, FieldValue)>,
479 pub portal: Option<Portal>,
480 pub inventory: Vec<Object>,
481 pub custom: Option<String>,
483}
484
485#[derive(Clone, Debug, PartialEq)]
488pub enum FieldValue {
489 Null,
490 Bool(bool),
491 Integer(i64),
492 Number(f64),
493 String(String),
494 Json(String),
495}
496
497#[derive(Clone, Debug, Eq, PartialEq)]
498pub enum Portal {
499 Location {
500 plane: Option<String>,
501 layer: i32,
502 at: [i64; 2],
503 },
504 Anchor {
505 plane: Option<String>,
506 anchor: String,
507 },
508}
509
510#[derive(Clone, Debug, PartialEq)]
511pub struct Definition {
512 pub fields: Vec<(String, FieldValue)>,
513 pub canonical: String,
515}
516
517pub fn load(root: &Path) -> Result<World, Vec<Diagnostic>> {
522 load_with(root, &LoadOptions::default())
523}
524
525pub fn load_with(root: &Path, options: &LoadOptions) -> Result<World, Vec<Diagnostic>> {
530 load_edited(root, options, &Overlay::new())
531}
532
533pub type Overlay = BTreeMap<String, Option<String>>;
537
538pub fn load_edited(
547 root: &Path,
548 options: &LoadOptions,
549 overlay: &Overlay,
550) -> Result<World, Vec<Diagnostic>> {
551 load_edited_in(&SourceRoot::new(root), options, overlay)
552}
553
554pub fn load_edited_in(
561 root: &SourceRoot,
562 options: &LoadOptions,
563 overlay: &Overlay,
564) -> Result<World, Vec<Diagnostic>> {
565 let options = &LoadOptions {
567 limits: within_format(options.limits),
568 maximum_diagnostics: options.maximum_diagnostics.max(1),
569 ..*options
570 };
571 if overlay.len() > options.maximum_entries {
574 return Err(vec![Diagnostic::file(
575 ".",
576 Code::Budget,
577 format!(
578 "the edits hold more than {} entries",
579 options.maximum_entries
580 ),
581 )]);
582 }
583 if let Some(key) = overlay
586 .keys()
587 .find(|key| key.is_empty() || access::checked(key).is_err())
588 {
589 let shown = if key.len() <= 128 {
590 format!("{key:?}")
591 } else {
592 format!("a path of {} bytes", key.len())
593 };
594 return Err(vec![Diagnostic::file(
595 ".",
596 Code::Path,
597 format!("the edits name {shown}, which is not a path a file of the tree can have"),
598 )]);
599 }
600 let mut diagnostics = Diagnostics::new(options.maximum_diagnostics);
601 let mut tree = files::Tree::in_root(root.platform(), options, overlay, diagnostics.total());
602 let world = load_tree(&mut tree, &mut diagnostics);
603 diagnostics.append(tree.take_diagnostics());
604 match world {
605 Some(world) if diagnostics.is_empty() => Ok(World {
606 source_digest: tree.digest(),
607 ..world
608 }),
609 _ => Err(diagnostics.finish()),
610 }
611}
612
613fn within_format(limits: Limits) -> Limits {
615 let format = Limits::default();
616 Limits {
617 max_input_bytes: limits.max_input_bytes.min(format.max_input_bytes),
618 max_depth: limits.max_depth.min(format.max_depth),
619 max_values: limits.max_values.min(format.max_values),
620 max_array_items: limits.max_array_items.min(format.max_array_items),
621 max_object_keys: limits.max_object_keys.min(format.max_object_keys),
622 max_string_bytes: limits.max_string_bytes.min(format.max_string_bytes),
623 max_number_bytes: limits.max_number_bytes.min(format.max_number_bytes),
624 max_comments: limits.max_comments.min(format.max_comments),
625 max_comment_bytes: limits.max_comment_bytes.min(format.max_comment_bytes),
626 max_lines: limits.max_lines.min(format.max_lines),
627 max_line_bytes: limits.max_line_bytes.min(format.max_line_bytes),
628 }
629}
630
631fn load_tree(tree: &mut files::Tree, diagnostics: &mut Diagnostics) -> Option<World> {
632 let (manifest_path, plane_ids) = profile::read_manifest(tree, diagnostics)?;
633 let (mut profile, transitions_path) = profile::read_profile(tree, &manifest_path, diagnostics)?;
634 let mut references = schema::References::new(&profile);
635 let definitions = defs::read_definitions(tree, &profile, &mut references, diagnostics);
636 if let Some(path) = transitions_path {
639 profile.transitions =
640 transitions::read_transitions(tree, &path, &profile, &references, diagnostics)
641 .map(Arc::new);
642 }
643 let mut planes = BTreeMap::new();
644 for plane in plane::read_planes(tree, &profile, &plane_ids, &mut references, diagnostics) {
645 planes.insert(plane.id.clone(), plane);
646 }
647 let object_uids = uid::index(&planes);
648 Some(World {
649 profile,
650 planes,
651 definitions,
652 object_uids,
653 source_digest: [0; 32],
654 })
655}
656
657#[cfg(test)]
658mod tests;