steel_core/chunk_saver/
registry.rs1use std::path::{Path, PathBuf};
4
5use rustc_hash::FxHashMap;
6use serde::Deserialize;
7use steel_utils::Identifier;
8
9use crate::config::{StorageSelection, WorldStorageConfig, validate_relative_path};
10
11pub struct WorldStorageOutput {
13 pub storage: WorldStorageConfig,
15 pub level_data_path: Option<PathBuf>,
17}
18
19struct WorldStorageFactory {
20 validate: fn(&toml::Value) -> Result<(), String>,
21 create: fn(&toml::Value, &Path, &Path) -> Result<WorldStorageOutput, String>,
22}
23
24pub struct WorldStorageRegistry {
26 factories: FxHashMap<Identifier, WorldStorageFactory>,
27}
28
29impl WorldStorageRegistry {
30 pub fn new_with_builtins() -> Result<Self, String> {
32 let mut registry = Self {
33 factories: FxHashMap::default(),
34 };
35 registry.register(
36 Identifier::from_steel("disk"),
37 WorldStorageFactory {
38 validate: validate_disk_config,
39 create: create_disk_storage,
40 },
41 )?;
42 registry.register(
43 Identifier::from_steel("ram"),
44 WorldStorageFactory {
45 validate: validate_empty_config,
46 create: create_ram_storage,
47 },
48 )?;
49 Ok(registry)
50 }
51
52 fn register(&mut self, key: Identifier, factory: WorldStorageFactory) -> Result<(), String> {
53 if self.factories.insert(key.clone(), factory).is_some() {
54 return Err(format!("duplicate world storage registration {key}"));
55 }
56 Ok(())
57 }
58
59 pub fn validate_selection(&self, selection: &StorageSelection) -> Result<(), String> {
61 let factory = self
62 .factories
63 .get(&selection.kind)
64 .ok_or_else(|| format!("unknown world storage {}", selection.kind))?;
65 (factory.validate)(&selection.config_value())
66 }
67
68 pub fn create(
70 &self,
71 selection: &StorageSelection,
72 save_root: &Path,
73 default_world_path: &Path,
74 ) -> Result<WorldStorageOutput, String> {
75 let factory = self
76 .factories
77 .get(&selection.kind)
78 .ok_or_else(|| format!("unknown world storage {}", selection.kind))?;
79 (factory.create)(&selection.config_value(), save_root, default_world_path)
80 }
81}
82
83#[derive(Deserialize)]
84#[serde(deny_unknown_fields)]
85struct DiskStorageConfig {
86 path: Option<String>,
87}
88
89fn validate_disk_config(config: &toml::Value) -> Result<(), String> {
90 let parsed: DiskStorageConfig = config
91 .clone()
92 .try_into()
93 .map_err(|e| format!("invalid steel:disk config: {e}"))?;
94 if let Some(path) = parsed.path {
95 validate_relative_path(&path, "storage.config.path")?;
96 }
97 Ok(())
98}
99
100fn validate_empty_config(config: &toml::Value) -> Result<(), String> {
101 let Some(table) = config.as_table() else {
102 return Err("storage config must be a table".to_owned());
103 };
104 if !table.is_empty() {
105 return Err("this storage backend does not accept config".to_owned());
106 }
107 Ok(())
108}
109
110fn create_disk_storage(
111 config: &toml::Value,
112 save_root: &Path,
113 default_world_path: &Path,
114) -> Result<WorldStorageOutput, String> {
115 let parsed: DiskStorageConfig = config
116 .clone()
117 .try_into()
118 .map_err(|e| format!("invalid steel:disk config: {e}"))?;
119 let path = parsed.path.map_or_else(
120 || default_world_path.to_path_buf(),
121 |path| save_root.join(path),
122 );
123 Ok(WorldStorageOutput {
124 storage: WorldStorageConfig::Disk {
125 path: path_to_string(path.join("region")),
126 },
127 level_data_path: Some(path),
128 })
129}
130
131fn create_ram_storage(
132 config: &toml::Value,
133 _save_root: &Path,
134 _default_world_path: &Path,
135) -> Result<WorldStorageOutput, String> {
136 validate_empty_config(config)?;
137 Ok(WorldStorageOutput {
138 storage: WorldStorageConfig::RamOnly,
139 level_data_path: None,
140 })
141}
142
143fn path_to_string(path: PathBuf) -> String {
144 path.to_string_lossy().into_owned()
145}