Skip to main content

steel_core/chunk_saver/
registry.rs

1//! Runtime registry for world storage backends.
2
3use 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
11/// Storage paths and backend for a loaded world.
12pub struct WorldStorageOutput {
13    /// Chunk storage backend config.
14    pub storage: WorldStorageConfig,
15    /// Directory containing level data, if persistent.
16    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
24/// Registry of server-side world storage factories.
25pub struct WorldStorageRegistry {
26    factories: FxHashMap<Identifier, WorldStorageFactory>,
27}
28
29impl WorldStorageRegistry {
30    /// Creates a registry containing Steel's built-in world storage backends.
31    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    /// Validates a storage selection.
60    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    /// Creates a resolved world storage config.
69    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}