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,
84 pub maximum_total_bytes: u64,
86 pub maximum_diagnostics: usize,
89}
90
91impl Default for LoadOptions {
92 fn default() -> Self {
93 Self {
94 limits: Limits::default(),
95 maximum_files: 500_000,
96 maximum_entries: 1_000_000,
97 maximum_total_bytes: 8 * 1024 * 1024 * 1024,
98 maximum_diagnostics: 10_000,
99 }
100 }
101}
102
103#[derive(Clone, Debug, PartialEq)]
115pub struct World {
116 profile: Profile,
117 planes: BTreeMap<String, Plane>,
118 definitions: BTreeMap<(String, String), Definition>,
119 object_uids: BTreeMap<String, ObjectLocation>,
120 source_digest: [u8; 32],
121}
122
123impl World {
124 #[must_use]
125 pub const fn profile(&self) -> &Profile {
126 &self.profile
127 }
128
129 #[must_use]
131 pub const fn planes(&self) -> &BTreeMap<String, Plane> {
132 &self.planes
133 }
134
135 #[must_use]
137 pub const fn definitions(&self) -> &BTreeMap<(String, String), Definition> {
138 &self.definitions
139 }
140
141 #[must_use]
143 pub const fn source_digest(&self) -> [u8; 32] {
144 self.source_digest
145 }
146
147 #[must_use]
150 pub const fn object_uids(&self) -> &BTreeMap<String, ObjectLocation> {
151 &self.object_uids
152 }
153
154 #[must_use]
156 pub fn object_location(&self, uid: &str) -> Option<&ObjectLocation> {
157 self.object_uids.get(uid)
158 }
159
160 #[must_use]
162 pub fn object(&self, uid: &str) -> Option<&Object> {
163 let location = self.object_uids.get(uid)?;
164 self.planes
165 .get(&location.plane)?
166 .chunks
167 .get(&location.chunk)?
168 .object(&location.path)
169 }
170}
171
172#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
174pub struct ObjectLocation {
175 pub plane: String,
176 pub chunk: (i32, i64, i64),
178 pub path: Vec<usize>,
182}
183
184#[derive(Clone, Debug, PartialEq)]
186pub struct Profile {
187 pub id: String,
188 pub version: String,
189 pub projection: Projection,
190 pub render_layers: Vec<String>,
191 pub ground_layers: Vec<String>,
193 pub definition_types: BTreeMap<String, Arc<Schema>>,
196 pub object_fields: Arc<Schema>,
197 pub zone_properties: Arc<Schema>,
198 pub layer_height: Option<u32>,
202 pub movement: Option<Movement>,
203 pub transitions: Option<Arc<Transitions>>,
207 pub canonical: String,
209}
210
211impl Profile {
212 #[must_use]
214 pub fn render_layer_index(&self, name: &str) -> Option<usize> {
215 self.render_layers.iter().position(|layer| layer == name)
216 }
217}
218
219#[derive(Clone, Copy, Debug, Eq, PartialEq)]
220pub enum Projection {
221 Isometric { tile_width: u32, tile_height: u32 },
222 TopDown { tile_size: u32 },
223}
224
225#[derive(Clone, Copy, Debug, Eq, PartialEq)]
227pub struct Movement {
228 pub max_step: u32,
229 pub fall_unit: u32,
230}
231
232#[derive(Clone, Copy, Debug, Eq, PartialEq)]
233pub enum PlaneKind {
234 Shared,
235 Instanced,
236 PerPlayer,
237}
238
239impl PlaneKind {
240 #[must_use]
241 pub const fn as_str(self) -> &'static str {
242 match self {
243 Self::Shared => "shared",
244 Self::Instanced => "instanced",
245 Self::PerPlayer => "per_player",
246 }
247 }
248}
249
250#[derive(Clone, Debug, PartialEq)]
252pub struct Plane {
253 pub id: String,
254 pub title: Option<String>,
255 pub kind: PlaneKind,
256 pub chunk_size: u16,
257 pub default_zone: Option<String>,
258 pub canonical: String,
260 pub zones: BTreeMap<String, Zone>,
261 pub anchors: BTreeMap<String, Anchor>,
262 pub stairwells: BTreeMap<String, Stairwell>,
263 pub chunks: BTreeMap<(i32, i64, i64), Chunk>,
265}
266
267#[derive(Clone, Debug, PartialEq)]
268pub struct Zone {
269 pub id: String,
270 pub parent: Option<String>,
271 pub name: Option<String>,
272 pub canonical: String,
274}
275
276#[derive(Clone, Copy, Debug, Eq, PartialEq)]
277pub struct Anchor {
278 pub layer: i32,
279 pub at: [i64; 2],
280}
281
282#[derive(Clone, Debug, Eq, PartialEq)]
285pub struct Stairwell {
286 pub id: String,
287 pub steps: Vec<StairwellStep>,
291 pub direction: StairwellDirection,
292 pub hidden_on_other_layers: bool,
294 pub custom: Option<String>,
296 pub canonical: String,
298}
299
300impl Stairwell {
301 #[must_use]
305 pub fn connects(steps: &[StairwellStep]) -> bool {
306 let [first, second, ..] = steps else {
307 return false;
308 };
309 let up = second.layer > first.layer;
310 steps.windows(2).all(|pair| pair[0].leads_to(&pair[1], up))
311 }
312
313 #[must_use]
315 pub fn lower_end(&self) -> Option<&StairwellStep> {
316 self.steps.iter().min_by_key(|step| step.layer)
317 }
318
319 #[must_use]
321 pub fn upper_end(&self) -> Option<&StairwellStep> {
322 self.steps.iter().max_by_key(|step| step.layer)
323 }
324}
325
326#[derive(Clone, Debug, Eq, PartialEq)]
328pub struct StairwellStep {
329 pub layer: i32,
330 pub at: [i64; 2],
331 pub proto: String,
332}
333
334impl StairwellStep {
335 #[must_use]
339 pub fn leads_to(&self, next: &Self, up: bool) -> bool {
340 adjacent((self.layer, self.at), (next.layer, next.at), up)
341 }
342}
343
344pub(crate) fn adjacent(before: (i32, [i64; 2]), next: (i32, [i64; 2]), up: bool) -> bool {
347 let change = if up { 1 } else { -1 };
348 i64::from(next.0) - i64::from(before.0) == change
349 && before
350 .1
351 .iter()
352 .zip(next.1)
353 .all(|(before, after)| (i128::from(*before) - i128::from(after)).abs() <= 1)
354}
355
356#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
358pub enum StairwellDirection {
359 #[default]
361 Both,
362 Down,
364 Up,
366}
367
368impl StairwellDirection {
369 #[must_use]
370 pub const fn as_str(self) -> &'static str {
371 match self {
372 Self::Both => "both",
373 Self::Down => "down",
374 Self::Up => "up",
375 }
376 }
377
378 #[must_use]
380 pub fn parse(text: &str) -> Option<Self> {
381 match text {
382 "both" => Some(Self::Both),
383 "down" => Some(Self::Down),
384 "up" => Some(Self::Up),
385 _ => None,
386 }
387 }
388}
389
390#[derive(Clone, Debug, Default, Eq, PartialEq)]
392pub struct Grid {
393 pub palette: Vec<String>,
395 pub cells: Vec<u16>,
398}
399
400impl Grid {
401 #[must_use]
403 pub fn value(&self, index: usize) -> Option<&str> {
404 let cell = usize::from(*self.cells.get(index)?);
405 cell.checked_sub(1)
406 .and_then(|palette| self.palette.get(palette))
407 .map(String::as_str)
408 }
409}
410
411#[derive(Clone, Debug, PartialEq)]
413pub struct Chunk {
414 pub path: String,
416 pub ground: Vec<Grid>,
418 pub heights: Vec<i32>,
420 pub zones: Grid,
422 pub objects: Vec<Object>,
423}
424
425impl Chunk {
426 #[must_use]
429 pub fn object(&self, path: &[usize]) -> Option<&Object> {
430 let (first, rest) = path.split_first()?;
431 rest.iter()
432 .try_fold(self.objects.get(*first)?, |object, index| {
433 object.inventory.get(*index)
434 })
435 }
436
437 #[must_use]
442 pub fn depth_first(&self) -> DepthFirst<'_> {
443 DepthFirst {
444 levels: vec![self.objects.iter()],
445 }
446 }
447}
448
449#[derive(Clone, Debug)]
451pub struct DepthFirst<'c> {
452 levels: Vec<std::slice::Iter<'c, Object>>,
454}
455
456impl<'c> Iterator for DepthFirst<'c> {
457 type Item = &'c Object;
458
459 fn next(&mut self) -> Option<Self::Item> {
460 loop {
461 let level = self.levels.last_mut()?;
462 if let Some(object) = level.next() {
463 self.levels.push(object.inventory.iter());
464 return Some(object);
465 }
466 self.levels.pop();
467 }
468 }
469}
470
471#[derive(Clone, Debug, PartialEq)]
473pub struct Object {
474 pub uid: Option<String>,
476 pub at: Option<[u16; 2]>,
478 pub proto: String,
479 pub layer: Option<String>,
480 pub sub_layer: i16,
481 pub z: i32,
482 pub fields: Vec<(String, FieldValue)>,
483 pub portal: Option<Portal>,
484 pub inventory: Vec<Object>,
485 pub custom: Option<String>,
487}
488
489#[derive(Clone, Debug, PartialEq)]
492pub enum FieldValue {
493 Null,
494 Bool(bool),
495 Integer(i64),
496 Number(f64),
497 String(String),
498 Json(String),
499}
500
501#[derive(Clone, Debug, Eq, PartialEq)]
502pub enum Portal {
503 Location {
504 plane: Option<String>,
505 layer: i32,
506 at: [i64; 2],
507 },
508 Anchor {
509 plane: Option<String>,
510 anchor: String,
511 },
512}
513
514#[derive(Clone, Debug, PartialEq)]
515pub struct Definition {
516 pub fields: Vec<(String, FieldValue)>,
517 pub canonical: String,
519}
520
521pub fn load(root: &Path) -> Result<World, Vec<Diagnostic>> {
526 load_with(root, &LoadOptions::default())
527}
528
529pub fn load_with(root: &Path, options: &LoadOptions) -> Result<World, Vec<Diagnostic>> {
534 load_edited(root, options, &Overlay::new())
535}
536
537pub type Overlay = BTreeMap<String, Option<String>>;
541
542pub fn load_edited(
552 root: &Path,
553 options: &LoadOptions,
554 overlay: &Overlay,
555) -> Result<World, Vec<Diagnostic>> {
556 load_edited_in(&SourceRoot::new(root), options, overlay)
557}
558
559pub fn load_edited_in(
566 root: &SourceRoot,
567 options: &LoadOptions,
568 overlay: &Overlay,
569) -> Result<World, Vec<Diagnostic>> {
570 let options = &LoadOptions {
572 limits: within_format(options.limits),
573 maximum_diagnostics: options.maximum_diagnostics.max(1),
574 ..*options
575 };
576 if overlay.len() > options.maximum_entries {
579 return Err(vec![Diagnostic::file(
580 ".",
581 Code::Budget,
582 format!(
583 "the edits hold more than {} entries",
584 options.maximum_entries
585 ),
586 )]);
587 }
588 if let Some(key) = overlay
591 .keys()
592 .find(|key| key.is_empty() || access::checked(key).is_err())
593 {
594 let shown = if key.len() <= 128 {
595 format!("{key:?}")
596 } else {
597 format!("a path of {} bytes", key.len())
598 };
599 return Err(vec![Diagnostic::file(
600 ".",
601 Code::Path,
602 format!("the edits name {shown}, which is not a path a file of the tree can have"),
603 )]);
604 }
605 let mut diagnostics = Diagnostics::new(options.maximum_diagnostics);
606 let mut tree = files::Tree::in_root(root.platform(), options, overlay, diagnostics.total());
607 let world = load_tree(&mut tree, &mut diagnostics);
608 diagnostics.append(tree.take_diagnostics());
609 match world {
610 Some(world) if diagnostics.is_empty() => Ok(World {
611 source_digest: tree.digest(),
612 ..world
613 }),
614 _ => Err(diagnostics.finish()),
615 }
616}
617
618fn within_format(limits: Limits) -> Limits {
620 let format = Limits::default();
621 Limits {
622 max_input_bytes: limits.max_input_bytes.min(format.max_input_bytes),
623 max_depth: limits.max_depth.min(format.max_depth),
624 max_values: limits.max_values.min(format.max_values),
625 max_array_items: limits.max_array_items.min(format.max_array_items),
626 max_object_keys: limits.max_object_keys.min(format.max_object_keys),
627 max_string_bytes: limits.max_string_bytes.min(format.max_string_bytes),
628 max_number_bytes: limits.max_number_bytes.min(format.max_number_bytes),
629 max_comments: limits.max_comments.min(format.max_comments),
630 max_comment_bytes: limits.max_comment_bytes.min(format.max_comment_bytes),
631 max_lines: limits.max_lines.min(format.max_lines),
632 max_line_bytes: limits.max_line_bytes.min(format.max_line_bytes),
633 }
634}
635
636fn load_tree(tree: &mut files::Tree, diagnostics: &mut Diagnostics) -> Option<World> {
637 let (manifest_path, plane_ids) = profile::read_manifest(tree, diagnostics)?;
638 let (mut profile, transitions_path) = profile::read_profile(tree, &manifest_path, diagnostics)?;
639 let mut references = schema::References::new(&profile);
640 let definitions = defs::read_definitions(tree, &profile, &mut references, diagnostics);
641 if let Some(path) = transitions_path {
644 profile.transitions =
645 transitions::read_transitions(tree, &path, &profile, &references, diagnostics)
646 .map(Arc::new);
647 }
648 let mut planes = BTreeMap::new();
649 for plane in plane::read_planes(tree, &profile, &plane_ids, &mut references, diagnostics) {
650 planes.insert(plane.id.clone(), plane);
651 }
652 let object_uids = uid::index(&planes);
653 Some(World {
654 profile,
655 planes,
656 definitions,
657 object_uids,
658 source_digest: [0; 32],
659 })
660}
661
662#[cfg(test)]
663mod tests;