enshwick 1.0.0-alpha.4

The Enshwick Loader's modding API for Enshrouded: the one crate mods are written against
Documentation
use crate::abi::{Header, InventoryOp, InventoryOpResult, InventoryRequest, InventoryResult};

use super::*;

fn all(from: u32, to: u32) -> Op {
    Op::MoveAll {
        from: Entity(from),
        to: Entity(to),
        include_special: false,
    }
}

fn give(to: u32) -> Op {
    Op::Give {
        to: Entity(to),
        item: 7,
        count: 1,
        fill: None,
    }
}

#[test]
fn slot_refs_pack_the_sub_inventory_low_and_the_index_high() {
    let slot = SlotRef::new(Entity(u32::MAX), 7);
    assert_eq!(slot.0, 0x7_ffff_ffff);
    assert_eq!(slot.inventory(), Entity(u32::MAX));
    assert_eq!(slot.index(), 7);
    assert_eq!(SlotRef(0x41).index(), 0);
}

#[test]
fn batches_are_checked_against_the_game_limits() {
    assert_eq!(Batch::new().check(), Err(BatchError::Empty));
    assert_eq!(
        Batch::new().with(all(5, 5)).check(),
        Err(BatchError::Malformed)
    );
    let renew = Op::Renew {
        slot: SlotRef::new(Entity(41), 2),
        root: Entity(40),
        item: 7,
        count: 1,
        fill: Fill::FULL,
    };
    assert_eq!(Batch::new().with(renew).check(), Ok(()));
    let raw = renew.raw();
    assert_eq!(
        (raw.kind, raw.target, raw.other & 0xffff_ffff),
        (InventoryOp::RENEW, SlotRef::new(Entity(41), 2).0, 40)
    );
    assert_eq!(raw.other >> 32, u64::from(1.0f32.to_bits()));
    assert_eq!(
        Batch::new()
            .with(Op::Renew {
                slot: SlotRef::new(Entity(41), 2),
                root: Entity::NONE,
                item: 7,
                count: 1,
                fill: Fill::FULL,
            })
            .check(),
        Err(BatchError::Malformed)
    );
    let zero = Op::Give {
        to: Entity(2),
        item: 7,
        count: 0,
        fill: None,
    };
    assert_eq!(Batch::new().with(zero).check(), Err(BatchError::Malformed));
    let mut many = Batch::new();
    for _ in 0..=MAX_OPS {
        many.push(Op::DestroySlot(SlotRef(0x41)));
    }
    assert_eq!(many.check(), Err(BatchError::TooManyOps));
    let mut wide = Batch::new();
    for to in 1..=17 {
        wide.push(give(to));
    }
    assert_eq!(wide.check(), Err(BatchError::TooManyInventories));
    let heavy = Batch::new().with(all(1, 2)).with(all(3, 2)).with(give(2));
    assert_eq!(heavy.check(), Err(BatchError::TooLarge));
    assert_eq!(Batch::new().with(all(1, 2)).with(all(3, 2)).check(), Ok(()));
}

#[test]
fn splitting_keeps_order_and_every_part_fits() {
    let mut big = Batch::new().allow_partial().actor(Entity(2));
    for from in 100..105 {
        big.push(all(from, 2));
    }
    for to in 200..230 {
        big.push(give(to));
    }
    let ops = big.ops().to_vec();
    let parts = big.split();
    assert!(parts.len() > 3);
    assert!(parts.iter().all(|p| p.check().is_ok()));
    assert!(
        parts
            .iter()
            .all(|p| p.allow_partial && p.actor == Entity(2))
    );
    let joined: Vec<Op> = parts.iter().flat_map(|p| p.ops().to_vec()).collect();
    assert_eq!(joined, ops);
    assert_eq!(Batch::new().with(give(2)).split().len(), 1);
    assert!(Batch::new().split().is_empty());
}

#[test]
fn requests_notify_for_gives_and_credit_the_first_destination() {
    let loot = Batch::new().with(all(40, 2));
    assert_eq!(loot.request_flags(), 0);
    assert_eq!(
        loot.clone().notify().request_flags(),
        InventoryRequest::NOTIFY
    );
    assert_eq!(loot.actor_entity(), Entity(2));
    let paid = Batch::new()
        .with(Op::Take {
            from: Entity(3),
            item: 1,
            count: 1,
        })
        .with(give(3))
        .allow_partial();
    assert_eq!(
        paid.request_flags(),
        InventoryRequest::ALLOW_PARTIAL | InventoryRequest::NOTIFY
    );
    assert_eq!(paid.actor_entity(), Entity(3));
    assert_eq!(loot.actor(Entity(9)).actor_entity(), Entity(9));
}

#[test]
fn operations_become_the_loaders_records() {
    let slot = SlotRef::new(Entity(0x41), 2);
    let raw = Op::MoveSlot {
        slot,
        to: Entity(2),
        amount: Some(3),
    }
    .raw();
    assert_eq!(
        (raw.kind, raw.target, raw.other, raw.count),
        (InventoryOp::MOVE_SLOT, slot.0, 2, 3)
    );
    let special = Op::MoveAll {
        from: Entity(40),
        to: Entity(2),
        include_special: true,
    }
    .raw();
    assert_eq!(special.flags, InventoryOp::INCLUDE_SPECIAL);
}

#[test]
fn outcomes_read_the_loaders_result() {
    let ops = [
        InventoryOpResult {
            error: 0,
            remaining: 0,
        },
        InventoryOpResult {
            error: 0x15,
            remaining: 4,
        },
    ];
    let raw = InventoryResult {
        header: Header::of::<InventoryResult>(),
        outcome: InventoryResult::ROLLED_BACK,
        op_count: 2,
        ops: ops.as_ptr(),
    };
    // SAFETY: `raw.ops` points at the two results of `ops`, alive here.
    let outcome = unsafe { Outcome::from_raw(&raw) };
    assert_eq!(outcome.done, Done::RolledBack);
    assert!(!outcome.is_committed());
    assert_eq!(
        outcome.ops[1],
        OpResult {
            error: OpResult::NO_ROOM,
            remaining: 4
        }
    );
    let empty = InventoryResult {
        outcome: InventoryResult::TIMED_OUT,
        op_count: 0,
        ops: core::ptr::null(),
        ..raw
    };
    // SAFETY: `empty.ops` is null.
    let outcome = unsafe { Outcome::from_raw(&empty) };
    assert_eq!(outcome, Outcome::not_run(Done::TimedOut));
}