use std::fmt::Write as _;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum ReloadClass {
Live,
Respawn,
Restart,
}
impl ReloadClass {
pub fn word(self) -> &'static str {
match self {
ReloadClass::Live => "Live",
ReloadClass::Respawn => "Respawn",
ReloadClass::Restart => "Restart",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Lifetime {
Global,
PerMap,
}
impl Lifetime {
pub fn word(self) -> &'static str {
match self {
Lifetime::Global => "global",
Lifetime::PerMap => "per map",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Base {
Grimoire,
Root,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FileKind {
pub id: &'static str,
pub base: Base,
pub pattern: &'static str,
pub parses_to: &'static str,
pub class: ReloadClass,
pub lifetime: Lifetime,
pub other_writers: &'static str,
pub note: &'static str,
}
#[allow(clippy::too_many_arguments)]
const fn kind(
id: &'static str,
base: Base,
pattern: &'static str,
parses_to: &'static str,
class: ReloadClass,
lifetime: Lifetime,
other_writers: &'static str,
note: &'static str,
) -> FileKind {
FileKind { id, base, pattern, parses_to, class, lifetime, other_writers, note }
}
use Base::{Grimoire, Root};
use Lifetime::{Global, PerMap};
use ReloadClass::{Live, Respawn, Restart};
pub const KINDS: &[FileKind] = &[
kind("kits", Grimoire, "kits.ron", "Kits", Respawn, Global,
"the project chooser (create, bind, unbind)",
"The editor reopens the whole project; the game's `GrimoireLivePlugin` reloads it whole."),
kind("vocab", Grimoire, "vocab.ron", "Vocabularies", Respawn, Global,
"the project chooser (create), the vocabulary editor",
"The editor re-resolves its libraries in place; the game reloads the project whole."),
kind("library", Grimoire, "<kit>/library.ron", "Library", Respawn, Global,
"the project chooser (create a kit)",
"One per kit. The editor rereads the kit; the game rebuilds the maps that place a changed piece."),
kind("policy", Grimoire, "<kit>/project.ron", "Policy", Respawn, Global,
"the project chooser (create a kit, remove a policy), `policy::remove_patch`",
"One per kit: its exclusions, patches and mounts. Reloads with the kit's library."),
kind("rules", Grimoire, "rules.ron", "Rules", Respawn, Global, "",
"The editor reopens the project; the game revalidates it against the library."),
kind("compositions", Grimoire, "compositions.ron", "Compositions", Respawn, Global, "",
"A stamp expands to rows, so the maps that stamp a changed composition are rebuilt."),
kind("standards", Grimoire, "standards.ron", "Standards", Live, Global,
"by hand",
"Read fresh each time a kit is checked; the editor revalidates it when it changes."),
kind("repetition", Grimoire, "repetition.ron", "RepetitionRules", Live, Global,
"by hand",
"Read fresh on each `repetition_vary` run; the editor revalidates it when it changes."),
kind("checks", Grimoire, "checks.ron", "Checks", Live, Global,
"by hand",
"`Checks::for_project` reads it fresh on every run, so an edit takes effect at once."),
kind("data_registry", Grimoire, "<kit>/data.ron", "DataFiles", Live, Global,
"by hand",
"The registry of a game's own tuning files, read at each `data_set`; nothing holds it."),
kind("rigs", Grimoire, "rigs.ron", "Rigs", Restart, Global,
"the animation tab",
"Read when the project opens and cached; nothing watches it (the guide says so)."),
kind("oplog", Grimoire, "oplog.jsonl", "JSON lines", Live, Global,
"the dispatcher appends a line for every operation",
"Appended and read on request by the log tools; nothing holds it."),
kind("reference_images", Root, "references/<file>", "image bytes", Live, Global,
"the editor, when a person drops an image on a map",
"Copied into the project, where `reference_set` checks that the file exists; nothing watches it and nothing holds it."),
kind("map", Grimoire, "maps/<name>.map.ron", "Map", Respawn, PerMap,
"the project chooser (create, settings, rename)",
"The editor adopts it into the open map, or keeps what it had when it does not parse; the game respawns the placements that changed."),
kind("notes", Grimoire, "maps/<name>.notes.ron", "Notes", Live, PerMap, "",
"The editor rereads it when its modification time moves; the game has no reader."),
kind("plan", Grimoire, "maps/<name>.plan.ron", "Plan", Live, PerMap, "",
"Read on demand by `plan_set` and the plan view."),
kind("references", Grimoire, "maps/<name>.references.ron", "References", Live, PerMap, "",
"Read on demand by `reference_set` and the notes."),
kind("relations", Grimoire, "maps/<name>.relations.ron", "Relations", Live, PerMap,
"by hand",
"Read on demand by `relations_solve`."),
kind("terrain", Grimoire, "<kit>/terrain/<name>.ron", "Heightfield", Respawn, PerMap,
"`terrain_preset` (only in a build with the elements feature)",
"The editor redraws the ground when the file's modification time moves. A file that does not parse is refused with its field named, and what is drawn stays; with nothing drawn yet, no ground is drawn."),
kind("water_plan", Grimoire, "<kit>/water/<name>.ron", "WaterPlan", Live, PerMap, "",
"The plan `water_run` reads; nothing draws it directly."),
kind("water_field", Grimoire, "<kit>/water/<name>.field.ron", "WaterField", Respawn, PerMap, "",
"The editor redraws the water surface when the file's modification time moves. A refused file keeps the water drawn before it."),
kind("volume", Grimoire, "<kit>/volume/<name>.ron", "Volume", Respawn, PerMap, "",
"The editor rebuilds the carved ground when the file's modification time moves. A refused file keeps the ground drawn before it."),
kind("forest", Grimoire, "<kit>/forest/<name>.ron", "Forest", Respawn, PerMap, "",
"The editor respawns the trees when the file's modification time moves."),
kind("people", Grimoire, "<kit>/people/<name>.ron", "People", Live, PerMap, "",
"Read at each `people_run`; nothing holds it."),
kind("scripts", Grimoire, "<kit>/scripts/<name>.ron", "Scripts", Live, PerMap, "",
"The map's triggers, read when they are run; nothing holds it."),
kind("trees", Grimoire, "<kit>/trees/<name>.tree.ron", "TreeParams", Respawn, Global, "",
"A species a forest uses: the editor regrows every tree of it when the file's modification time moves."),
kind("family", Root, "family/<name>.ron", "a family crate's settings", Live, Global, "",
"One file per family crate: the editor applies it in place, polled every 0.1 s. The game-side `GrimoireLivePlugin` does not watch `<root>/family/`."),
kind("family_list", Root, "family.ron", "Family", Live, Global, "by hand",
"Which family crates the project uses, read when the editor needs the list; nothing holds it."),
kind("agent", Root, "agent.ron", "AgentSettings", Live, Global, "by hand",
"The note agent's model and prompt, read each time a note is sent; nothing holds it."),
kind("play", Root, "play.ron", "PlayConfig", Live, Global, "by hand",
"How the editor launches the game, read each time `play` runs; nothing holds it."),
kind("destructible_marks", Root, "destructible.ron", "Marks", Live, Global,
"`destructible_set` (only in a build with the carnage feature)",
"Read at each `destructible_hit`; nothing holds it."),
kind("destructible_bake", Root, "destructible/<name>.bake.ron", "a baked fracture", Live, Global,
"`destructible_set` and `destructible_hit` (only in a build with the carnage feature)",
"A cache the next hit rebuilds when it is absent."),
kind("game_data", Root, "", "the game's own RON", Restart, Global, "",
"Written by `data_set` into files the game owns. This repository ships no reader for them, so a running game is restarted unless it re-reads them itself."),
];
pub fn kind_by_id(id: &str) -> Option<&'static FileKind> {
KINDS.iter().find(|k| k.id == id)
}
fn segment_matches(pattern: &str, segment: &str) -> bool {
let Some(open) = pattern.find('<') else {
return pattern == segment;
};
let Some(close) = pattern[open..].find('>').map(|c| c + open) else {
return false;
};
let (head, tail) = (&pattern[..open], &pattern[close + 1..]);
let Some(rest) = segment.strip_prefix(head).and_then(|s| s.strip_suffix(tail)) else {
return false;
};
let any = &pattern[open..=close] == "<file>";
!rest.is_empty() && (any || rest.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'))
}
impl FileKind {
pub fn matches(&self, path: &str) -> bool {
if self.pattern.is_empty() {
return false;
}
let mut want = self.pattern.split('/');
let mut have = path.split('/');
loop {
match (want.next(), have.next()) {
(None, None) => return true,
(Some(w), Some(h)) if segment_matches(w, h) => {}
_ => return false,
}
}
}
}
pub fn kind_of(base: Base, path: &str) -> Option<&'static FileKind> {
KINDS.iter().find(|k| k.base == base && k.matches(path))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Reload {
InPlace,
Respawn,
NeedsRestart,
NotLoaded,
}
pub fn reload_for(kind: &FileKind, loaded: bool) -> Reload {
if !loaded {
return Reload::NotLoaded;
}
match kind.class {
Live => Reload::InPlace,
Respawn => Reload::Respawn,
Restart => Reload::NeedsRestart,
}
}
pub fn kinds_of_class(class: ReloadClass) -> impl Iterator<Item = &'static FileKind> {
KINDS.iter().filter(move |k| k.class == class)
}
pub fn table(writers: &dyn Fn(&str) -> Vec<String>) -> String {
let mut out = String::new();
out.push_str("| kind | path | parses to | class | lifetime | written by | how a change lands |\n");
out.push_str("|---|---|---|---|---|---|---|\n");
for k in KINDS {
let base = match k.base {
Base::Grimoire => "<grimoire>",
Base::Root => "<root>",
};
let path = if k.pattern.is_empty() {
"(any file registered in `data.ron`)".to_owned()
} else {
format!("`{base}/{}`", k.pattern)
};
let ops = writers(k.id);
let mut by = if ops.is_empty() {
String::new()
} else {
ops.iter().map(|o| format!("`{o}`")).collect::<Vec<_>>().join(", ")
};
if !k.other_writers.is_empty() {
if !by.is_empty() {
by.push_str("; also ");
}
by.push_str(k.other_writers);
}
let _ = writeln!(
out,
"| `{}` | {path} | {} | {} | {} | {} | {} |",
k.id,
k.parses_to,
k.class.word(),
k.lifetime.word(),
by,
k.note
);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_segment_pattern_matches_one_word() {
assert!(segment_matches("<name>.map.ron", "town.map.ron"));
assert!(!segment_matches("<name>.ron", "town.field.ron"));
assert!(!segment_matches("<name>.map.ron", ".map.ron"));
assert!(segment_matches("kits.ron", "kits.ron"));
assert!(!segment_matches("kits.ron", "kits.ron.tmp"));
}
#[test]
fn a_changed_path_is_classified_by_its_pattern() {
let map = kind_of(Base::Grimoire, "maps/town.map.ron").expect("a map is a kind");
assert_eq!((map.id, map.class, map.lifetime), ("map", Respawn, PerMap));
assert_eq!(kind_of(Base::Grimoire, "site/water/lake.field.ron").map(|k| k.id), Some("water_field"));
assert_eq!(kind_of(Base::Grimoire, "site/water/lake.ron").map(|k| k.id), Some("water_plan"));
assert_eq!(kind_of(Base::Grimoire, "site/library.ron").map(|k| k.id), Some("library"));
assert_eq!(kind_of(Base::Root, "references/Sketch v2.JPG").map(|k| k.id), Some("reference_images"));
assert_eq!(kind_of(Base::Root, "family/weather.ron").map(|k| k.id), Some("family"));
assert_eq!(kind_of(Base::Grimoire, "maps/town.map.ron.tmp"), None);
assert_eq!(kind_of(Base::Grimoire, ".live/game.ron"), None);
}
#[test]
fn a_request_for_a_kind_never_loaded_is_reported_not_loaded() {
for k in KINDS {
assert_eq!(reload_for(k, false), Reload::NotLoaded, "{} must not load itself", k.id);
}
let by = |id| reload_for(kind_by_id(id).unwrap(), true);
assert_eq!(by("family"), Reload::InPlace);
assert_eq!(by("map"), Reload::Respawn);
assert_eq!(by("rigs"), Reload::NeedsRestart);
}
}