use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use crate::Entity;
use crate::abi::{AffixRoll, Header, ItemLine, StatRef, Status, Str};
use crate::game_events::CraftRecipeEvent;
use crate::inventory::Slot;
use crate::item_data::ItemInstance;
use crate::locks::Relock;
use crate::loot::Source;
use crate::stats::Op;
use crate::{Host, Load, Off};
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct AffixInfo {
pub text: String,
pub rarity: ItemRarity,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum RollError {
#[error("the item rolled before")]
Rolled,
#[error("not on the server's game thread")]
WrongThread,
#[error("roll refused (status {0})")]
Refused(u32),
}
#[derive(Clone, Copy, Debug)]
pub struct Affixes {
host: Host,
}
fn fresh_seed(item: ItemInstance) -> u64 {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
#[expect(clippy::cast_possible_truncation, reason = "the low bits seed")]
let now = now as u64;
now ^ (u64::from(item.pide.0) << 32 | u64::from(item.item.get()))
}
impl Affixes {
pub fn roll(
&self,
item: ItemInstance,
rules: RollRules,
seed: Option<u64>,
once: bool,
) -> Result<u32, RollError> {
let request = AffixRoll {
header: Header::of::<AffixRoll>(),
pide: item.pide,
item: item.item,
min: rules.min,
max: rules.max,
max_rarity: rules.max_rarity.to_raw(),
flags: if once { AffixRoll::ONCE } else { 0 },
seed: seed.unwrap_or_else(|| fresh_seed(item)),
};
let mut count = 0;
match (self.host.api().affix_roll)(self.host.handle(), &raw const request, &raw mut count) {
Status::OK => Ok(count),
Status::REFUSED => Err(RollError::Rolled),
Status::WRONG_THREAD => Err(RollError::WrongThread),
other => Err(RollError::Refused(other.0)),
}
}
#[must_use]
pub fn lines(&self, item: ItemInstance) -> Vec<Line> {
let mut out = vec![ItemLine::default(); 16];
loop {
let mut count = 0u32;
let status = (self.host.api().item_lines)(
self.host.handle(),
item.pide,
item.item,
out.as_mut_ptr(),
u32::try_from(out.len()).unwrap_or(u32::MAX),
&raw mut count,
);
if !status.is_ok() {
return Vec::new();
}
let count = count as usize;
if count <= out.len() {
out.truncate(count);
break;
}
out = vec![ItemLine::default(); count];
}
out.iter()
.filter_map(|l| {
Some(Line {
stat: l.modifier.stat,
op: Op::from_raw(l.modifier.op)?,
value: l.modifier.value,
affix: l.affix.checked_sub(1).map(AffixId::new),
})
})
.collect()
}
#[must_use]
pub fn info(&self, affix: AffixId) -> Option<AffixInfo> {
let mut text = Str::EMPTY;
let mut rarity = 0u8;
let status = (self.host.api().affix_info)(
self.host.handle(),
affix.get(),
&raw mut text,
&raw mut rarity,
);
if !status.is_ok() {
return None;
}
let text = unsafe { text.as_str() }?.to_owned();
Some(AffixInfo {
text,
rarity: ItemRarity::from_raw(rarity)?,
})
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct AffixLoot {
pub from: Source,
pub chance: f64,
pub rules: RollRules,
}
impl AffixLoot {
#[must_use]
pub const fn new(from: Source, chance: f64) -> Self {
Self {
from,
chance,
rules: RollRules::new(1, 1),
}
}
#[must_use]
pub const fn count(mut self, min: u8, max: u8) -> Self {
self.rules.min = min;
self.rules.max = max;
self
}
#[must_use]
pub const fn max_rarity(mut self, rarity: ItemRarity) -> Self {
self.rules.max_rarity = rarity;
self
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct AffixCraft {
pub chance: f64,
pub rules: RollRules,
}
impl AffixCraft {
#[must_use]
pub const fn new(chance: f64, rules: RollRules) -> Self {
Self { chance, rules }
}
}
type Crafts = Mutex<HashMap<Entity, (Instant, HashSet<ItemInstance>)>>;
const CRAFT_WINDOW: Duration = Duration::from_secs(1);
const SWEEP: Duration = Duration::from_millis(250);
fn instances(slots: &[Slot]) -> impl Iterator<Item = ItemInstance> + '_ {
slots.iter().filter_map(ItemInstance::of)
}
fn try_roll(affixes: Affixes, item: ItemInstance, chance: f64, rules: RollRules) {
let hit = fastrand::f64() < chance;
let rules = if hit { rules } else { RollRules::new(0, 0) };
let _rolled = affixes.roll(item, rules, None, true);
}
impl Load<'_> {
pub fn define_affix(&mut self, name: &str, def: &AffixDef) -> Result<AffixId, Off> {
def.check()
.map_err(|why| Off::new(format!("affix {name:?}: {why}")))?;
let host = self.host();
let ranges = def.to_ranges();
let data = def.to_data(&ranges);
let mut out = StatRef::default();
let status =
(host.api().affix_open)(host.handle(), Str::new(name), &raw const data, &raw mut out);
crate::load::ok(status, out.index, || format!("affix {name:?}")).map(AffixId::new)
}
#[must_use]
pub fn affixes(&mut self) -> Affixes {
Affixes { host: self.host() }
}
pub fn add_loot_affixes(&mut self, rule: AffixLoot) -> Result<(), Off> {
if !(0.0..=1.0).contains(&rule.chance) {
return Err(Off::new(format!(
"affix loot: chance {} is not between 0 and 1",
rule.chance
)));
}
let affixes = self.affixes();
let inventories = self.inventories()?;
let rolled = crate::loot::watch_rolls(self)?;
let scheduler = self.scheduler()?;
scheduler.every(SWEEP, move || {
let containers: Vec<_> = core::mem::take(&mut *rolled.relock());
for roll in containers.iter().filter(|r| rule.from.matches(r)) {
let Some(container) = roll.entity else {
continue;
};
for item in instances(&inventories.read().slots(container)) {
try_roll(affixes, item, rule.chance, rule.rules);
}
}
core::ops::ControlFlow::Continue(())
});
Ok(())
}
pub fn add_craft_affixes(&mut self, rule: AffixCraft) -> Result<(), Off> {
if !(0.0..=1.0).contains(&rule.chance) {
return Err(Off::new(format!(
"affix craft: chance {} is not between 0 and 1",
rule.chance
)));
}
let affixes = self.affixes();
let inventories = self.inventories()?;
let scheduler = self.scheduler()?;
let pending: Arc<Crafts> = Arc::default();
let requests = Arc::clone(&pending);
self.on::<CraftRecipeEvent>(move |craft| {
let held: HashSet<ItemInstance> =
instances(&inventories.read().slots(craft.crafting_operator_id)).collect();
requests
.relock()
.entry(craft.crafting_operator_id)
.or_insert((Instant::now(), held));
})?;
scheduler.every(SWEEP, move || {
let due: Vec<(Entity, HashSet<ItemInstance>)> = {
let mut pending = pending.relock();
let now = Instant::now();
let ready: Vec<Entity> = pending
.iter()
.filter(|(_, (at, _))| now.duration_since(*at) >= CRAFT_WINDOW)
.map(|(&player, _)| player)
.collect();
ready
.into_iter()
.filter_map(|player| Some((player, pending.remove(&player)?.1)))
.collect()
};
for (player, before) in due {
for item in
instances(&inventories.read().slots(player)).filter(|i| !before.contains(i))
{
try_roll(affixes, item, rule.chance, rule.rules);
}
}
core::ops::ControlFlow::Continue(())
});
Ok(())
}
}
mod defs;
mod roll;
#[cfg(test)]
mod tests;
pub use defs::*;
pub use roll::*;