enshwick 1.0.0-alpha.11

The Enshwick Loader's modding API for Enshrouded: the one crate mods are written against
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
//! Stats: named numbers on entities, such as extra health or a chance to
//! crit, that items, buffs and mods modify, worked out once per change and
//! read for free.
//!
//! A stat kind has a namespaced id (`<mod>:<name>`) and a [`StatDef`]: what
//! it is ([`StatType`]), its default, its range and the text key that names
//! it. A mod defines its own kinds with [`Load::define_stat`] and opens any other,
//! the platform's built-in ones in [`builtin`] among them, with
//! [`Load::open_stat`]; every mod that opens one id shares its values.
//!
//! [`Modifier`]s come from three sources: items an entity wears (their
//! [`ItemStats`]), buffs it has ([`Load::add_buff_modifiers`]) and modifiers a
//! mod keeps on it ([`Stats::set`]). The server sums them with [`aggregate`]
//! after each step in which a source changed, keeps the values, and raises
//! [`Load::on_stats_changed`] for each value that moved; [`Stats::value`]
//! reads a kept value without locks from any thread.
//!
//! The platform applies the built-in kinds to the game itself on the
//! server; a mod's own kinds mean what the mods that read them make of
//! them.
//!
//! # Examples
//!
//! ```
//! use core::time::Duration;
//! use enshwick::events::PlayerJoined;
//! use enshwick::stats::{Lasting, Modifier, Op, StatDef, builtin};
//!
//! fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
//!     let luck = load.define_stat("luck", &StatDef::additive("stat.luck").range(0.0, 1.0))?;
//!     let health = load.open_stat(builtin::HEALTH_MAX)?;
//!     let stats = load.stats()?;
//!     load.on::<PlayerJoined>(move |joined| {
//!         let player = joined.entity;
//!         let minute = Lasting::For(Duration::from_secs(60));
//!         let _health = stats.set(player, "welcome", Modifier::new(health, Op::Add, 50.0), minute);
//!         let _luck = stats.set(player, "welcome", Modifier::new(luck, Op::Add, 0.1), minute);
//!     })
//! }
//! ```

use core::ffi::c_void;

use crate::abi::{
    Header, StatChange as StatChangeData, StatModifierData, StatModify, StatRef, StatSource,
    Status, Str,
};
use crate::buffs::BuffTypeId;
use crate::content::items::StatFormat;
use crate::item_data::{ItemData, ItemInstance, SetError};
use crate::{Entity, ItemId};
use crate::{Host, Load, Off};

/// The item data kind that holds every item's [`ItemStats`].
pub const ITEM_KIND: &str = "enshwick:stats";

/// The key stored for a full stat id: the game id of the id's GUID, as
/// [`Guid::named`](crate::content::Guid::named) derives it.
///
/// # Examples
///
/// ```
/// use enshwick::content::Guid;
/// use enshwick::stats::{StatKey, key_of};
///
/// assert_eq!(key_of("enshwick:health_max"), StatKey(Guid::named("enshwick:health_max").fnv1a()));
/// assert_ne!(key_of("a:b"), StatKey(0));
/// ```
#[must_use]
pub const fn key_of(id: &str) -> StatKey {
    StatKey(crate::content::Guid::hashed(id).fnv1a())
}

/// The platform's built-in stat kinds, which it applies to the game on the
/// server.
///
/// Open one with [`Load::open_stat`]. The maximum kinds add points to the
/// entity's maximum health, stamina and mana, as gear does. Damage kinds are factors on the hits
/// an entity deals, 1 without modifiers; resistances are the part of a
/// hit's damage the entity takes less, 0 without modifiers and 1 for none
/// at all. Both act on hits the entity deals or takes directly; the book's
/// chapter on stats lists what reaches the game.
///
/// # Examples
///
/// ```
/// use enshwick::stats::builtin;
///
/// assert_eq!(builtin::DAMAGE_FIRE, "enshwick:damage_fire");
/// assert!(builtin::ALL.iter().any(|(id, _)| *id == builtin::RESISTANCE));
/// ```
pub mod builtin;

/// A stat whose value changed, for [`Load::on_stats_changed`].
///
/// # Examples
///
/// See [`Load::on_stats_changed`].
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct StatChange {
    /// The entity.
    pub entity: Entity,
    /// The stat.
    pub stat: Stat,
    /// The value before.
    pub old: f32,
    /// The value now.
    pub new: f32,
}

/// What a stat kind is called and how it reads, from [`Stats::info`].
///
/// # Examples
///
/// See [`Stats::info`].
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct StatInfo {
    /// The full id, `<mod>:<name>`.
    pub id: String,
    /// The definition.
    pub def: StatDef,
}

/// The FNV-1a 32 hash of a modifier's key.
const fn key_hash(key: &str) -> u32 {
    key_of(key).0
}

/// Stat values and modifiers; get it from [`Load::stats`]. Copy it anywhere.
///
/// # Examples
///
/// See the [module](self).
#[derive(Clone, Copy, Debug)]
pub struct Stats {
    host: Host,
    items: ItemData<ItemStats>,
}

impl Stats {
    /// The value of `stat` on `entity`: what the server worked out after the
    /// last step a source changed, or the default. Any thread, without
    /// locks; a client answers defaults.
    ///
    /// # Examples
    ///
    /// See the [module](self).
    #[must_use]
    pub fn value(&self, entity: Entity, stat: Stat) -> f32 {
        let mut out = 0.0;
        let status =
            (self.host.api().stat_value)(self.host.handle(), entity, stat.index, &raw mut out);
        if status.is_ok() { out } else { 0.0 }
    }

    /// Whether `stat` is on for `entity`: its value is not 0.
    ///
    /// # Examples
    ///
    /// ```
    /// fn glows(stats: &enshwick::stats::Stats, glow: enshwick::stats::Stat, entity: enshwick::Entity) -> bool {
    ///     stats.is_on(entity, glow)
    /// }
    /// ```
    #[must_use]
    pub fn is_on(&self, entity: Entity, stat: Stat) -> bool {
        self.value(entity, stat) != 0.0
    }

    /// Sets the mod's modifier `key` on `entity` for `modifier.stat`,
    /// replacing the one the mod set before under the same key and stat. The
    /// value follows after the next step. On the server, any thread.
    ///
    /// # Errors
    ///
    /// [`ModifyError`] on a client alone, or for a saved modifier on an
    /// entity that is no player.
    ///
    /// # Examples
    ///
    /// See the [module](self).
    pub fn set(
        &self,
        entity: Entity,
        key: &str,
        modifier: Modifier,
        lasting: Lasting,
    ) -> Result<(), ModifyError> {
        let (flags, duration) = match lasting {
            Lasting::UntilRemoved => (0, 0),
            Lasting::For(d) => (0, u64::try_from(d.as_millis()).unwrap_or(u64::MAX).max(1)),
            Lasting::Saved => (StatModify::SAVED, 0),
        };
        self.modify(entity, key, modifier.to_data(), flags, duration)
    }

    /// Removes the mod's modifier `key` on `entity` for `stat`.
    ///
    /// # Errors
    ///
    /// As for [`Stats::set`].
    ///
    /// # Examples
    ///
    /// ```
    /// fn unbless(stats: &enshwick::stats::Stats, health: enshwick::stats::Stat, player: enshwick::Entity) {
    ///     let _removed = stats.remove(player, "bless", health);
    /// }
    /// ```
    pub fn remove(&self, entity: Entity, key: &str, stat: Stat) -> Result<(), ModifyError> {
        let data = StatModifierData {
            stat: stat.index,
            ..StatModifierData::default()
        };
        self.modify(entity, key, data, StatModify::REMOVE, 0)
    }

    fn modify(
        &self,
        entity: Entity,
        key: &str,
        modifier: StatModifierData,
        flags: u32,
        duration_ms: u64,
    ) -> Result<(), ModifyError> {
        let request = StatModify {
            header: Header::of::<StatModify>(),
            entity,
            key: key_hash(key),
            modifier,
            flags,
            duration_ms,
        };
        match (self.host.api().stat_modify)(self.host.handle(), &raw const request) {
            Status::OK => Ok(()),
            Status::REFUSED if flags & StatModify::SAVED != 0 => Err(ModifyError::NotPlayer),
            Status::REFUSED | Status::WRONG_THREAD => Err(ModifyError::NotServer),
            other => Err(ModifyError::Refused(other.0)),
        }
    }

    /// What `item` gives the entity that wears it; empty for an item without
    /// any. On a client, what the server sent.
    ///
    /// # Examples
    ///
    /// ```
    /// fn bonuses(stats: &enshwick::stats::Stats, item: enshwick::item_data::ItemInstance) -> usize {
    ///     stats.item(item).modifiers.len()
    /// }
    /// ```
    #[must_use]
    pub fn item(&self, item: ItemInstance) -> ItemStats {
        self.items.get(item).unwrap_or_default()
    }

    /// Sets what `item` gives the entity that wears it; an empty
    /// [`ItemStats`] removes it. Clients that run Enshwick get it, and the
    /// entities that wear the item follow after the next step.
    ///
    /// # Errors
    ///
    /// [`SetError`] as for [`ItemData::set`].
    ///
    /// # Examples
    ///
    /// ```
    /// use enshwick::stats::{ItemStats, Modifier, Op};
    ///
    /// fn enchant(stats: &enshwick::stats::Stats, fire: enshwick::stats::Stat, ring: enshwick::item_data::ItemInstance) {
    ///     let mut bonuses = stats.item(ring);
    ///     bonuses.modifiers.push(Modifier::new(fire, Op::Add, 0.1).to_item());
    ///     let _set = stats.set_item(ring, &bonuses);
    /// }
    /// ```
    pub fn set_item(&self, item: ItemInstance, stats: &ItemStats) -> Result<(), SetError> {
        if *stats == ItemStats::default() {
            self.items.remove(item)
        } else {
            self.items.set(item, stats)
        }
    }

    /// Every modifier that counts toward `stat` on `entity`, or toward every
    /// stat for `None`, in the order they apply. On the server, any thread.
    ///
    /// # Examples
    ///
    /// ```
    /// use enshwick::stats::Origin;
    ///
    /// fn from_items(stats: &enshwick::stats::Stats, player: enshwick::Entity) -> usize {
    ///     stats.sources(player, None).iter().filter(|s| matches!(s.origin, Origin::Item(_))).count()
    /// }
    /// ```
    #[must_use]
    pub fn sources(&self, entity: Entity, stat: Option<Stat>) -> Vec<Source> {
        let stat = stat.map_or(u32::MAX, |s| s.index);
        let mut out = vec![StatSource::default(); 32];
        loop {
            let mut count = 0u32;
            let status = (self.host.api().stat_sources)(
                self.host.handle(),
                entity,
                stat,
                out.as_mut_ptr(),
                u32::try_from(out.len()).unwrap_or(u32::MAX),
                &raw mut count,
            );
            let count = count as usize;
            if !status.is_ok() {
                return Vec::new();
            }
            if count <= out.len() {
                out.truncate(count);
                break;
            }
            out = vec![StatSource::default(); count];
        }
        out.iter()
            .filter_map(|s| {
                let origin = match s.kind {
                    StatSource::ITEM => Origin::Item(ItemInstance {
                        pide: Entity(s.id),
                        item: ItemId::new(s.owner),
                    }),
                    StatSource::BUFF => Origin::Buff(BuffTypeId(s.id)),
                    StatSource::ENTITY => Origin::Mod {
                        index: s.owner,
                        key: s.id,
                    },
                    _ => return None,
                };
                Some(Source {
                    origin,
                    stat: s.stat,
                    op: Op::from_raw(s.op)?,
                    value: s.value,
                })
            })
            .collect()
    }

    /// The id and definition of `stat`; `None` for a kind opened but never
    /// defined. Any thread.
    ///
    /// # Examples
    ///
    /// ```
    /// fn name(stats: &enshwick::stats::Stats, stat: enshwick::stats::Stat) -> String {
    ///     stats.info(stat).map_or_else(String::new, |info| info.id)
    /// }
    /// ```
    #[must_use]
    pub fn info(&self, stat: Stat) -> Option<StatInfo> {
        let mut def = StatDef::additive("").to_data();
        let mut id = Str::EMPTY;
        let status =
            (self.host.api().stat_info)(self.host.handle(), stat.index, &raw mut def, &raw mut id);
        if !status.is_ok() {
            return None;
        }
        // SAFETY: `stat_info` returned OK and pointed `def.text` at a text the
        // loader keeps alive and unchanged for the process.
        let def = unsafe { StatDef::from_data(&def) }?;
        // SAFETY: `stat_info` returned OK and pointed `id` at an id the loader
        // keeps alive and unchanged for the process; the id is copied.
        let id = unsafe { id.as_str() }?.to_owned();
        Some(StatInfo { id, def })
    }

    /// Watches `entity` for the items it wears and the buffs it has, or stops
    /// watching it. Players are always watched, and so is every entity a mod
    /// keeps a modifier on; watch other entities whose buffs should count,
    /// such as an enemy a mod curses. Any thread, on the process that runs
    /// the world; elsewhere it does nothing.
    ///
    /// # Examples
    ///
    /// ```
    /// fn curse(stats: &enshwick::stats::Stats, enemy: enshwick::Entity) {
    ///     stats.watch(enemy, true);
    /// }
    /// ```
    pub fn watch(&self, entity: Entity, watch: bool) {
        let _status = (self.host.api().stat_watch)(self.host.handle(), entity, u8::from(watch));
    }
}

/// Calls the stats handler `F` with one change the loader reports; a change
/// shorter than its first version reaches no handler.
///
/// # Safety
///
/// - `change` is null or points at a change the loader wrote, readable for
///   the size its header states; no alignment is needed.
/// - No other thread writes that change during the call.
/// - `user` is the `Box<F>` that [`Load::on_stats_changed`] handed over,
///   which the loader keeps for the process.
unsafe extern "system" fn run_changed<F>(change: *const StatChangeData, user: *mut c_void)
where
    F: Fn(&StatChange),
{
    // SAFETY: the caller guarantees `change` is null or readable for the size
    // its header states, with no concurrent writer.
    let Some(change) = (unsafe { crate::abi::read_headed(change, size_of::<StatChangeData>()) })
    else {
        return;
    };
    // SAFETY: the caller guarantees `user` is the boxed `F` that `hand_over`
    // leaked for the process, so it is aligned, alive and only read through
    // shared references.
    let handler = unsafe { &*user.cast::<F>() };
    let change = StatChange {
        entity: change.entity,
        stat: Stat::from_ref(change.stat),
        old: change.old,
        new: change.new,
    };
    crate::host::guard("panic in a stats handler", || (), || handler(&change));
}

impl Load<'_> {
    fn open_stat_with(&mut self, id: &str, def: Option<&StatDef>) -> Result<Stat, Off> {
        let host = self.host();
        let data = def.map(StatDef::to_data);
        let def_ptr = data.as_ref().map_or(core::ptr::null(), core::ptr::from_ref);
        let mut out = StatRef::default();
        let status = (host.api().stat_open)(host.handle(), Str::new(id), def_ptr, &raw mut out);
        crate::load::ok(status, Stat::from_ref(out), || format!("stat {id:?}"))
    }

    /// Defines the mod's stat kind `name` (`<mod>:<name>`, or the full id in
    /// the mod's namespace), each side 1 to 64 of `[a-z0-9_-]`. Call it
    /// during `init`, on both sides.
    ///
    /// # Errors
    ///
    /// [`Off`] for a malformed id or definition, a kind defined twice, or
    /// after init.
    ///
    /// # Examples
    ///
    /// See the [module](self).
    pub fn define_stat(&mut self, name: &str, def: &StatDef) -> Result<Stat, Off> {
        def.check()
            .map_err(|why| Off::new(format!("stat {name:?}: {why}")))?;
        self.open_stat_with(name, Some(def))
    }

    /// Opens stat kind `id` by its full id, another mod's or a built-in one
    /// from [`builtin`]. A kind no mod defines keeps 0 and reads nothing.
    /// Call it during `init`, on both sides.
    ///
    /// # Errors
    ///
    /// [`Off`] for a malformed id or after init.
    ///
    /// # Examples
    ///
    /// See the [module](self).
    pub fn open_stat(&mut self, id: &str) -> Result<Stat, Off> {
        self.open_stat_with(id, None)
    }

    /// The stat values and modifiers. Call it during `init`.
    ///
    /// # Errors
    ///
    /// [`Off`] after init.
    ///
    /// # Examples
    ///
    /// See the [module](self).
    pub fn stats(&mut self) -> Result<Stats, Off> {
        let host = self.host();
        Ok(Stats {
            host,
            items: self.item_data(ITEM_KIND)?,
        })
    }

    /// Gives every entity that has buff `buff` the `modifiers`, while it has
    /// it: a mod's own buff type, or a vanilla one. Buffs count on watched
    /// entities (see [`Stats::watch`]). Call it during `init`.
    ///
    /// # Errors
    ///
    /// [`Off`] after init.
    ///
    /// # Examples
    ///
    /// ```
    /// use enshwick::buffs::BuffTypeId;
    /// use enshwick::stats::{Modifier, Op, builtin};
    ///
    /// fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
    ///     let fire = load.open_stat(builtin::RESISTANCE_FIRE)?;
    ///     load.add_buff_modifiers(BuffTypeId(0x89cb_0b34), &[Modifier::new(fire, Op::Add, 0.2)])
    /// }
    /// ```
    pub fn add_buff_modifiers(
        &mut self,
        buff: BuffTypeId,
        modifiers: &[Modifier],
    ) -> Result<(), Off> {
        let host = self.host();
        let data: Vec<StatModifierData> = modifiers.iter().map(|m| m.to_data()).collect();
        let status = (host.api().stat_buff)(
            host.handle(),
            buff.0,
            data.as_ptr(),
            u32::try_from(data.len()).unwrap_or(u32::MAX),
        );
        crate::load::ok(status, (), || format!("buff modifiers for {:#x}", buff.0))
    }

    /// Runs `handler` for every stat value that changed on a watched entity,
    /// on the server's game thread after the step that changed it.
    ///
    /// # Errors
    ///
    /// [`Off`] after init.
    ///
    /// # Examples
    ///
    /// ```
    /// fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
    ///     let luck = load.open_stat("other-mod:luck")?;
    ///     load.on_stats_changed(move |change| {
    ///         if change.stat == luck {
    ///             enshwick::info!("{:?} has luck {}", change.entity, change.new);
    ///         }
    ///     })
    /// }
    /// ```
    pub fn on_stats_changed<F>(&mut self, handler: F) -> Result<(), Off>
    where
        F: Fn(&StatChange) + Send + Sync + 'static,
    {
        let host = self.host();
        crate::load::hand_over(
            handler,
            |user| (host.api().stats_changed)(host.handle(), run_changed::<F>, user),
            "stats handler",
        )
    }
}

mod defs;
mod modifiers;
#[cfg(test)]
mod tests;

pub use defs::*;
pub use modifiers::*;