use super::{FakeHost, FakeTransaction, Installed, table};
use crate::Entity;
use crate::ItemId;
use crate::abi::{PropRemove, Status, TransactionInfo};
use crate::test::FakeChange;
#[cfg(feature = "unstable")]
use crate::world::HeldState;
const REACH: f64 = 2.0;
const FIRST_ENTITY: u32 = 1000;
const STILL: [f32; 4] = [0.0, 0.0, 0.0, 1.0];
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct FakeProp {
pub(crate) entity: Entity,
pub(crate) item: ItemId,
pub(crate) at: [f64; 3],
pub(crate) orientation: [f32; 4],
pub(crate) holds: u32,
pub(crate) settings_changed: bool,
}
impl FakeHost {
#[must_use]
pub fn prop(mut self, entity: Entity, item: ItemId, at: [f64; 3]) -> Self {
self.props.push(FakeProp {
entity,
item,
at,
orientation: STILL,
holds: 0,
settings_changed: false,
});
self
}
#[must_use]
pub fn item_triggers(mut self, item: ItemId) -> Self {
self.trigger_items.push(item.get());
self
}
#[must_use]
pub fn item_grows(mut self, item: ItemId) -> Self {
self.growing_items.push(item.get());
self
}
#[must_use]
pub fn item_not_a_prop(mut self, item: ItemId) -> Self {
self.no_prop_items.push(item.get());
self
}
#[must_use]
pub fn placement_box(mut self, item: ItemId, min: [f32; 3], max: [f32; 3]) -> Self {
self.placement_boxes.push((item.get(), min, max));
self
}
}
#[expect(
clippy::unused_self,
reason = "the fake's state is process-wide; `self` proves it is installed"
)]
impl Installed {
#[must_use]
pub fn props(&self) -> Vec<(Entity, ItemId, [f64; 3])> {
table::with(Vec::new(), |s| {
s.host
.props
.iter()
.map(|prop| (prop.entity, prop.item, prop.at))
.collect()
})
}
pub fn change_trigger_settings(&self, entity: Entity) {
change_start(entity);
}
pub fn grow_plant(&self, entity: Entity) {
change_start(entity);
}
#[cfg(feature = "unstable")]
pub fn set_prop_state(&self, entity: Entity, kinds: &[HeldState]) {
let holds = kinds.iter().fold(0, |bits, kind| bits | kind.bit());
table::with((), |s| {
if let Some(prop) = s.host.props.iter_mut().find(|prop| prop.entity == entity) {
prop.holds = holds;
}
});
}
}
fn change_start(entity: Entity) {
table::with((), |s| {
if let Some(prop) = s.host.props.iter_mut().find(|prop| prop.entity == entity) {
prop.settings_changed = true;
}
});
}
fn turn(rotation: [f32; 4], vector: [f64; 3]) -> [f64; 3] {
let [qx, qy, qz, real] = rotation.map(f64::from);
let axis = [qx, qy, qz];
let cross = |a: [f64; 3], b: [f64; 3]| {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
};
let twice = cross(axis, vector).map(|part| 2.0 * part);
let again = cross(axis, twice);
[0, 1, 2].map(|i| vector[i] + real * twice[i] + again[i])
}
pub(crate) fn reach(
host: &FakeHost,
item: u32,
at: [f64; 3],
orientation: [f32; 4],
) -> ([f64; 3], [f64; 3]) {
let (low, high) = match host.placement_boxes.iter().find(|(of, ..)| *of == item) {
Some(&(_, min, max)) => {
let mut low = [f64::INFINITY; 3];
let mut high = [f64::NEG_INFINITY; 3];
for corner in 0..8 {
let pick = |axis: usize| {
if corner >> axis & 1 == 0 {
f64::from(min[axis])
} else {
f64::from(max[axis])
}
};
let turned = turn(orientation, [pick(0), pick(1), pick(2)]);
for axis in 0..3 {
low[axis] = low[axis].min(turned[axis]);
high[axis] = high[axis].max(turned[axis]);
}
}
(low, high)
}
None => ([-REACH; 3], [REACH; 3]),
};
(
[0, 1, 2].map(|axis| (at[axis] + low[axis]).max(0.0)),
[0, 1, 2].map(|axis| at[axis] + high[axis]),
)
}
fn start_bits(host: &FakeHost, prop: &FakeProp, unknown: bool) -> u32 {
if !unknown && !prop.settings_changed {
return 0;
}
let mut bits = 0;
if host.trigger_items.contains(&prop.item.get()) {
bits |= TransactionInfo::HOLDS_LOGIC_GRAPH_TRIGGER;
}
if host.growing_items.contains(&prop.item.get()) {
bits |= TransactionInfo::HOLDS_GROWTH;
}
bits
}
fn next_entity(host: &FakeHost) -> Entity {
Entity(
host.props
.iter()
.map(|prop| prop.entity.0 + 1)
.max()
.unwrap_or(FIRST_ENTITY)
.max(FIRST_ENTITY),
)
}
pub(crate) fn apply(
host: &mut FakeHost,
transaction: &mut FakeTransaction,
) -> Result<(), (Status, u32)> {
for change in &transaction.changes {
if let FakeChange::RemoveProp(remove) = change {
let prop = host
.props
.iter()
.find(|prop| prop.entity == remove.entity)
.ok_or((Status::NOT_A_PROP, 0))?;
if host.no_prop_items.contains(&prop.item.get()) {
return Err((Status::NOT_A_PROP, 0));
}
let holds = prop.holds | start_bits(host, prop, true);
if holds != 0 && remove.flags & PropRemove::DISCARD_STATE == 0 {
return Err((Status::REFUSED, holds));
}
}
}
for change in &transaction.changes {
match change {
FakeChange::PlaceProp(place) => {
let entity = next_entity(host);
host.props.push(FakeProp {
entity,
item: place.item,
at: place.position,
orientation: place.orientation,
holds: 0,
settings_changed: false,
});
transaction.created.push(entity);
}
FakeChange::RemoveProp(remove) => {
if let Some(index) = host
.props
.iter()
.position(|prop| prop.entity == remove.entity)
{
let mut prop = host.props.remove(index);
prop.holds |= start_bits(host, &prop, true);
transaction.removed.push(prop);
}
}
_ => {}
}
}
Ok(())
}
pub(crate) fn undo(
host: &mut FakeHost,
transaction: &mut FakeTransaction,
) -> Result<(), (Status, u32)> {
for entity in &transaction.created {
let prop = host
.props
.iter()
.find(|prop| prop.entity == *entity)
.ok_or((Status::REFUSED, 0))?;
let holds = prop.holds | start_bits(host, prop, false);
if holds != 0 {
return Err((Status::REFUSED, holds));
}
}
for removed in &transaction.removed {
if removed.holds != 0 {
return Err((Status::REFUSED, removed.holds));
}
let taken = host.props.iter().any(|prop| {
prop.item == removed.item && crate::ecs::fixed(prop.at) == crate::ecs::fixed(removed.at)
});
if taken {
return Err((Status::REFUSED, 0));
}
}
host.props
.retain(|prop| !transaction.created.contains(&prop.entity));
transaction.created.clear();
for prop in transaction.removed.drain(..) {
let entity = next_entity(host);
host.props.push(FakeProp { entity, ..prop });
}
Ok(())
}