1use std::collections::BTreeMap;
35use std::io::{self, BufRead, Write};
36use std::path::{Path, PathBuf};
37
38#[derive(Debug, Clone, Copy)]
42pub enum StorageScope<'a> {
43 Global,
45 Player(&'a str),
47 World(&'a str),
49 Entity(&'a str),
51 Chunk(&'a str, i32, i32),
53}
54
55#[derive(Debug, Clone, PartialEq)]
63pub enum Value {
64 Str(String),
65 Int(i64),
66 Float(f64),
67 Bool(bool),
68 Bytes(Vec<u8>),
70}
71
72impl Value {
73 pub fn as_str(&self) -> Option<&str> {
74 if let Value::Str(s) = self { Some(s) } else { None }
75 }
76
77 pub fn as_int(&self) -> Option<i64> {
79 match self {
80 Value::Int(n) => Some(*n),
81 Value::Float(f) => Some(*f as i64),
82 _ => None,
83 }
84 }
85
86 pub fn as_float(&self) -> Option<f64> {
88 match self {
89 Value::Float(f) => Some(*f),
90 Value::Int(n) => Some(*n as f64),
91 _ => None,
92 }
93 }
94
95 pub fn as_bool(&self) -> Option<bool> {
96 if let Value::Bool(b) = self { Some(*b) } else { None }
97 }
98
99 pub fn as_bytes(&self) -> Option<&[u8]> {
100 if let Value::Bytes(b) = self { Some(b) } else { None }
101 }
102}
103
104impl From<String> for Value { fn from(s: String) -> Self { Value::Str(s) } }
105impl From<&str> for Value { fn from(s: &str) -> Self { Value::Str(s.to_string()) } }
106impl From<i64> for Value { fn from(n: i64) -> Self { Value::Int(n) } }
107impl From<i32> for Value { fn from(n: i32) -> Self { Value::Int(n as i64) } }
108impl From<u32> for Value { fn from(n: u32) -> Self { Value::Int(n as i64) } }
109impl From<u64> for Value { fn from(n: u64) -> Self { Value::Int(n as i64) } }
110impl From<usize> for Value { fn from(n: usize) -> Self { Value::Int(n as i64) } }
111impl From<f64> for Value { fn from(f: f64) -> Self { Value::Float(f) } }
112impl From<f32> for Value { fn from(f: f32) -> Self { Value::Float(f as f64) } }
113impl From<bool> for Value { fn from(b: bool) -> Self { Value::Bool(b) } }
114impl From<Vec<u8>> for Value { fn from(b: Vec<u8>) -> Self { Value::Bytes(b) } }
115
116pub struct Storage {
124 path: PathBuf,
125 data: BTreeMap<String, Value>,
126 dirty: bool,
127}
128
129impl Storage {
130 pub fn open(game_dir: &str, mod_id: &str) -> Self {
134 Self::from_path(scope_path(game_dir, mod_id, StorageScope::Global))
135 }
136
137 pub fn open_scoped(game_dir: &str, mod_id: &str, scope: StorageScope<'_>) -> Self {
139 Self::from_path(scope_path(game_dir, mod_id, scope))
140 }
141
142 pub fn open_player(game_dir: &str, mod_id: &str, player_uuid: &str) -> Self {
144 Self::from_path(scope_path(game_dir, mod_id, StorageScope::Player(player_uuid)))
145 }
146
147 pub fn open_world(game_dir: &str, mod_id: &str, dimension: &str) -> Self {
149 Self::from_path(scope_path(game_dir, mod_id, StorageScope::World(dimension)))
150 }
151
152 pub fn open_entity(game_dir: &str, mod_id: &str, entity_uuid: &str) -> Self {
154 Self::from_path(scope_path(game_dir, mod_id, StorageScope::Entity(entity_uuid)))
155 }
156
157 pub fn open_chunk(game_dir: &str, mod_id: &str, dimension: &str, cx: i32, cz: i32) -> Self {
159 Self::from_path(scope_path(game_dir, mod_id, StorageScope::Chunk(dimension, cx, cz)))
160 }
161
162 fn from_path(path: PathBuf) -> Self {
163 let data = load_file(&path);
164 Self { path, data, dirty: false }
165 }
166
167 pub fn get(&self, key: &str) -> Option<&Value> {
170 self.data.get(key)
171 }
172
173 pub fn get_str(&self, key: &str) -> Option<&str> {
174 self.data.get(key)?.as_str()
175 }
176
177 pub fn get_int(&self, key: &str) -> Option<i64> {
178 self.data.get(key)?.as_int()
179 }
180
181 pub fn get_float(&self, key: &str) -> Option<f64> {
182 self.data.get(key)?.as_float()
183 }
184
185 pub fn get_bool(&self, key: &str) -> Option<bool> {
186 self.data.get(key)?.as_bool()
187 }
188
189 pub fn get_bytes(&self, key: &str) -> Option<&[u8]> {
190 self.data.get(key)?.as_bytes()
191 }
192
193 pub fn contains(&self, key: &str) -> bool {
194 self.data.contains_key(key)
195 }
196
197 pub fn set(&mut self, key: impl Into<String>, value: impl Into<Value>) {
204 self.data.insert(key.into(), value.into());
205 self.dirty = true;
206 }
207
208 pub fn remove(&mut self, key: &str) -> Option<Value> {
209 let v = self.data.remove(key);
210 if v.is_some() { self.dirty = true; }
211 v
212 }
213
214 pub fn clear(&mut self) {
215 if !self.data.is_empty() {
216 self.data.clear();
217 self.dirty = true;
218 }
219 }
220
221 pub fn len(&self) -> usize { self.data.len() }
224 pub fn is_empty(&self) -> bool { self.data.is_empty() }
225 pub fn is_dirty(&self) -> bool { self.dirty }
226
227 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
228 self.data.iter().map(|(k, v)| (k.as_str(), v))
229 }
230
231 pub fn path(&self) -> &Path { &self.path }
233
234 pub fn flush(&mut self) -> io::Result<()> {
241 if let Some(parent) = self.path.parent() {
242 std::fs::create_dir_all(parent)?;
243 }
244 let tmp = self.path.with_extension("kv.tmp");
245 {
246 let mut f = std::fs::File::create(&tmp)?;
247 writeln!(f, "# yog-storage v2")?;
248 for (k, v) in &self.data {
249 let (typ, enc) = encode_value(v);
250 writeln!(f, "{}\t{}\t{}", str_escape(k), typ, enc)?;
251 }
252 f.flush()?;
253 }
254 std::fs::rename(&tmp, &self.path)?;
255 self.dirty = false;
256 Ok(())
257 }
258}
259
260impl Drop for Storage {
261 fn drop(&mut self) {
262 if self.dirty {
263 let _ = self.flush();
264 }
265 }
266}
267
268fn scope_path(game_dir: &str, mod_id: &str, scope: StorageScope<'_>) -> PathBuf {
271 let safe_mod = mod_id.replace([':', '/'], "_");
272 let base = Path::new(game_dir).join("yog-data").join(safe_mod);
273 match scope {
274 StorageScope::Global => base.join("global.kv"),
275 StorageScope::Player(uuid) => base.join("player").join(format!("{uuid}.kv")),
276 StorageScope::World(dim) => base.join("world").join(format!("{}.kv", dim_safe(dim))),
277 StorageScope::Entity(uuid) => base.join("entity").join(format!("{uuid}.kv")),
278 StorageScope::Chunk(dim,x,z) => {
279 base.join("chunk").join(format!("{}_{x}_{z}.kv", dim_safe(dim)))
280 }
281 }
282}
283
284fn dim_safe(dim: &str) -> String { dim.replace([':', '/'], "_") }
285
286fn load_file(path: &Path) -> BTreeMap<String, Value> {
287 let file = match std::fs::File::open(path) {
288 Ok(f) => f,
289 Err(_) => return BTreeMap::new(),
290 };
291 let mut map = BTreeMap::new();
292 for line in io::BufReader::new(file).lines() {
293 let Ok(line) = line else { continue };
294 if line.is_empty() || line.starts_with('#') { continue }
295 let mut cols = line.splitn(3, '\t');
296 let (Some(raw_k), Some(typ), Some(raw_v)) =
297 (cols.next(), cols.next(), cols.next()) else { continue };
298 if let Some(v) = parse_value(typ, raw_v) {
299 map.insert(str_unescape(raw_k), v);
300 }
301 }
302 map
303}
304
305fn parse_value(typ: &str, raw: &str) -> Option<Value> {
306 match typ {
307 "s" => Some(Value::Str(str_unescape(raw))),
308 "i" => raw.parse::<i64>().ok().map(Value::Int),
309 "f" => raw.parse::<f64>().ok().map(Value::Float),
310 "b" => Some(Value::Bool(raw == "1")),
311 "x" => Some(Value::Bytes(hex_decode(raw))),
312 _ => None,
313 }
314}
315
316fn encode_value(v: &Value) -> (&'static str, String) {
317 match v {
318 Value::Str(s) => ("s", str_escape(s)),
319 Value::Int(n) => ("i", n.to_string()),
320 Value::Float(f) => ("f", f.to_string()),
321 Value::Bool(b) => ("b", if *b { "1" } else { "0" }.to_string()),
322 Value::Bytes(b) => ("x", hex_encode(b)),
323 }
324}
325
326fn str_escape(s: &str) -> String {
327 let mut out = String::with_capacity(s.len() + 4);
328 for c in s.chars() {
329 match c {
330 '\\' => out.push_str(r"\\"),
331 '\t' => out.push_str(r"\t"),
332 '\n' => out.push_str(r"\n"),
333 '\r' => out.push_str(r"\r"),
334 c => out.push(c),
335 }
336 }
337 out
338}
339
340fn str_unescape(s: &str) -> String {
341 let mut out = String::with_capacity(s.len());
342 let mut chars = s.chars();
343 while let Some(c) = chars.next() {
344 if c == '\\' {
345 match chars.next() {
346 Some('\\') => out.push('\\'),
347 Some('t') => out.push('\t'),
348 Some('n') => out.push('\n'),
349 Some('r') => out.push('\r'),
350 Some(c) => { out.push('\\'); out.push(c); }
351 None => out.push('\\'),
352 }
353 } else {
354 out.push(c);
355 }
356 }
357 out
358}
359
360fn hex_encode(b: &[u8]) -> String {
361 use std::fmt::Write as FmtWrite;
362 b.iter().fold(String::with_capacity(b.len() * 2), |mut s, byte| {
363 let _ = write!(s, "{byte:02x}");
364 s
365 })
366}
367
368fn hex_decode(s: &str) -> Vec<u8> {
369 (0..s.len())
370 .step_by(2)
371 .filter_map(|i| s.get(i..i + 2))
372 .filter_map(|h| u8::from_str_radix(h, 16).ok())
373 .collect()
374}