use core::ffi::c_void;
use crate::Entity;
use crate::abi::{
AltarInfo, AltarLimit as RawAltarLimit, AltarZone as RawAltarZone, Bounds,
BuildingZoneCheck as RawBuildingZoneCheck,
};
use crate::{Host, Load, Off};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct CellBox {
pub min: [i64; 3],
pub max: [i64; 3],
}
impl CellBox {
#[must_use]
pub const fn new(min: [i64; 3], max: [i64; 3]) -> Self {
Self { min, max }
}
#[must_use]
pub fn is_empty(&self) -> bool {
(0..3).any(|a| self.min[a] >= self.max[a])
}
#[must_use]
pub fn contains(&self, cell: [i64; 3]) -> bool {
(0..3).all(|a| self.min[a] <= cell[a] && cell[a] < self.max[a])
}
#[must_use]
pub fn touches(&self, other: &Self) -> bool {
!self.is_empty()
&& !other.is_empty()
&& (0..3).all(|a| self.min[a] <= other.max[a] && other.min[a] <= self.max[a])
}
#[must_use]
pub fn union(&self, other: &Self) -> Self {
Self {
min: [0, 1, 2].map(|a| self.min[a].min(other.min[a])),
max: [0, 1, 2].map(|a| self.max[a].max(other.max[a])),
}
}
#[must_use]
pub fn footprint_area(&self) -> i64 {
if self.is_empty() {
return 0;
}
(self.max[0] - self.min[0]) * (self.max[2] - self.min[2])
}
const fn from_bounds(bounds: Bounds) -> Self {
Self {
min: bounds.min,
max: bounds.max,
}
}
}
#[must_use]
pub const fn cell_of_fixed(fixed: [i64; 3]) -> [i64; 3] {
[fixed[0] >> 31, fixed[1] >> 31, fixed[2] >> 31]
}
#[must_use]
#[expect(
clippy::cast_possible_truncation,
reason = "world coordinates fit in i64 cells"
)]
pub fn cell_of_metres(metres: [f64; 3]) -> [i64; 3] {
metres.map(|m| (m * 2.0).floor() as i64)
}
#[derive(Clone, Debug, PartialEq)]
pub struct Altar {
pub entity: Entity,
pub level: u8,
pub tilted: bool,
pub position: [f64; 3],
pub zone: CellBox,
pub levels: Vec<CellBox>,
}
impl Altar {
fn from_info(info: &AltarInfo) -> Self {
let count = usize::from(info.level_count).min(info.levels.len());
Self {
entity: info.entity,
level: info.level,
tilted: info.tilted != 0,
position: info.position,
zone: CellBox::from_bounds(info.zone),
levels: info.levels[..count]
.iter()
.copied()
.map(CellBox::from_bounds)
.collect(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Base {
pub altars: Vec<Entity>,
pub zones: Vec<CellBox>,
pub bounds: CellBox,
}
impl Base {
#[must_use]
pub fn contains(&self, cell: [i64; 3]) -> bool {
self.zones.iter().any(|zone| zone.contains(cell))
}
}
#[must_use]
pub fn bases_of(altars: &[Altar]) -> Vec<Base> {
let mut placed: Vec<&Altar> = altars.iter().filter(|a| !a.zone.is_empty()).collect();
placed.sort_by_key(|a| a.entity.0);
let mut group: Vec<usize> = (0..placed.len()).collect();
for i in 0..placed.len() {
for j in i + 1..placed.len() {
if placed[i].zone.touches(&placed[j].zone) {
let (a, b) = (root(&mut group, i), root(&mut group, j));
group[a.max(b)] = a.min(b);
}
}
}
let mut bases: Vec<(usize, Base)> = Vec::new();
for (i, altar) in placed.iter().enumerate() {
let r = root(&mut group, i);
let at = if let Some(at) = bases.iter().position(|(key, _)| *key == r) {
at
} else {
bases.push((
r,
Base {
bounds: altar.zone,
..Base::default()
},
));
bases.len() - 1
};
let base = &mut bases[at].1;
base.altars.push(altar.entity);
base.zones.push(altar.zone);
base.bounds = base.bounds.union(&altar.zone);
}
bases.into_iter().map(|(_, base)| base).collect()
}
fn root(group: &mut [usize], mut i: usize) -> usize {
while group[i] != i {
group[i] = group[group[i]];
i = group[i];
}
i
}
#[derive(Clone, Copy, Debug)]
pub struct Altars {
host: Host,
}
impl Altars {
#[must_use]
pub fn list(&self) -> Vec<Altar> {
let api = self.host.api();
let entry_size = u32::try_from(size_of::<AltarInfo>()).unwrap_or(u32::MAX);
let mut count = 0u32;
let status = (api.altars)(
self.host.handle(),
core::ptr::null_mut(),
entry_size,
0,
&raw mut count,
);
if !status.is_ok() {
return Vec::new();
}
let mut infos = vec![AltarInfo::EMPTY; count as usize + 8];
let capacity = u32::try_from(infos.len()).unwrap_or(u32::MAX);
let status = (api.altars)(
self.host.handle(),
infos.as_mut_ptr(),
entry_size,
capacity,
&raw mut count,
);
if !status.is_ok() {
return Vec::new();
}
infos.truncate((count as usize).min(infos.len()));
infos.iter().map(Altar::from_info).collect()
}
#[must_use]
pub fn bases(&self) -> Vec<Base> {
bases_of(&self.list())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct AltarZone {
pub altar: Entity,
pub level: u8,
pub grow: i64,
pub height: i64,
}
#[derive(Clone, Copy, Debug)]
pub struct ZoneSizes {
host: Host,
}
impl ZoneSizes {
#[must_use]
pub const fn altars(&self) -> Altars {
Altars { host: self.host }
}
pub fn changed(&self) -> Result<(), ResizeError> {
match (self.host.api().altar_zones_changed)(self.host.handle()) {
crate::abi::Status::OK => Ok(()),
crate::abi::Status::FAULTED => Err(ResizeError::TurnedOff),
other => Err(ResizeError::Refused(other.0)),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ResizeError {
#[error("altar zones not sized again: the mod is turned off")]
TurnedOff,
#[error("altar zones not sized again: refused by the loader (status {0})")]
Refused(u32),
}
unsafe extern "system" fn run_zone<F>(zone: *mut RawAltarZone, user: *mut c_void)
where
F: Fn(&mut AltarZone),
{
let Some(raw) = (unsafe { zone.as_mut() }) else {
return;
};
let mut zone = AltarZone {
altar: raw.altar,
level: raw.level,
grow: raw.grow,
height: raw.height,
};
let filter = unsafe { &*user.cast::<F>() };
let returned = crate::host::guard(
"panic in an altar zone filter",
|| false,
|| {
filter(&mut zone);
true
},
);
if returned {
raw.grow = zone.grow;
raw.height = zone.height;
}
}
pub const MAX_ZONES: usize = 256;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct AltarLimit {
pub flame_level: u32,
pub vanilla: u8,
pub allowed: u8,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct BaseLimit {
pub zones: u32,
pub bases: u32,
pub vanilla_zones: u32,
pub vanilla_bases: u32,
pub max_zones: u32,
pub max_bases: u32,
}
impl BaseLimit {
#[must_use]
pub const fn past_vanilla(&self, grouping: Grouping) -> bool {
grouping.zones > self.vanilla_zones || grouping.bases > self.vanilla_bases
}
}
#[must_use]
pub fn base_limit() -> Option<BaseLimit> {
let host = crate::host()?;
let mut out = crate::abi::BaseLimitInfo {
header: crate::abi::Header::of::<crate::abi::BaseLimitInfo>(),
zones: 0,
bases: 0,
vanilla_zones: 0,
reserved: 0,
vanilla_bases: 0,
max_zones: 0,
max_bases: 0,
};
let status = (host.api().base_limit)(host.handle(), &raw mut out);
status.is_ok().then_some(BaseLimit {
zones: out.zones,
bases: out.bases,
vanilla_zones: out.vanilla_zones,
vanilla_bases: out.vanilla_bases,
max_zones: out.max_zones,
max_bases: out.max_bases,
})
}
#[derive(Clone, Copy, Debug)]
pub struct BuildingZoneCheck<'a> {
pub zones: &'a [Bounds],
pub object: Bounds,
pub margin: i64,
}
unsafe extern "system" fn run_building_zone_check<F>(
check: *const RawBuildingZoneCheck,
user: *mut c_void,
) -> u8
where
F: Fn(&BuildingZoneCheck<'_>) -> bool,
{
let Some(check) = (unsafe { check.as_ref() }) else {
return 0;
};
let zones = if check.zones.is_null() || check.zone_count == 0 {
&[][..]
} else {
unsafe { core::slice::from_raw_parts(check.zones, check.zone_count) }
};
let filter = unsafe { &*user.cast::<F>() };
let call = BuildingZoneCheck {
zones,
object: check.object,
margin: check.margin,
};
u8::from(crate::host::guard(
"panic in a zone check",
|| false,
|| filter(&call),
))
}
impl Load<'_> {
pub fn altars(&mut self) -> Result<Altars, Off> {
let host = self.host();
let status = (host.api().altars_open)(host.handle());
crate::load::ok(status, Altars { host }, || "altars".to_owned())
}
pub fn on_altar_zone<F>(&mut self, filter: F) -> Result<ZoneSizes, Off>
where
F: Fn(&mut AltarZone) + Send + Sync + 'static,
{
let host = self.host();
crate::load::hand_over(
filter,
|user| (host.api().altar_zone_filter)(host.handle(), run_zone::<F>, user),
"the altar zone filter",
)?;
Ok(ZoneSizes { host })
}
pub fn on_building_zone_check<F>(&mut self, filter: F) -> Result<(), Off>
where
F: Fn(&BuildingZoneCheck<'_>) -> bool + Send + Sync + 'static,
{
let host = self.host();
crate::load::hand_over(
filter,
|user| {
(host.api().building_zone_check_filter)(
host.handle(),
run_building_zone_check::<F>,
user,
)
},
"building zone checks",
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Grouping {
pub zones: u32,
pub bases: u32,
}
#[must_use]
pub fn grouping() -> Option<Grouping> {
let host = crate::host()?;
let (mut zones, mut bases) = (0u32, 0u32);
let status = (host.api().base_zones)(host.handle(), &raw mut zones, &raw mut bases);
status.is_ok().then_some(Grouping { zones, bases })
}
unsafe extern "system" fn run_limit<F>(limit: *mut RawAltarLimit, user: *mut c_void)
where
F: Fn(&mut AltarLimit),
{
let Some(raw) = (unsafe { limit.as_mut() }) else {
return;
};
let mut limit = AltarLimit {
flame_level: raw.flame_level,
vanilla: raw.vanilla,
allowed: raw.allowed,
};
let filter = unsafe { &*user.cast::<F>() };
let returned = crate::host::guard(
"panic in an altar limit",
|| false,
|| {
filter(&mut limit);
true
},
);
if returned {
raw.allowed = limit.allowed;
}
}
impl Load<'_> {
pub fn on_altar_limit<F>(&mut self, filter: F) -> Result<(), Off>
where
F: Fn(&mut AltarLimit) + Send + Sync + 'static,
{
let host = self.host();
crate::load::hand_over(
filter,
|user| (host.api().altar_limit_filter)(host.handle(), run_limit::<F>, user),
"the altar limit filter",
)
}
pub fn raise_base_limit(&mut self) -> Result<bool, Off> {
let host = self.host();
armed((host.api().raise_base_limit)(host.handle()))
}
#[cfg(feature = "unstable")]
pub fn request_base_limits(&mut self, zones: u32, bases: u32) -> Result<bool, Off> {
let host = self.host();
armed((host.api().request_base_limits)(
host.handle(),
zones,
bases,
))
.map_err(|off| match base_limit() {
Some(most) if zones > most.max_zones || bases > most.max_bases => Off::new(format!(
"{zones} altar zones in {bases} bases asked for; the most is {} in {}",
most.max_zones, most.max_bases
)),
_ => off,
})
}
}
fn armed(status: crate::abi::Status) -> Result<bool, Off> {
if status.is_ok() {
Ok(true)
} else if status == crate::abi::Status::MISSING {
Ok(false)
} else {
Err(Off::status("the base limit", status))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn altar(entity: u32, min: [i64; 3], max: [i64; 3]) -> Altar {
Altar {
entity: Entity(entity),
level: 1,
tilted: false,
position: [0.0; 3],
zone: CellBox::new(min, max),
levels: Vec::new(),
}
}
#[test]
fn touching_zones_form_one_base_through_a_chain() {
let bases = bases_of(&[
altar(3, [20, 0, 0], [30, 10, 10]),
altar(1, [0, 0, 0], [10, 10, 10]),
altar(2, [10, 0, 0], [20, 10, 10]),
altar(4, [100, 0, 0], [110, 10, 10]),
]);
assert_eq!(bases.len(), 2);
assert_eq!(bases[0].altars, vec![Entity(1), Entity(2), Entity(3)]);
assert_eq!(bases[0].bounds, CellBox::new([0, 0, 0], [30, 10, 10]));
assert_eq!(bases[1].altars, vec![Entity(4)]);
}
#[test]
fn an_altar_without_a_zone_forms_no_base() {
let mut tilted = altar(1, [0; 3], [0; 3]);
tilted.tilted = true;
assert!(bases_of(&[tilted]).is_empty());
}
#[test]
fn cells_round_down() {
assert_eq!(cell_of_metres([-0.1, 0.0, 0.49]), [-1, 0, 0]);
assert_eq!(cell_of_fixed([-1, 0, (1 << 31) - 1]), [-1, 0, 0]);
}
}