use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::ron_surgery::{fmt_f32, scan_ron_leaves};
use crate::reload_check::Refusal;
pub const REGISTRY_FILE: &str = "data.ron";
pub const REGISTRY_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct Bounds {
#[serde(default)]
pub min: Option<f64>,
#[serde(default)]
pub max: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DataFile {
pub id: String,
pub path: String,
#[serde(default)]
pub note: Option<String>,
#[serde(default)]
pub bounds: BTreeMap<String, Bounds>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DataFiles {
pub version: u32,
pub files: Vec<DataFile>,
#[serde(skip)]
grimoire_dir: PathBuf,
}
impl DataFiles {
pub fn parse(text: &str) -> Result<DataFiles, String> {
let d: DataFiles = ron::Options::default()
.with_default_extension(ron::extensions::Extensions::IMPLICIT_SOME)
.from_str(text)
.map_err(|e| e.to_string())?;
if d.version != REGISTRY_VERSION {
return Err(format!("version {} is not {REGISTRY_VERSION}", d.version));
}
for (i, f) in d.files.iter().enumerate() {
if d.files[..i].iter().any(|g| g.id == f.id) {
return Err(format!("id `{}` is registered twice", f.id));
}
if let Some(name) = std::path::Path::new(&f.path).file_name().and_then(|n| n.to_str())
&& crate::trust::COMMAND_FILES.contains(&name)
{
return Err(format!(
"entry `{}`: `{}` names a program the editor would start, so it cannot be a data file. Tuning values belong in the game's own data files",
f.id, f.path
));
}
}
Ok(d)
}
pub fn load(grimoire_dir: &Path) -> Result<Option<DataFiles>, String> {
let reg = grimoire_dir.join(REGISTRY_FILE);
let text = match std::fs::read_to_string(®) {
Ok(t) => t,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(format!("{}: {e}", reg.display())),
};
let mut d = Self::parse(&text).map_err(|e| format!("{}: {e}", reg.display()))?;
d.grimoire_dir = grimoire_dir.to_path_buf();
for f in &d.files {
let p = d.resolve(f);
if !p.is_file() {
return Err(format!(
"{}: entry `{}`: file `{}` does not exist ({})",
reg.display(),
f.id,
f.path,
p.display()
));
}
}
Ok(Some(d))
}
pub fn get(&self, id: &str) -> Option<&DataFile> {
self.files.iter().find(|f| f.id == id)
}
pub fn resolve(&self, f: &DataFile) -> PathBuf {
self.grimoire_dir.join(&f.path)
}
}
pub fn leaves(text: &str) -> Result<Vec<(String, String)>, String> {
Ok(scan_ron_leaves(text)?
.into_iter()
.map(|l| (l.path, l.text))
.collect())
}
const KIND: &str = "game_data";
fn stopped_at(why: &str) -> Option<String> {
let (_, path) = why.strip_suffix('`')?.rsplit_once(" at `")?;
(!path.is_empty()).then(|| path.to_owned())
}
pub fn read(file: &str, text: &str) -> Result<Vec<(String, String)>, Refusal> {
let refuse = |field, reason: String| Refusal { kind: KIND, file: file.to_owned(), field, reason };
let all = leaves(text).map_err(|why| refuse(stopped_at(&why), why))?;
ron::from_str::<ron::Value>(text).map_err(|e| refuse(None, e.to_string()))?;
Ok(all)
}
fn range(b: &Bounds) -> String {
format!("[{}, {}]", b.min.map_or("-inf".into(), |m| m.to_string()), b.max.map_or("inf".into(), |m| m.to_string()))
}
pub fn check(file: &str, entry: &DataFile, text: &str) -> Result<(), Refusal> {
let all = read(file, text)?;
for (path, b) in &entry.bounds {
let refuse = |reason: String| Refusal { kind: KIND, file: file.to_owned(), field: Some(path.clone()), reason };
let Some((_, token)) = all.iter().find(|(p, _)| p == path) else {
let known: Vec<String> = all.iter().map(|(p, _)| p.clone()).collect();
let near = nearest(path, &known).iter().map(|p| format!("`{p}`")).collect::<Vec<_>>().join(", ");
return Err(refuse(format!("the registry bounds `{path}` and this file has no such leaf; nearest: {near}")));
};
let kind = Kind::of(token);
let x = match (kind, leaf_json(token).and_then(|v| v.as_f64())) {
(Kind::Int | Kind::Float, Some(x)) => x,
_ => return Err(refuse(format!("`{path}` holds {} (`{token}`) and the registry bounds it as a number", kind.name()))),
};
if b.min.is_some_and(|m| x < m) || b.max.is_some_and(|m| x > m) {
return Err(refuse(format!("`{path}` is {token}, outside its bounds {}", range(b))));
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Kind {
Int,
Float,
Str,
Bool,
None,
Variant,
}
impl Kind {
fn of(token: &str) -> Kind {
match token {
"true" | "false" => Kind::Bool,
"None" => Kind::None,
_ if token.starts_with('"') => Kind::Str,
_ if is_int(token) => Kind::Int,
_ if token.parse::<f64>().is_ok() => Kind::Float,
_ => Kind::Variant,
}
}
fn name(self) -> &'static str {
match self {
Kind::Int => "an integer",
Kind::Float => "a float",
Kind::Str => "a string",
Kind::Bool => "a bool",
Kind::None => "`None`",
Kind::Variant => "an enum variant",
}
}
}
fn is_int(t: &str) -> bool {
let u = t.strip_prefix(['-', '+']).unwrap_or(t);
let digits = |s: &str, radix| {
!s.is_empty()
&& s.chars().all(|c| c == '_' || c.is_digit(radix))
&& s.starts_with(|c: char| c.is_digit(radix))
};
match u.get(..2) {
Some("0x") => digits(&u[2..], 16),
Some("0o") => digits(&u[2..], 8),
Some("0b") => digits(&u[2..], 2),
_ => digits(u, 10),
}
}
fn json_kind(v: &serde_json::Value) -> &'static str {
match v {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "a bool",
serde_json::Value::Number(_) => "a number",
serde_json::Value::String(_) => "a string",
serde_json::Value::Array(_) => "an array",
serde_json::Value::Object(_) => "an object",
}
}
fn nearest(path: &str, known: &[String]) -> Vec<String> {
let common = |k: &str| {
path.chars()
.zip(k.chars())
.take_while(|(a, b)| a == b)
.count()
};
let best = known.iter().map(|k| common(k)).max().unwrap_or(0);
if best == 0 {
return known.iter().take(5).cloned().collect();
}
known
.iter()
.filter(|k| common(k) == best)
.take(5)
.cloned()
.collect()
}
pub fn set_leaf(
text: &str,
path: &str,
value: &serde_json::Value,
bounds: Option<&Bounds>,
) -> Result<String, String> {
let all = scan_ron_leaves(text)?;
let Some(leaf) = all.iter().find(|l| l.path == path) else {
let known: Vec<String> = all.iter().map(|l| l.path.clone()).collect();
let near = nearest(path, &known);
return Err(format!(
"`{path}` is not a leaf of this file; nearest: {}",
near.iter()
.map(|p| format!("`{p}`"))
.collect::<Vec<_>>()
.join(", ")
));
};
let kind = Kind::of(&leaf.text);
let mismatch = |why: &str| {
format!(
"`{path}` holds {} (`{}`); {} is {why}",
kind.name(),
leaf.text,
json_kind(value)
)
};
let new = match (kind, value) {
(Kind::Int | Kind::Float, serde_json::Value::Number(n)) => {
let x = n
.as_f64()
.filter(|x| x.is_finite())
.ok_or_else(|| format!("`{path}`: {n} is not a finite number"))?;
if let Some(b) = bounds
&& (b.min.is_some_and(|m| x < m) || b.max.is_some_and(|m| x > m))
{
return Err(format!("`{path}`: {n} is outside its bounds {}", range(b)));
}
if kind == Kind::Int {
if let Some(i) = n.as_i64() {
i.to_string()
} else if let Some(u) = n.as_u64() {
u.to_string()
} else if x.fract() == 0.0 && x.abs() < 9.0e15 {
(x as i64).to_string()
} else {
return Err(format!(
"`{path}` holds an integer (`{}`); {n} is not a whole number",
leaf.text
));
}
} else {
let f = x as f32;
if !f.is_finite() {
return Err(format!("`{path}`: {n} does not fit an f32"));
}
fmt_f32(f)
}
}
(Kind::Str, serde_json::Value::String(s)) => {
ron::to_string(s).map_err(|e| format!("`{path}`: {e}"))?
}
(Kind::Bool, serde_json::Value::Bool(b)) => b.to_string(),
(Kind::None, _) => return Err(mismatch("a change from `None` to a value; refused")),
(Kind::Variant, _) => {
return Err(format!(
"`{path}` holds an enum variant (`{}`); variants are not editable",
leaf.text
));
}
_ => return Err(mismatch("a different kind of value")),
};
let mut out = String::with_capacity(text.len() + new.len());
out.push_str(&text[..leaf.span.start]);
out.push_str(&new);
out.push_str(&text[leaf.span.end..]);
ron::from_str::<ron::Value>(&out)
.map_err(|e| format!("`{path}`: the edited file would not parse: {e}"))?;
Ok(out)
}
pub fn leaf_json(token: &str) -> Option<serde_json::Value> {
match Kind::of(token) {
Kind::Bool => Some(serde_json::Value::Bool(token == "true")),
Kind::Str => ron::from_str::<String>(token).ok().map(serde_json::Value::String),
Kind::Int => ron::from_str::<i64>(token).ok().map(Into::into),
Kind::Float => token
.parse::<f64>()
.ok()
.and_then(serde_json::Number::from_f64)
.map(serde_json::Value::Number),
Kind::None | Kind::Variant => None,
}
}
pub fn restore_leaf(text: &str, path: &str, token: &str) -> Result<String, String> {
let all = scan_ron_leaves(text)?;
let Some(leaf) = all.iter().find(|l| l.path == path) else {
return Err(format!("`{path}` is not a leaf of this file"));
};
if Kind::of(token) != Kind::of(&leaf.text) {
return Err(format!("`{token}` is not a value `{path}` can hold"));
}
let mut out = String::with_capacity(text.len() + token.len());
out.push_str(&text[..leaf.span.start]);
out.push_str(token);
out.push_str(&text[leaf.span.end..]);
ron::from_str::<ron::Value>(&out).map_err(|e| format!("`{path}`: the restored file would not parse: {e}"))?;
Ok(out)
}