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
//! Status lines: what a mod reports about itself in the loader's status
//! summary, `logs/enshwick/status.log`, written every few minutes and on
//! `enshwick status` on the server console.
//!
//! The loader fills the summary with what it knows of every mod: version,
//! whether it runs, why it was turned off, its hooks and their calls. A mod
//! adds its own counters with [`Load::add_status`]: a closure returning a few
//! lines, called only when the summary is written, so keeping the numbers
//! (atomics, say) is the only cost while the game runs. A mod made of parts
//! that can fail apart wraps each in [`Load::feature`], so a game build that
//! lacks one part turns off only that part, and players see which.
use core::ffi::c_void;
use std::cell::RefCell;
use crate::abi::Str;
use crate::{Load, Off};
thread_local! {
/// The last status text, alive until the next source runs on this thread.
static TEXT: RefCell<String> = const { RefCell::new(String::new()) };
}
unsafe extern "system" fn run_source<F>(user: *mut c_void, out: *mut Str)
where
F: Fn() -> String,
{
// SAFETY: `user` is the leaked `F` this function was registered with.
let source = unsafe { &*user.cast::<F>() };
let text = crate::host::guard("panic in a status source", String::new, source);
if out.is_null() {
return;
}
TEXT.with_borrow_mut(|kept| {
*kept = text;
// SAFETY: the loader's out pointer; the text lives in this thread's
// buffer until the next source runs here, after the loader copied it.
unsafe { out.write(Str::new(kept)) };
});
}
impl Load<'_> {
/// Adds `source` to the loader's status summary: whenever the summary is
/// written, the loader calls it and shows each line it returns under the
/// mod. It runs on a loader thread or the game thread, never inside a
/// hook, and should only read what the mod keeps, such as atomics.
///
/// # Errors
///
/// [`Off`] when the loader refuses the source, as after init.
///
/// # Examples
///
/// ```
/// use std::sync::atomic::{AtomicU64, Ordering};
///
/// static MERGED: AtomicU64 = AtomicU64::new(0);
///
/// fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
/// load.add_status(|| format!("{} zones merged", MERGED.load(Ordering::Relaxed)))
/// }
/// ```
pub fn add_status<F>(&mut self, source: F) -> Result<(), Off>
where
F: Fn() -> String + Send + Sync + 'static,
{
let host = self.host();
crate::load::hand_over(
source,
|user| (host.api().status)(host.handle(), run_source::<F>, user),
"status source",
)
}
/// Runs `init`, the setup of one optional part of the mod, and reports
/// it to the loader by `name`: on, or off with the reason `init` gave.
/// The log, the status summary and the Mods screen show it, a reason
/// from [`Off::text`] in the reader's language, and the mod keeps
/// running either way.
///
/// Everything registered before `init` fails stays, so `init` looks up
/// what the part needs before it links hooks or adds content.
///
/// # Examples
///
/// ```
/// fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
/// if let Some(glow) = load.feature("glow", |load| load.symbol("lantern.glow")) {
/// let _ = glow;
/// }
/// Ok(())
/// }
/// ```
pub fn feature<T>(
&mut self,
name: &str,
init: impl FnOnce(&mut Self) -> Result<T, Off>,
) -> Option<T> {
let outcome = init(self);
let reason = outcome.as_ref().err().map_or("", Off::to_reason);
let host = self.host();
let _status = (host.api().feature)(host.handle(), Str::new(name), Str::new(reason));
outcome.ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_source_writes_its_lines_and_keeps_them_until_it_runs_again() {
let source = || "3 zones merged\n1 seam".to_owned();
let user = core::ptr::from_ref(&source).cast_mut().cast::<c_void>();
let mut out = Str::EMPTY;
// SAFETY: the source as user and a writable out pointer.
unsafe { run_source_of(&source, user, &raw mut out) };
// SAFETY: valid until the next source on this thread.
assert_eq!(unsafe { out.as_str() }, Some("3 zones merged\n1 seam"));
}
/// Calls `run_source` with the type of `source`.
///
/// # Safety
///
/// - `user` points at `source`, alive for the call.
/// - `out` is null or points at a writable, aligned [`Str`] that no other
/// thread touches during the call.
unsafe fn run_source_of<F: Fn() -> String>(_source: &F, user: *mut c_void, out: *mut Str) {
// SAFETY: `run_source::<F>` needs `user` to be an `F` and `out` null
// or a writable `Str`; this function's `# Safety` bullets give
// `user` pointing at `source`, an `F`, and such an `out`.
unsafe { run_source::<F>(user, out) };
}
}