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
//! What a mod does while the loader initialises it.

use core::ffi::c_void;
use std::borrow::Cow;

use settings::check_text;

use crate::abi::{HostApi, Status, Str};
use crate::config::{Config, ConfigError, Stage};
use crate::host::Host;
use crate::locks::Relock;
use crate::services::{ModEvent, Service};

/// Why a mod turns itself off during initialisation; the loader logs the
/// reason and keeps the game vanilla where the mod would have acted.
///
/// Every error type of this crate turns into an `Off` with `?`, its message
/// the reason.
///
/// # Examples
///
/// ```
/// let off = enshwick::Off::new("this game build is not supported");
/// assert_eq!(off.to_string(), "this game build is not supported");
///
/// fn calls(load: &enshwick::Load<'_>) -> Result<usize, enshwick::Off> {
///     let pattern = enshwick::code::Pattern::parse("E8 ?? ?? ?? ??")?;
///     Ok(pattern.find_all(load.code()?).count())
/// }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{reason}")]
pub struct Off {
    reason: Cow<'static, str>,
    /// The reason as the loader reads a text of the mod's texts, when it is
    /// one.
    text: Option<String>,
}

impl Off {
    /// An `Off` with a reason for the log.
    ///
    /// # Examples
    ///
    /// See [`Off`].
    #[must_use]
    pub fn new(reason: impl Into<Cow<'static, str>>) -> Self {
        Self {
            reason: reason.into(),
            text: None,
        }
    }

    /// An `Off` whose reason is the text of `key` in the mod's texts with
    /// each `{name}` filled from `args`, so the Mods screen, the status
    /// summary and the log show it in the reader's language.
    ///
    /// The mod hands its texts over with [`Load::texts`] first; a reason
    /// whose key has no text shows as the key and its values.
    ///
    /// # Examples
    ///
    /// ```
    /// fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
    ///     let _texts = load.texts("[en]\n\"glow.off\" = \"No glow on this game version.\"\n")?;
    ///     let glow = load.feature("glow", |load| {
    ///         load.symbol("lantern.glow").map_err(|_| enshwick::Off::text("glow.off", &[]))
    ///     });
    ///     let _ = glow;
    ///     Ok(())
    /// }
    ///
    /// let off = enshwick::Off::text("stalls.min", &[("min", "2")]);
    /// assert_eq!(off.to_string(), "stalls.min (min = 2)");
    /// ```
    #[must_use]
    pub fn text(key: &str, args: &[(&str, &str)]) -> Self {
        Self {
            reason: crate::i18n::reason::show(key, args).into(),
            text: Some(crate::i18n::reason::encode(key, args)),
        }
    }

    /// The reason as the loader reads it: a text as its key and values,
    /// any other as its words.
    pub(crate) fn to_reason(&self) -> &str {
        self.text.as_deref().unwrap_or(&self.reason)
    }

    pub(crate) fn status(what: &str, status: Status) -> Self {
        let why = match status {
            Status::MISSING => "missing on this game build",
            Status::AMBIGUOUS => "ambiguous on this game build",
            Status::REFUSED => "refused by the loader",
            Status::BAD_ARGS => "malformed",
            Status::TOO_LATE => "asked for too late",
            Status::FAULTED => "unavailable: the mod has faulted",
            Status::WRONG_THREAD => "called off the thread it serves",
            Status::NOT_LOADED => "not loaded yet",
            Status::OUTSIDE_WORLD => "outside the world",
            Status::STATIC_HALF => "template data, never written",
            Status::NOT_A_PROP => "not a prop",
            Status::NOT_KEPT => "not found again after a load",
            _ => "failed",
        };
        Self::new(format!("{what}: {why}"))
    }
}

/// `From<E> for Off` with `E`'s message as the reason.
macro_rules! off_from {
    ($($error:ty),* $(,)?) => {$(
        impl From<$error> for Off {
            fn from(error: $error) -> Self {
                Self::new(error.to_string())
            }
        }
    )*};
}

off_from!(
    crate::affixes::RollError,
    crate::altars::ResizeError,
    crate::code::FindError,
    crate::code::PatternError,
    crate::content::vanilla::VanillaError,
    crate::inventory::BatchError,
    crate::item_data::SetError,
    crate::migrations::MigrationError,
    crate::net::SendError,
    crate::packs::PackError,
    crate::packs::ReadPackError,
    crate::services::LayoutError,
    crate::stats::ModifyError,
    crate::ui::UiError,
    crate::WorldError,
);

#[cfg(feature = "unstable")]
off_from!(crate::console::RunError, crate::rates::RateError);

impl From<ConfigError> for Off {
    /// `error`'s message as the reason; a [`ConfigError::Text`] stays a text
    /// of the mod's texts, as from [`Off::text`].
    fn from(error: ConfigError) -> Self {
        let text = matches!(error, ConfigError::Text { .. }).then(|| error.to_reason());
        Self {
            reason: error.to_string().into(),
            text,
        }
    }
}

/// A mod: what the loader initialises when it loads the DLL.
///
/// Implement it on the mod's state type and export it with
/// [`declare_mod!`](crate::declare_mod).
///
/// # Examples
///
/// ```
/// struct Quiet;
///
/// impl enshwick::Mod for Quiet {
///     fn manifest() -> &'static enshwick::abi::Manifest {
///         enshwick::manifest!(id: "you.quiet", side: SERVER)
///     }
///
///     fn init(_load: &mut enshwick::Load<'_>) -> Result<Self, enshwick::Off> {
///         Ok(Self)
///     }
/// }
/// ```
pub trait Mod: Sized + Send + Sync + 'static {
    /// What the mod is; static data built with [`manifest!`](crate::manifest).
    fn manifest() -> &'static crate::abi::Manifest;

    /// Registers hooks, services and settings. Nothing takes effect until every
    /// mod is initialised and the loader arms the hooks.
    ///
    /// # Errors
    ///
    /// [`Off`] when the mod cannot run on this game or with these settings.
    fn init(load: &mut Load<'_>) -> Result<Self, Off>;

    /// Runs once the hooks are armed; start background work here.
    fn ready(&'static self, _host: Host) {}

    /// Runs before a development reload unloads the mod (only for a mod
    /// with `reload: true` in its manifest): stop every thread and timer
    /// the mod started and write what the next copy reads back. The loader
    /// has unlinked the mod's hooks and events already.
    fn shutdown(&'static self) {}
}

/// The loader during a mod's initialisation: lookups, hooks, settings and
/// services.
///
/// # Examples
///
/// See [`Mod`].
#[derive(Debug)]
pub struct Load<'a> {
    host: Host,
    /// Whether the mod takes part in the join check, as its manifest says.
    joins: bool,
    _init: core::marker::PhantomData<&'a ()>,
}

impl Load<'_> {
    pub(crate) fn new(host: Host) -> Self {
        Self::of(host, false)
    }

    /// A `Load` for a mod that takes part in the join check when `joins`.
    pub(crate) fn of(host: Host, joins: bool) -> Self {
        Self {
            host,
            joins,
            _init: core::marker::PhantomData,
        }
    }

    /// Whether the mod takes part in the join check, as its manifest says.
    pub(crate) fn joins(&self) -> bool {
        self.joins
    }

    /// The loader's services, also available later through
    /// [`host()`](crate::host()).
    ///
    /// # Examples
    ///
    /// ```
    /// fn init(load: &mut enshwick::Load<'_>) {
    ///     let side = load.host().build().side;
    ///     let _ = side;
    /// }
    /// ```
    #[must_use]
    pub fn host(&self) -> Host {
        self.host
    }

    fn api(&self) -> &'static HostApi {
        self.host.api()
    }

    /// The server's [`World`](crate::world::World): reads, queries and
    /// requests; on a client, the built-in server's while it hosts a game.
    ///
    /// Unstable: needs the `unstable` feature and may change in any release.
    ///
    /// # Errors
    ///
    /// [`Off`] when the game build lacks a type the reads need.
    ///
    /// # Examples
    ///
    /// See [`World`](crate::world::World).
    #[cfg(feature = "unstable")]
    pub fn world(&mut self) -> Result<crate::world::World, Off> {
        crate::world::World::resolve(self)
    }

    /// The map's markers and the players' waypoints in the game server,
    /// dedicated or hosted by the client.
    ///
    /// Unstable: needs the `unstable` feature and may change in any release.
    ///
    /// # Errors
    ///
    /// [`Off`] when the game build lacks the marker reads.
    ///
    /// # Examples
    ///
    /// See [`Map`](crate::map::Map).
    #[cfg(feature = "unstable")]
    pub fn map(&mut self) -> Result<crate::map::Map, Off> {
        crate::map::Map::resolve(self)
    }

    /// The client's fog of war, which the loader copies for the mod from
    /// now on; a mod asks for it during `init`.
    ///
    /// Unstable: needs the `unstable` feature and may change in any release.
    ///
    /// # Errors
    ///
    /// [`Off`] on a dedicated server, which has no fog, or after `init`.
    ///
    /// # Examples
    ///
    /// See [`FogOfWar`](crate::map::FogOfWar).
    #[cfg(feature = "unstable")]
    pub fn fog_of_war(&mut self) -> Result<crate::map::FogOfWar, Off> {
        crate::map::FogOfWar::open(self)
    }

    /// Inventory reads and transactions in the game server, dedicated or
    /// hosted by the client; a mod asks for them during `init`.
    ///
    /// # Errors
    ///
    /// [`Off`] when this game build lacks a piece of them.
    ///
    /// # Examples
    ///
    /// See [`Inventories`](crate::inventory::Inventories).
    pub fn inventories(&mut self) -> Result<crate::inventory::Inventories, Off> {
        crate::inventory::Inventories::resolve(self)
    }

    /// The mod's [`Scheduler`](crate::schedule::Scheduler), which runs work on
    /// the server's game thread on a tick of the default period: the dedicated
    /// server's, or the built-in server's while a client hosts a game.
    ///
    /// # Errors
    ///
    /// [`Off`] when the loader raises no ticks.
    ///
    /// # Examples
    ///
    /// See [`Scheduler`](crate::schedule::Scheduler).
    pub fn scheduler(&mut self) -> Result<crate::schedule::Scheduler, Off> {
        let (scheduler, subscribe) = crate::schedule::Scheduler::get(self.host.handle());
        if subscribe
            && let Err(off) = self.on::<crate::events::Ticked>(move |_| scheduler.run_due())
        {
            scheduler.unsubscribed();
            return Err(off);
        }
        Ok(scheduler)
    }

    /// The mod's own data folder, `mod-data/<id>` beside the game, for state
    /// that outlives a session.
    ///
    /// # Errors
    ///
    /// [`Off`] when the folder cannot be created.
    ///
    /// # Examples
    ///
    /// ```
    /// fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
    ///     let dir = load.data_dir()?;
    ///     assert!(dir.ends_with("my-mod"));
    ///     Ok(())
    /// }
    /// ```
    pub fn data_dir(&self) -> Result<std::path::PathBuf, Off> {
        let mut out = Str::EMPTY;
        let status = (self.api().data_dir)(self.host.handle(), &raw mut out);
        ok(status, (), || "data folder".to_owned())?;
        // SAFETY: the loader keeps the path alive for the process.
        let path = unsafe { out.as_str() }.ok_or_else(|| Off::new("data folder: not UTF-8"))?;
        Ok(std::path::PathBuf::from(path))
    }

    /// A value of type `T` kept per world under the mod's data folder,
    /// opened when the world loads and written when it saves; see
    /// [`WorldStore`](crate::state::WorldStore).
    ///
    /// # Errors
    ///
    /// [`Off`] when `name` is not 1 to 64 of `[a-z0-9_-]`, the data folder
    /// cannot be created or the world events are refused.
    ///
    /// # Examples
    ///
    /// See [`WorldStore`](crate::state::WorldStore).
    pub fn world_store<T>(&mut self, name: &str) -> Result<crate::state::WorldStore<T>, Off>
    where
        T: serde::de::DeserializeOwned + serde::Serialize + Default + Send + 'static,
    {
        let store = crate::state::WorldStore::new(&self.data_dir()?, name)?;
        let host = self.host;
        let opened = store.clone();
        self.on::<crate::events::WorldLoaded>(move |()| match crate::state::world_key(host) {
            Some(key) => {
                if let Err(error) = opened.open(&key) {
                    crate::error!("world store kept shut: {error}");
                }
            }
            None => crate::warn!("world store kept shut: the loader names no world"),
        })?;
        let saved = store.clone();
        self.on::<crate::events::WorldSaved>(move |()| saved.flush_in_background())?;
        let closed = store.clone();
        self.on::<crate::events::ShutdownStarted>(move |()| {
            if let Err(error) = closed.close() {
                crate::error!("{error}");
            }
        })?;
        Ok(store)
    }
}

/// The ABI description of the service `T`, with `table` or null.
fn service_spec<T: Service>(table: *const c_void) -> crate::abi::ServiceSpec {
    crate::abi::ServiceSpec {
        header: crate::abi::Header::of::<crate::abi::ServiceSpec>(),
        name: crate::services::name(T::NAME),
        major: T::MAJOR,
        layout: crate::services::layout(T::LAYOUT, size_of::<T>()),
        table,
    }
}

/// The ABI description of the mod event `E`.
fn event_spec<E: ModEvent>() -> crate::abi::EventSpec {
    crate::abi::EventSpec {
        header: crate::abi::Header::of::<crate::abi::EventSpec>(),
        name: crate::services::name(E::NAME),
        layout: crate::services::layout(E::LAYOUT, size_of::<E>()),
    }
}

/// The `startup` values in force, as an inline table: read in
/// [`Load::config`] and again as each world loads; a change between waits
/// for the next load.
static STARTED: Kept = Kept::new();

/// The `restart` values the mod read in [`Load::config`], as an inline
/// table; later reads keep them until the game starts again.
static RESTART: Kept = Kept::new();

/// Settings values kept for one mod life.
type Kept = crate::fresh::Fresh<std::sync::Mutex<Option<String>>>;

/// Notes `settings`' values at `stage` as the ones in force.
fn keep<T: Config>(kept: &Kept, settings: &T, stage: Stage) {
    *kept.get_or_init(std::sync::Mutex::default).relock() =
        Some(settings.values(stage).unwrap_or_default());
}

std::thread_local! {
    /// The reason a mod's check refused a settings text, kept on the thread
    /// that asked until its next check there: the game thread and the UI
    /// thread may check at once, and each reads its own after the call.
    pub(crate) static CHECK_WHY: core::cell::RefCell<String> = const { core::cell::RefCell::new(String::new()) };
}

impl Load<'_> {
    /// Hands the loader the mod's check of new settings texts, when it
    /// takes one.
    fn config_check<T: Config>(&self) {
        let api = self.api();
        let _checked =
            (api.config_check)(self.host.handle(), check_text::<T>, core::ptr::null_mut());
    }
}

/// Puts the join values `server` over `settings` and checks them; an error
/// leaves `settings` as they were.
fn with_server<T: Config>(settings: &mut T, server: &str) -> Result<(), ConfigError> {
    let own = settings.synced().unwrap_or_default();
    settings.merge_synced(server)?;
    settings.validate().inspect_err(|_| {
        let _back = settings.merge_synced(&own);
    })
}

/// A mid-function hook's handler and where it runs, for the fault message.
struct MidHook<F> {
    address: u64,
    handler: F,
}

/// `value` when `status` is OK, otherwise an [`Off`] naming `what` and why.
pub(crate) fn ok<T>(status: Status, value: T, what: impl FnOnce() -> String) -> Result<T, Off> {
    if status.is_ok() {
        Ok(value)
    } else {
        Err(Off::status(&what(), status))
    }
}

/// Hands `value` to the loader through `register` as its user pointer, which
/// the loader keeps for the life of the process once it accepts it.
pub(crate) fn hand_over<T>(
    value: T,
    register: impl FnOnce(*mut c_void) -> Status,
    what: &str,
) -> Result<(), Off> {
    let user = Box::into_raw(Box::new(value));
    let status = register(user.cast());
    if !status.is_ok() {
        // SAFETY: refused, so the loader keeps no copy of the pointer.
        drop(unsafe { Box::from_raw(user) });
    }
    ok(status, (), || what.to_owned())
}

mod build;
mod code;
mod events;
mod services;
mod settings;
#[cfg(test)]
mod tests;