enshwick 1.0.0-alpha.6

The Enshwick Loader's modding API for Enshrouded: the one crate mods are written against
Documentation
use core::ffi::c_void;
use std::sync::{Arc, Mutex};

use crate::abi::{InventoryOpResult, InventoryResult};
use crate::engine_enum::Values;
use crate::engine_ids as ids;
use crate::locks::Relock;

use super::ops::BatchError;

/// How a request ended.
///
/// # Examples
///
/// See [`Outcome`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Done {
    /// Every operation succeeded and the transaction was committed.
    Committed,
    /// An operation failed or fell short; nothing changed.
    RolledBack,
    /// This game build lacks a piece, or the loader's hook is not in place;
    /// nothing changed.
    Unavailable,
    /// The game did not run its inventory system in time, as with nobody
    /// online; nothing changed.
    TimedOut,
    /// The batch was refused before it reached the game.
    Refused(BatchError),
}

/// Why an operation failed; the loader reads the game's code for each
/// variant by its constant's name.
///
/// # Examples
///
/// ```
/// use enshwick::inventory::{OpError, Outcome};
///
/// fn split(outcome: &Outcome) -> bool {
///     outcome.ops.iter().any(|op| op.error == Some(OpError::OutOfMemory))
/// }
/// ```
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum OpError {
    /// An operation before it failed, so it did not run; the loader's
    /// name, not the game's.
    NotRun,
    /// The item did not fit or does not exist.
    InvalidState,
    /// The op log is full or more than 16 inventories are touched; split the
    /// batch.
    OutOfMemory,
    /// A code this crate does not name, as the game gave it.
    Other(u32),
}

impl OpError {
    /// Each error with the id of the game's constant for it.
    pub(super) const IDS: [(Self, &'static str); 2] = [
        (Self::InvalidState, ids::INVENTORY_ERROR_INVALID_STATE),
        (Self::OutOfMemory, ids::INVENTORY_ERROR_OUT_OF_MEMORY),
    ];
}

/// The game's code of each [`OpError`] it names, for the running build.
pub(super) type OpErrors = Values<OpError, 2>;

/// The result of one operation.
///
/// # Examples
///
/// See [`Outcome`].
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct OpResult {
    /// Why it failed; `None` when it ran without an error.
    pub error: Option<OpError>,
    /// How much of the count the operation could not do.
    pub remaining: u32,
}

impl OpResult {
    /// Whether the operation ran without an error.
    ///
    /// # Examples
    ///
    /// See [`Outcome`].
    #[must_use]
    pub const fn ok(self) -> bool {
        self.error.is_none()
    }

    fn from_raw(raw: InventoryOpResult, errors: &OpErrors) -> Self {
        let error = match raw.error {
            0 => None,
            InventoryOpResult::NOT_RUN => Some(OpError::NotRun),
            code => Some(
                errors
                    .variant(u64::from(code))
                    .unwrap_or(OpError::Other(code)),
            ),
        };
        Self {
            error,
            remaining: raw.remaining,
        }
    }
}

/// What became of a request, with a result per operation once it ran.
///
/// # Examples
///
/// ```
/// use enshwick::inventory::{Done, Outcome};
///
/// fn report(outcome: &Outcome) {
///     match outcome.done {
///         Done::Committed => enshwick::info!("done"),
///         Done::RolledBack => {
///             let failed = outcome.ops.iter().position(|op| !op.ok() || op.remaining > 0);
///             enshwick::warn!("rolled back at operation {failed:?}");
///         }
///         other => enshwick::warn!("not run: {other:?}"),
///     }
/// }
/// ```
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Outcome {
    /// How it ended.
    pub done: Done,
    /// One per operation when the request ran, else empty.
    pub ops: Vec<OpResult>,
}

impl Outcome {
    /// Whether the change was made.
    ///
    /// # Examples
    ///
    /// See [`Ticket`].
    #[must_use]
    pub const fn is_committed(&self) -> bool {
        matches!(self.done, Done::Committed)
    }

    pub(super) const fn not_run(done: Done) -> Self {
        Self {
            done,
            ops: Vec::new(),
        }
    }

    /// The outcome the loader reported in `result`, its error codes read
    /// with `errors`.
    ///
    /// # Safety
    ///
    /// - `result.ops` is null or points at `op_count` initialised, aligned
    ///   op results, alive for the call.
    pub(super) unsafe fn from_raw(result: &InventoryResult, errors: &OpErrors) -> Self {
        let done = match result.outcome {
            InventoryResult::COMMITTED => Done::Committed,
            InventoryResult::ROLLED_BACK => Done::RolledBack,
            InventoryResult::TIMED_OUT => Done::TimedOut,
            _ => Done::Unavailable,
        };
        let ops = if result.ops.is_null() || result.op_count == 0 {
            &[][..]
        } else {
            // SAFETY: `ops` is not null in this branch, and the caller
            // guarantees `op_count` aligned op results there, alive for the
            // call.
            unsafe { core::slice::from_raw_parts(result.ops, result.op_count as usize) }
        };
        Self {
            done,
            ops: ops
                .iter()
                .map(|&raw| OpResult::from_raw(raw, errors))
                .collect(),
        }
    }
}

/// A submitted request; holds its [`Outcome`] once it is known. Clone it
/// freely.
///
/// # Examples
///
/// ```
/// fn finished(ticket: &enshwick::inventory::Ticket) -> bool {
///     ticket.outcome().is_some_and(|outcome| outcome.is_committed())
/// }
/// ```
#[derive(Clone, Debug, Default)]
pub struct Ticket(Arc<Mutex<Option<Outcome>>>);

impl Ticket {
    /// The outcome, once the request is done.
    ///
    /// # Examples
    ///
    /// See [`Ticket`].
    #[must_use]
    pub fn outcome(&self) -> Option<Outcome> {
        self.0.relock().clone()
    }

    /// Whether the request is done.
    ///
    /// # Examples
    ///
    /// ```
    /// assert!(!enshwick::inventory::Ticket::default().is_done());
    /// ```
    #[must_use]
    pub fn is_done(&self) -> bool {
        self.0.relock().is_some()
    }
}

pub(super) type Callback = Box<dyn FnOnce(&Outcome) + Send>;

/// A request on its way: where its outcome goes.
pub(super) struct Pending {
    pub(super) ticket: Ticket,
    pub(super) done: Callback,
    pub(super) errors: OpErrors,
}

impl Pending {
    pub(super) fn complete(self, outcome: &Outcome) {
        *self.ticket.0.relock() = Some(outcome.clone());
        (self.done)(outcome);
    }
}

/// Completes the request whose `Pending` the loader hands back.
///
/// # Safety
///
/// - `user` is null or the `Box<Pending>` that [`Inventories::submit`] handed
///   over, passed back once.
/// - `result` is null or points at an [`InventoryResult`] readable for the
///   size its header states, with no other writer during the call.
/// - The result's `ops` is null or points at `op_count` aligned op results,
///   alive for the call.
pub(super) unsafe extern "system" fn deliver(result: *const InventoryResult, user: *mut c_void) {
    if user.is_null() {
        return;
    }
    // SAFETY: `user` is not null, as the early return ensures, and the caller
    // guarantees it is the `Box<Pending>` that `submit` leaked, passed back
    // once, so this is its only owner.
    let pending = unsafe { Box::from_raw(user.cast::<Pending>()) };
    // SAFETY: the caller guarantees `result` is null or an `InventoryResult`
    // readable for the size its header states with no concurrent writer,
    // which is the contract of `read_headed`.
    let outcome = unsafe { crate::abi::read_headed(result, size_of::<InventoryResult>()) }
        .map_or_else(
            || Outcome::not_run(Done::Unavailable),
            // SAFETY: the caller guarantees the result's `ops` is null or
            // `op_count` aligned op results alive for the call.
            |r| unsafe { Outcome::from_raw(&r, &pending.errors) },
        );
    crate::host::guard(
        "panic in an inventory callback",
        || (),
        || {
            pending.complete(&outcome);
        },
    );
}