use std::collections::{BTreeMap, BTreeSet};
use std::num::NonZeroU32;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::AreaLighting;
use crate::diagnostic::{Diagnostic, DwCode, ExitTier};
use crate::envelope::Campaign;
use crate::ids::{DatumId, EdgeId, FactId, NodeId, ViewId, VolumeId};
use crate::layout::{Edge, LayoutGraphContent, StationKind};
use crate::metrics::{
MAX_JUMP_RISE_16, MetricKind, MetricValue, Metrics, Pitch, Reads, SizeClass, WayClass,
passable_clearance_cells, passable_width_cells,
};
mod check;
mod measure;
mod pack;
mod place;
mod region;
mod seam;
pub use check::*;
use measure::*;
pub use pack::*;
pub use place::*;
use region::*;
pub use seam::*;
crate::dw_code! {
pub const DW_PLAN_AGREEMENT: DwCode = DwCode::new("DW0824", ExitTier::Build);
}
crate::dw_code! {
pub const DW_BOX_OFF_GRID: DwCode = DwCode::new("DW0825", ExitTier::Build);
}
crate::dw_code! {
pub const DW_BOX_LEAVES_REGION: DwCode = DwCode::new("DW0826", ExitTier::Build);
}
crate::dw_code! {
pub const DW_BOXES_OVERLAP: DwCode = DwCode::new("DW0827", ExitTier::Build);
}
crate::dw_code! {
pub const DW_SEAM_NOT_SHARED: DwCode = DwCode::new("DW0828", ExitTier::Build);
}
pub const SHARED_FACE_GAP_CELLS: i64 = 1;
crate::dw_code! {
pub const DW_SEAM_OPENING: DwCode = DwCode::new("DW0829", ExitTier::Build);
}
crate::dw_code! {
pub const DW_STAIR_PITCH: DwCode = DwCode::new("DW0830", ExitTier::Build);
}
crate::dw_code! {
pub const DW_DROP_POLICY: DwCode = DwCode::new("DW0831", ExitTier::Build);
}
crate::dw_code! {
pub const DW_CONTACT: DwCode = DwCode::new("DW0876", ExitTier::Build);
}
crate::dw_code! {
pub const DW_SIZE_CLASS: DwCode = DwCode::new("DW0832", ExitTier::Build);
}
crate::dw_code! {
pub const DW_IDENTITY_FALSE: DwCode = DwCode::new("DW0833", ExitTier::Build);
}
crate::dw_code! {
pub const DW_IDENTITY_EMPTY: DwCode = DwCode::new("DW0834", ExitTier::Build);
}
crate::dw_code! {
pub const DW_VOLUME_IN_BOX: DwCode = DwCode::new("DW0835", ExitTier::Build);
}
crate::dw_code! {
pub const DW_TWO_AUTHORITIES: DwCode = DwCode::new("DW0839", ExitTier::Build);
}
crate::dw_code! {
pub const DW_UNPLACED: DwCode = DwCode::new("DW0883", ExitTier::Build);
}
pub const SITE_AREA: &str = "area/site";
pub const ENTRY_ANCHOR: &str = "spawn";
#[must_use]
pub fn node_anchor(node: &NodeId) -> String {
format!("anchor/node-{}", slug(node.0.as_str()))
}
#[must_use]
pub fn seam_anchor(edge: &EdgeId) -> String {
format!("anchor/seam-{}", slug(edge.0.as_str()))
}
#[must_use]
pub fn seam_unlock_anchor(edge: &EdgeId) -> String {
format!("anchor/unlock-{}", slug(edge.0.as_str()))
}
fn slug(id: &str) -> &str {
id.split_once('/').map_or(id, |(_, rest)| rest)
}
pub const SEAM_BAR: &str = "minecraft:iron_bars";
#[must_use]
pub fn synthesized_gate_block(c: &Campaign, anchor: &str) -> Option<&'static str> {
matches!(
synthesized_anchor_kinds(c).get(anchor),
Some(StationKind::Gate)
)
.then_some(SEAM_BAR)
}
#[must_use]
pub fn synthesized_anchor_kinds(c: &Campaign) -> BTreeMap<String, StationKind> {
let mut out: BTreeMap<String, StationKind> = BTreeMap::new();
if c.site_plan.is_none() {
return out;
}
let Some(graph) = c.layout_graph.as_ref().map(|g| &g.content) else {
return out; };
out.insert(ENTRY_ANCHOR.to_string(), StationKind::Point);
for n in &graph.nodes {
out.insert(node_anchor(&n.id), StationKind::Point);
for s in &n.stations {
out.insert(s.anchor.as_str().to_string(), s.kind);
}
}
for e in &graph.edges {
let Edge::Barred { id, opens_from, .. } = e else {
continue;
};
out.insert(seam_anchor(id), StationKind::Gate);
if !matches!(opens_from, crate::layout::OpensFrom::Either) {
out.insert(seam_unlock_anchor(id), StationKind::Point);
}
}
out
}
#[must_use]
pub fn synthesized_anchors(c: &Campaign) -> BTreeSet<String> {
synthesized_anchor_kinds(c).into_keys().collect()
}
#[must_use]
pub fn owed_anchors(c: &Campaign, node: &NodeId) -> BTreeSet<String> {
let mut out: BTreeSet<String> = BTreeSet::new();
if c.site_plan.is_none() {
return out;
}
let Some(graph) = c.layout_graph.as_ref().map(|g| &g.content) else {
return out; };
let Some(n) = graph.nodes.iter().find(|n| &n.id == node) else {
return out; };
out.insert(node_anchor(node));
if &graph.entry == node {
out.insert(ENTRY_ANCHOR.to_string());
}
for s in &n.stations {
out.insert(s.anchor.as_str().to_string());
}
for e in &graph.edges {
let Edge::Barred { id, opens_from, .. } = e else {
continue;
};
let side = match opens_from {
crate::layout::OpensFrom::A => e.a(),
crate::layout::OpensFrom::B => e.b(),
crate::layout::OpensFrom::Either => continue,
};
if side == node {
out.insert(seam_unlock_anchor(id));
}
}
out
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SitePlanContent {
pub region: WorldBox,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub datums: Vec<Datum>,
pub boxes: Vec<PlanBox>,
pub seams: Vec<Seam>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub volumes: Vec<Volume>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub identities: Vec<Identity>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sightlines: Vec<Sightline>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub views: Vec<View>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lighting: Option<AreaLighting>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorldBox {
pub min: [i64; 3],
pub extent: [NonZeroU32; 3],
}
impl WorldBox {
#[must_use]
pub fn max(&self) -> [i64; 3] {
[
self.min[0] + i64::from(self.extent[0].get()) - 1,
self.min[1] + i64::from(self.extent[1].get()) - 1,
self.min[2] + i64::from(self.extent[2].get()) - 1,
]
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Datum {
pub id: DatumId,
pub y: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub enum Floor {
Datum(DatumId),
Y(i64),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub enum Ceiling {
Clearance(NonZeroU32),
Open,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PlanBox {
pub node: NodeId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min: Option<[i64; 2]>,
pub extent: [NonZeroU32; 2],
pub floor: Floor,
pub ceiling: Ceiling,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub atmosphere: Option<crate::ids::AtmosphereId>,
}
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "kebab-case")]
pub enum Face {
East,
West,
Up,
Down,
South,
North,
}
impl Face {
#[must_use]
pub fn vector(self) -> [i64; 3] {
match self {
Face::East => [1, 0, 0],
Face::West => [-1, 0, 0],
Face::Up => [0, 1, 0],
Face::Down => [0, -1, 0],
Face::South => [0, 0, 1],
Face::North => [0, 0, -1],
}
}
#[must_use]
pub fn is_horizontal_plane(self) -> bool {
matches!(self, Face::Up | Face::Down)
}
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Face::East => "east",
Face::West => "west",
Face::Up => "up",
Face::Down => "down",
Face::South => "south",
Face::North => "north",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum Offset {
Along(i64),
Plane([i64; 2]),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Seam {
pub edge: EdgeId,
pub face: Face,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub at: Option<Offset>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub meets: Option<Offset>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub opening: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contact: Option<Contact>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stair_in: Option<NodeId>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Volume {
pub id: VolumeId,
pub region: WorldBox,
pub role: VolumeRole,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum VolumeRole {
Massif,
Ground,
Clearance,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Identity {
pub fact: FactId,
pub measure: Measure,
pub cmp: Cmp,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "of", rename_all = "kebab-case", deny_unknown_fields)]
pub enum Measure {
RegionExtent {
axis: Axis,
},
BoxExtent {
node: NodeId,
axis: PlanAxis,
},
BoxHeight {
node: NodeId,
},
DistanceXz {
from: NodeId,
to: NodeId,
},
DatumY {
datum: DatumId,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum Axis {
X,
Y,
Z,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum PlanAxis {
X,
Z,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum Cmp {
Eq,
Lt,
Le,
Gt,
Ge,
}
impl Cmp {
fn holds(self, measured: f64, fact: f64) -> bool {
match self {
Cmp::Eq => (measured - fact).abs() < 1e-9,
Cmp::Lt => measured < fact,
Cmp::Le => measured <= fact,
Cmp::Gt => measured > fact,
Cmp::Ge => measured >= fact,
}
}
fn as_str(self) -> &'static str {
match self {
Cmp::Eq => "exactly",
Cmp::Lt => "under",
Cmp::Le => "at most",
Cmp::Gt => "over",
Cmp::Ge => "at least",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Sightline {
pub edge: EdgeId,
pub from: [i64; 3],
pub to: [i64; 3],
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct View {
pub id: ViewId,
pub eye: [i64; 3],
pub look_at: [i64; 3],
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}