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 crate::script::{Choice, Error as ScriptError, FromScript, IntoScript, Ty, Typed, Value};
use super::ops::BatchError;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Done {
Committed,
RolledBack,
Unavailable,
TimedOut,
Refused(BatchError),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum OpError {
NotRun,
InvalidState,
OutOfMemory,
Other(u32),
}
impl OpError {
pub(super) const IDS: [(Self, &'static str); 2] = [
(Self::InvalidState, ids::INVENTORY_ERROR_INVALID_STATE),
(Self::OutOfMemory, ids::INVENTORY_ERROR_OUT_OF_MEMORY),
];
}
pub(super) type OpErrors = Values<OpError, 2>;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, crate::Script)]
pub struct OpResult {
pub error: Option<OpError>,
pub remaining: u32,
}
impl OpResult {
#[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,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, crate::Script)]
pub struct Outcome {
pub done: Done,
pub ops: Vec<OpResult>,
}
impl Outcome {
#[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(),
}
}
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 {
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(),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Ticket(Arc<Mutex<Option<Outcome>>>);
impl Ticket {
#[must_use]
pub fn outcome(&self) -> Option<Outcome> {
self.0.relock().clone()
}
#[must_use]
pub fn is_done(&self) -> bool {
self.0.relock().is_some()
}
}
pub(super) type Callback = Box<dyn FnOnce(&Outcome) + Send>;
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);
}
}
pub(super) unsafe extern "system" fn deliver(result: *const InventoryResult, user: *mut c_void) {
if user.is_null() {
return;
}
let pending = unsafe { Box::from_raw(user.cast::<Pending>()) };
let outcome = unsafe { crate::abi::read_headed(result, size_of::<InventoryResult>()) }
.map_or_else(
|| Outcome::not_run(Done::Unavailable),
|r| unsafe { Outcome::from_raw(&r, &pending.errors) },
);
crate::host::guard(
"panic in an inventory callback",
|| (),
|| {
pending.complete(&outcome);
},
);
}
impl Typed for Done {
const TY: &'static Ty = &Ty::Choice(&Choice::new(
"Done",
"How a request ended.",
&[
(
"Committed",
"Every operation succeeded and the transaction was committed.",
),
(
"RolledBack",
"An operation failed or fell short; nothing changed.",
),
(
"Unavailable",
"This game build lacks a piece, or the loader's hook is not in place; nothing changed.",
),
(
"TimedOut",
"The game did not run its inventory system in time; nothing changed.",
),
(
"Refused",
"The batch was refused before it reached the game.",
),
],
));
}
impl IntoScript for Done {
fn into_value(self) -> Value {
Value::String(
match self {
Self::Committed => "Committed",
Self::RolledBack => "RolledBack",
Self::Unavailable => "Unavailable",
Self::TimedOut => "TimedOut",
Self::Refused(_) => "Refused",
}
.to_owned(),
)
}
}
impl FromScript for Done {
fn from_value(value: Value) -> Result<Self, ScriptError> {
let name = String::from_value(value)?;
Ok(match name.as_str() {
"Committed" => Self::Committed,
"RolledBack" => Self::RolledBack,
"Unavailable" => Self::Unavailable,
"TimedOut" => Self::TimedOut,
_ => {
return Err(ScriptError::new(format!(
"{name:?} is not one of \"Committed\", \"RolledBack\", \"Unavailable\", \"TimedOut\""
)));
}
})
}
}
impl Typed for OpError {
const TY: &'static Ty = &Ty::Choice(&Choice::new(
"OpError",
"Why an operation failed.",
&[
(
"NotRun",
"An operation before it failed, so it did not run.",
),
("InvalidState", "The item did not fit or does not exist."),
(
"OutOfMemory",
"The op log is full or more than 16 inventories are touched.",
),
("Other", "A code the API does not name."),
],
));
}
impl IntoScript for OpError {
fn into_value(self) -> Value {
Value::String(
match self {
Self::NotRun => "NotRun",
Self::InvalidState => "InvalidState",
Self::OutOfMemory => "OutOfMemory",
Self::Other(_) => "Other",
}
.to_owned(),
)
}
}
impl FromScript for OpError {
fn from_value(value: Value) -> Result<Self, ScriptError> {
let name = String::from_value(value)?;
Ok(match name.as_str() {
"NotRun" => Self::NotRun,
"InvalidState" => Self::InvalidState,
"OutOfMemory" => Self::OutOfMemory,
_ => {
return Err(ScriptError::new(format!(
"{name:?} is not one of \"NotRun\", \"InvalidState\", \"OutOfMemory\""
)));
}
})
}
}
impl Typed for Ticket {
const TY: &'static Ty = &Ty::Nil;
}
impl IntoScript for Ticket {
fn into_value(self) -> Value {
Value::Nil
}
}