mod access;
mod chunk;
mod defs;
mod diagnostic;
mod files;
mod plane;
mod profile;
mod read;
mod schema;
mod stairwell;
pub mod transitions;
mod uid;
use std::{collections::BTreeMap, path::Path, sync::Arc};
use tilewright_jsonc::Limits;
pub use access::{
Access, AccessError, Audience, LOCK_NAME, Mounts, SourceDirectory, SourceLock, SourceRoot,
};
use diagnostic::Diagnostics;
pub use diagnostic::{Code, Diagnostic};
pub use files::is_jsonc_path;
pub use read::{is_identifier, is_namespace};
pub use schema::{FieldSpec, FieldType, MAXIMUM_FIELD_DEPTH, MAXIMUM_LIST_ITEMS, Schema};
pub use transitions::{ResolvedChunk, TerrainSource, Transitions};
pub const CHUNK_SIZE: u16 = 32;
pub const MAXIMUM_GROUND_LAYERS: usize = 8;
pub const MAXIMUM_LAYER_HEIGHT: u32 = 65_535;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LoadOptions {
pub limits: Limits,
pub maximum_files: usize,
pub maximum_entries: usize,
pub maximum_total_bytes: u64,
pub maximum_diagnostics: usize,
}
impl Default for LoadOptions {
fn default() -> Self {
Self {
limits: Limits::default(),
maximum_files: 500_000,
maximum_entries: 1_000_000,
maximum_total_bytes: 8 * 1024 * 1024 * 1024,
maximum_diagnostics: 10_000,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct World {
profile: Profile,
planes: BTreeMap<String, Plane>,
definitions: BTreeMap<(String, String), Definition>,
object_uids: BTreeMap<String, ObjectLocation>,
source_digest: [u8; 32],
}
impl World {
#[must_use]
pub const fn profile(&self) -> &Profile {
&self.profile
}
#[must_use]
pub const fn planes(&self) -> &BTreeMap<String, Plane> {
&self.planes
}
#[must_use]
pub const fn definitions(&self) -> &BTreeMap<(String, String), Definition> {
&self.definitions
}
#[must_use]
pub const fn source_digest(&self) -> [u8; 32] {
self.source_digest
}
#[must_use]
pub const fn object_uids(&self) -> &BTreeMap<String, ObjectLocation> {
&self.object_uids
}
#[must_use]
pub fn object_location(&self, uid: &str) -> Option<&ObjectLocation> {
self.object_uids.get(uid)
}
#[must_use]
pub fn object(&self, uid: &str) -> Option<&Object> {
let location = self.object_uids.get(uid)?;
self.planes
.get(&location.plane)?
.chunks
.get(&location.chunk)?
.object(&location.path)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ObjectLocation {
pub plane: String,
pub chunk: (i32, i64, i64),
pub path: Vec<usize>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Profile {
pub id: String,
pub version: String,
pub projection: Projection,
pub render_layers: Vec<String>,
pub ground_layers: Vec<String>,
pub definition_types: BTreeMap<String, Arc<Schema>>,
pub object_fields: Arc<Schema>,
pub zone_properties: Arc<Schema>,
pub layer_height: Option<u32>,
pub movement: Option<Movement>,
pub transitions: Option<Arc<Transitions>>,
pub canonical: String,
}
impl Profile {
#[must_use]
pub fn render_layer_index(&self, name: &str) -> Option<usize> {
self.render_layers.iter().position(|layer| layer == name)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Projection {
Isometric { tile_width: u32, tile_height: u32 },
TopDown { tile_size: u32 },
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Movement {
pub max_step: u32,
pub fall_unit: u32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PlaneKind {
Shared,
Instanced,
PerPlayer,
}
impl PlaneKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Shared => "shared",
Self::Instanced => "instanced",
Self::PerPlayer => "per_player",
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Plane {
pub id: String,
pub title: Option<String>,
pub kind: PlaneKind,
pub chunk_size: u16,
pub default_zone: Option<String>,
pub canonical: String,
pub zones: BTreeMap<String, Zone>,
pub anchors: BTreeMap<String, Anchor>,
pub stairwells: BTreeMap<String, Stairwell>,
pub chunks: BTreeMap<(i32, i64, i64), Chunk>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Zone {
pub id: String,
pub parent: Option<String>,
pub name: Option<String>,
pub canonical: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Anchor {
pub layer: i32,
pub at: [i64; 2],
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Stairwell {
pub id: String,
pub steps: Vec<StairwellStep>,
pub direction: StairwellDirection,
pub hidden_on_other_layers: bool,
pub custom: Option<String>,
pub canonical: String,
}
impl Stairwell {
#[must_use]
pub fn connects(steps: &[StairwellStep]) -> bool {
let [first, second, ..] = steps else {
return false;
};
let up = second.layer > first.layer;
steps.windows(2).all(|pair| pair[0].leads_to(&pair[1], up))
}
#[must_use]
pub fn lower_end(&self) -> Option<&StairwellStep> {
self.steps.iter().min_by_key(|step| step.layer)
}
#[must_use]
pub fn upper_end(&self) -> Option<&StairwellStep> {
self.steps.iter().max_by_key(|step| step.layer)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StairwellStep {
pub layer: i32,
pub at: [i64; 2],
pub proto: String,
}
impl StairwellStep {
#[must_use]
pub fn leads_to(&self, next: &Self, up: bool) -> bool {
adjacent((self.layer, self.at), (next.layer, next.at), up)
}
}
pub(crate) fn adjacent(before: (i32, [i64; 2]), next: (i32, [i64; 2]), up: bool) -> bool {
let change = if up { 1 } else { -1 };
i64::from(next.0) - i64::from(before.0) == change
&& before
.1
.iter()
.zip(next.1)
.all(|(before, after)| (i128::from(*before) - i128::from(after)).abs() <= 1)
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum StairwellDirection {
#[default]
Both,
Down,
Up,
}
impl StairwellDirection {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Both => "both",
Self::Down => "down",
Self::Up => "up",
}
}
#[must_use]
pub fn parse(text: &str) -> Option<Self> {
match text {
"both" => Some(Self::Both),
"down" => Some(Self::Down),
"up" => Some(Self::Up),
_ => None,
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Grid {
pub palette: Vec<String>,
pub cells: Vec<u16>,
}
impl Grid {
#[must_use]
pub fn value(&self, index: usize) -> Option<&str> {
let cell = usize::from(*self.cells.get(index)?);
cell.checked_sub(1)
.and_then(|palette| self.palette.get(palette))
.map(String::as_str)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Chunk {
pub path: String,
pub ground: Vec<Grid>,
pub heights: Vec<i32>,
pub zones: Grid,
pub objects: Vec<Object>,
}
impl Chunk {
#[must_use]
pub fn object(&self, path: &[usize]) -> Option<&Object> {
let (first, rest) = path.split_first()?;
rest.iter()
.try_fold(self.objects.get(*first)?, |object, index| {
object.inventory.get(*index)
})
}
#[must_use]
pub fn depth_first(&self) -> DepthFirst<'_> {
DepthFirst {
levels: vec![self.objects.iter()],
}
}
}
#[derive(Clone, Debug)]
pub struct DepthFirst<'c> {
levels: Vec<std::slice::Iter<'c, Object>>,
}
impl<'c> Iterator for DepthFirst<'c> {
type Item = &'c Object;
fn next(&mut self) -> Option<Self::Item> {
loop {
let level = self.levels.last_mut()?;
if let Some(object) = level.next() {
self.levels.push(object.inventory.iter());
return Some(object);
}
self.levels.pop();
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Object {
pub uid: Option<String>,
pub at: Option<[u16; 2]>,
pub proto: String,
pub layer: Option<String>,
pub sub_layer: i16,
pub z: i32,
pub fields: Vec<(String, FieldValue)>,
pub portal: Option<Portal>,
pub inventory: Vec<Object>,
pub custom: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum FieldValue {
Null,
Bool(bool),
Integer(i64),
Number(f64),
String(String),
Json(String),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Portal {
Location {
plane: Option<String>,
layer: i32,
at: [i64; 2],
},
Anchor {
plane: Option<String>,
anchor: String,
},
}
#[derive(Clone, Debug, PartialEq)]
pub struct Definition {
pub fields: Vec<(String, FieldValue)>,
pub canonical: String,
}
pub fn load(root: &Path) -> Result<World, Vec<Diagnostic>> {
load_with(root, &LoadOptions::default())
}
pub fn load_with(root: &Path, options: &LoadOptions) -> Result<World, Vec<Diagnostic>> {
load_edited(root, options, &Overlay::new())
}
pub type Overlay = BTreeMap<String, Option<String>>;
pub fn load_edited(
root: &Path,
options: &LoadOptions,
overlay: &Overlay,
) -> Result<World, Vec<Diagnostic>> {
load_edited_in(&SourceRoot::new(root), options, overlay)
}
pub fn load_edited_in(
root: &SourceRoot,
options: &LoadOptions,
overlay: &Overlay,
) -> Result<World, Vec<Diagnostic>> {
let options = &LoadOptions {
limits: within_format(options.limits),
maximum_diagnostics: options.maximum_diagnostics.max(1),
..*options
};
if overlay.len() > options.maximum_entries {
return Err(vec![Diagnostic::file(
".",
Code::Budget,
format!(
"the edits hold more than {} entries",
options.maximum_entries
),
)]);
}
if let Some(key) = overlay
.keys()
.find(|key| key.is_empty() || access::checked(key).is_err())
{
let shown = if key.len() <= 128 {
format!("{key:?}")
} else {
format!("a path of {} bytes", key.len())
};
return Err(vec![Diagnostic::file(
".",
Code::Path,
format!("the edits name {shown}, which is not a path a file of the tree can have"),
)]);
}
let mut diagnostics = Diagnostics::new(options.maximum_diagnostics);
let mut tree = files::Tree::in_root(root.platform(), options, overlay, diagnostics.total());
let world = load_tree(&mut tree, &mut diagnostics);
diagnostics.append(tree.take_diagnostics());
match world {
Some(world) if diagnostics.is_empty() => Ok(World {
source_digest: tree.digest(),
..world
}),
_ => Err(diagnostics.finish()),
}
}
fn within_format(limits: Limits) -> Limits {
let format = Limits::default();
Limits {
max_input_bytes: limits.max_input_bytes.min(format.max_input_bytes),
max_depth: limits.max_depth.min(format.max_depth),
max_values: limits.max_values.min(format.max_values),
max_array_items: limits.max_array_items.min(format.max_array_items),
max_object_keys: limits.max_object_keys.min(format.max_object_keys),
max_string_bytes: limits.max_string_bytes.min(format.max_string_bytes),
max_number_bytes: limits.max_number_bytes.min(format.max_number_bytes),
max_comments: limits.max_comments.min(format.max_comments),
max_comment_bytes: limits.max_comment_bytes.min(format.max_comment_bytes),
max_lines: limits.max_lines.min(format.max_lines),
max_line_bytes: limits.max_line_bytes.min(format.max_line_bytes),
}
}
fn load_tree(tree: &mut files::Tree, diagnostics: &mut Diagnostics) -> Option<World> {
let (manifest_path, plane_ids) = profile::read_manifest(tree, diagnostics)?;
let (mut profile, transitions_path) = profile::read_profile(tree, &manifest_path, diagnostics)?;
let mut references = schema::References::new(&profile);
let definitions = defs::read_definitions(tree, &profile, &mut references, diagnostics);
if let Some(path) = transitions_path {
profile.transitions =
transitions::read_transitions(tree, &path, &profile, &references, diagnostics)
.map(Arc::new);
}
let mut planes = BTreeMap::new();
for plane in plane::read_planes(tree, &profile, &plane_ids, &mut references, diagnostics) {
planes.insert(plane.id.clone(), plane);
}
let object_uids = uid::index(&planes);
Some(World {
profile,
planes,
definitions,
object_uids,
source_digest: [0; 32],
})
}
#[cfg(test)]
mod tests;