use core::ffi::c_void;
use core::fmt::Write as _;
use std::sync::{Arc, Mutex};
use crate::abi::{Event, Level, Status, Str};
use crate::config::{ConfigError, Schema, Stage, reason};
use crate::host::Host;
use crate::locks::Relock;
use crate::{Load, Off};
#[derive(Debug, Default)]
struct Kept {
startup: Option<toml::Table>,
restart: Option<toml::Table>,
}
#[derive(Clone, Debug)]
pub struct TableConfig {
host: Host,
schema: Arc<Schema>,
kept: Arc<Mutex<Kept>>,
}
impl Schema {
#[must_use]
pub fn template(&self) -> String {
let mut text = String::new();
comment(&mut text, &self.doc);
for field in &self.fields {
text.push('\n');
comment(&mut text, &field.doc);
text.push_str(match (field.stage, field.synced) {
(_, true) | (Stage::Sync, _) => {
"# The server's value applies to the players who join it.\n"
}
(Stage::Startup, _) => "# Takes effect when the world loads.\n",
(Stage::Restart, _) => "# Takes effect when the game starts again.\n",
_ => "",
});
if !field.default.is_empty() {
let key = super::schema::toml_key(&field.key);
let _written = writeln!(text, "{key} = {}", field.default);
}
}
text
}
fn values_where(
&self,
table: &toml::Table,
select: impl Fn(&super::Field) -> bool,
) -> Option<toml::Table> {
let picked: toml::Table = self
.fields
.iter()
.filter(|f| select(f))
.filter_map(|f| table.get(&f.key).map(|v| (f.key.clone(), v.clone())))
.collect();
(!picked.is_empty()).then_some(picked)
}
fn fill_defaults(&self, table: &mut toml::Table) {
for field in &self.fields {
if table.contains_key(&field.key) || field.default.is_empty() {
continue;
}
let parsed: Result<toml::Table, _> = toml::from_str(&format!("v = {}", field.default));
if let Some(value) = parsed.ok().and_then(|mut t| t.remove("v")) {
table.insert(field.key.clone(), value);
}
}
}
}
impl Load<'_> {
pub fn table_config(&mut self, schema: Schema) -> Result<(TableConfig, toml::Table), Off> {
refuse_unsupported(&schema)?;
let host = self.host();
host.config_schema(&schema.to_text());
let schema = Arc::new(schema);
let user = Arc::into_raw(Arc::clone(&schema))
.cast_mut()
.cast::<c_void>();
let checked = (host.api().config_check)(host.handle(), check_table, user);
if !checked.is_ok() {
drop(unsafe { Arc::from_raw(user.cast_const().cast::<Schema>()) });
}
let settings = TableConfig {
host,
schema,
kept: Arc::new(Mutex::new(Kept::default())),
};
let values = settings.read(|kept, table| {
kept.startup = settings
.schema
.values_where(table, |f| f.stage == Stage::Startup);
kept.restart = settings
.schema
.values_where(table, |f| f.stage == Stage::Restart);
})?;
Ok((settings, values))
}
}
impl TableConfig {
#[must_use]
pub fn schema(&self) -> &Schema {
&self.schema
}
pub fn on_change<F>(&self, load: &mut Load<'_>, handler: F) -> Result<(), Off>
where
F: Fn(toml::Table) + Send + Sync + 'static,
{
let handler = Arc::new(handler);
let on_world = Arc::clone(&handler);
let world = self.clone();
load.on_raw_event(Event::SETTINGS_REPLACED, move |_| {
let read = world.read(|kept, table| {
world.hold(kept.restart.as_ref(), table, Stage::Restart);
kept.startup = world
.schema
.values_where(table, |f| f.stage == Stage::Startup);
});
match read {
Ok(values) => {
on_world(values);
world.host.log(Level::INFO, "the world's settings apply");
}
Err(off) => world.host.log(
Level::WARN,
&format!("the world's settings are skipped: {off}"),
),
}
})?;
let running = self.clone();
load.on_raw_event(Event::SETTINGS_CHANGED, move |_| {
let read = running.read(|kept, table| {
running.hold(kept.restart.as_ref(), table, Stage::Restart);
running.hold(kept.startup.as_ref(), table, Stage::Startup);
});
match read {
Ok(values) => {
handler(values);
running.host.log(Level::INFO, "settings reloaded");
}
Err(off) => running.host.log(
Level::WARN,
&format!("settings change skipped, the running values stay: {off}"),
),
}
})
}
fn read(&self, stages: impl FnOnce(&mut Kept, &mut toml::Table)) -> Result<toml::Table, Off> {
let text = self.host.settings_text(&self.schema.template())?;
let mut table: toml::Table = toml::from_str(&text)
.map_err(|e| Off::new(format!("settings: {}", reason(Some(&text), &e))))?;
for held in self.schema.clamp(&mut table) {
self.host.log(Level::WARN, &format!("settings: {held}"));
}
self.schema.fill_defaults(&mut table);
self.schema
.check(&table)
.map_err(|why| Off::new(format!("settings: {why}")))?;
stages(&mut self.kept.relock(), &mut table);
let local = self
.schema
.values_where(&table, super::Field::follows_server)
.map(|t| inline(&t))
.unwrap_or_default();
let Some(server) = self.host.sync(&local) else {
return Ok(table);
};
match self.with_server(&mut table, &server) {
Ok(()) => self.host.log(
Level::INFO,
&format!("the server's join values apply over the settings file: {server}"),
),
Err(why) => self.host.log(
Level::WARN,
&format!("the server's join values are refused, the file's apply: {why}"),
),
}
Ok(table)
}
fn hold(&self, kept: Option<&toml::Table>, table: &mut toml::Table, stage: Stage) {
let Some(kept) = kept else {
return;
};
let now = self.schema.values_where(table, |f| f.stage == stage);
if now.as_ref() == Some(kept) {
return;
}
let waits = if stage == Stage::Restart {
"the game starts again"
} else {
"the world loads"
};
let changed: Vec<&str> = kept
.iter()
.filter(|(key, value)| now.as_ref().and_then(|n| n.get(*key)) != Some(*value))
.map(|(key, _)| key.as_str())
.collect();
self.host.log(
Level::INFO,
&format!(
"settings that take effect when {waits} changed: {}",
changed.join(", ")
),
);
for (key, value) in kept {
table.insert(key.clone(), value.clone());
}
}
fn with_server(&self, table: &mut toml::Table, server: &str) -> Result<(), ConfigError> {
let text = format!("v = {server}");
let parsed: toml::Table =
toml::from_str(&text).map_err(|e| ConfigError::Parse(reason(Some(&text), &e)))?;
let Some(toml::Value::Table(values)) = parsed.get("v") else {
return Err(ConfigError::Parse(
"the join values are no table".to_owned(),
));
};
let mut merged = table.clone();
for (key, value) in values {
if self
.schema
.field(key)
.is_some_and(super::Field::follows_server)
{
merged.insert(key.clone(), value.clone());
}
}
self.schema.check(&merged)?;
*table = merged;
Ok(())
}
}
fn refuse_unsupported(schema: &Schema) -> Result<(), Off> {
if !schema.groups.is_empty() {
return Err(Off::new("settings: a schema read as a table has no groups"));
}
for field in &schema.fields {
let key = &field.key;
if key.is_empty() {
return Err(Off::new("settings: a field has an empty key"));
}
if key.contains('.') {
return Err(Off::new(format!(
"settings: {key} is a dotted key; a table has top-level keys"
)));
}
if field.secret && field.follows_server() {
return Err(Off::new(format!(
"settings: {key} is secret and would go to every player"
)));
}
if field.synced && field.stage != Stage::Startup {
return Err(Off::new(format!(
"settings: {key} is synced but not startup"
)));
}
}
Ok(())
}
fn comment(text: &mut String, doc: &str) {
for line in doc.lines() {
if line.trim().is_empty() {
text.push_str("#\n");
} else {
let _written = writeln!(text, "# {line}");
}
}
}
fn inline(table: &toml::Table) -> String {
let entries: Vec<String> = table
.iter()
.map(|(key, value)| format!("{} = {value}", super::schema::toml_key(key)))
.collect();
format!("{{ {} }}", entries.join(", "))
}
unsafe extern "system" fn check_table(text: Str, why: *mut Str, user: *mut c_void) -> Status {
crate::host::guard(
"panic in the settings check",
|| Status::REFUSED,
|| {
let Some(schema) = (unsafe { user.cast::<Schema>().as_ref() }) else {
return Status::OK;
};
let text = unsafe { text.as_str() }.unwrap_or("");
let checked = toml::from_str::<toml::Table>(text)
.map_err(|e| ConfigError::Parse(reason(Some(text), &e)))
.and_then(|table| schema.check(&table));
let Err(reason) = checked else {
return Status::OK;
};
crate::load::CHECK_WHY.with_borrow_mut(|kept| {
*kept = reason.to_reason();
if !why.is_null() {
unsafe { why.write(Str::new(kept)) };
}
});
Status::REFUSED
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{Field, Group};
#[test]
fn groups_dotted_keys_and_secrets_that_follow_the_server_are_refused() {
let grouped = Schema::new("", "config", vec![Field::new("a", Stage::Runtime, &1)])
.with_groups(vec![Group::new("g", "")]);
assert!(refuse_unsupported(&grouped).is_err());
let dotted = Schema::new("", "config", vec![Field::new("g.a", Stage::Runtime, &1)]);
assert!(refuse_unsupported(&dotted).is_err());
let empty = Schema::new("", "config", vec![Field::new("", Stage::Runtime, &1)]);
assert!(refuse_unsupported(&empty).is_err());
let secret = Schema::new(
"",
"config",
vec![Field::new("p", Stage::Sync, "x").with_secret(true)],
);
assert!(refuse_unsupported(&secret).is_err());
let synced = Schema::new(
"",
"config",
vec![Field::new("r", Stage::Runtime, &true).with_synced(true)],
);
assert!(refuse_unsupported(&synced).is_err());
let fine = Schema::new(
"",
"config",
vec![Field::new("a", Stage::Startup, &1).with_synced(true)],
);
assert!(refuse_unsupported(&fine).is_ok());
}
}