1use std::{
4 collections::BTreeMap,
5 io::{self, Cursor},
6 sync::atomic::{AtomicU64, Ordering},
7};
8
9use rustc_hash::FxHashMap;
10use serde::{Deserialize, Serialize};
11use simdnbt::owned::{NbtCompound, read_compound};
12use steel_utils::{
13 Identifier,
14 locks::{AsyncMutex, SyncRwLock},
15};
16
17use crate::{server::worlds::WorldMap, world::World};
18use steel_utils::saved_data::names as saved_data_names;
19
20#[derive(Default, Deserialize, Serialize)]
21struct PersistentCommandStorage {
22 entries: BTreeMap<String, Vec<u8>>,
23}
24
25struct CommandStorageSaveSnapshot {
26 revision: u64,
27 state: PersistentCommandStorage,
28}
29
30pub(crate) struct CommandStorage {
32 entries: SyncRwLock<FxHashMap<Identifier, NbtCompound>>,
33 revision: AtomicU64,
34 saved_revision: AtomicU64,
35}
36
37impl CommandStorage {
38 #[must_use]
40 pub(crate) fn new() -> Self {
41 Self {
42 entries: SyncRwLock::new(FxHashMap::default()),
43 revision: AtomicU64::new(0),
44 saved_revision: AtomicU64::new(0),
45 }
46 }
47
48 fn from_persistent(persistent: PersistentCommandStorage) -> io::Result<Self> {
49 let mut entries = FxHashMap::default();
50 for (raw_key, bytes) in persistent.entries {
51 let key = raw_key.parse::<Identifier>().map_err(|error| {
52 io::Error::new(
53 io::ErrorKind::InvalidData,
54 format!("invalid command storage key '{raw_key}': {error}"),
55 )
56 })?;
57 let mut cursor = Cursor::new(bytes.as_slice());
58 let compound = read_compound(&mut cursor).map_err(|error| {
59 io::Error::new(
60 io::ErrorKind::InvalidData,
61 format!("invalid NBT for command storage key '{key}': {error:?}"),
62 )
63 })?;
64 if cursor.position() != bytes.len() as u64 {
65 return Err(io::Error::new(
66 io::ErrorKind::InvalidData,
67 format!("trailing NBT data for command storage key '{key}'"),
68 ));
69 }
70 if !compound.is_empty() {
71 entries.insert(key, compound);
72 }
73 }
74 Ok(Self {
75 entries: SyncRwLock::new(entries),
76 revision: AtomicU64::new(0),
77 saved_revision: AtomicU64::new(0),
78 })
79 }
80
81 #[must_use]
83 pub(crate) fn get(&self, id: &Identifier) -> NbtCompound {
84 self.entries
85 .read()
86 .get(id)
87 .cloned()
88 .unwrap_or_else(NbtCompound::new)
89 }
90
91 pub(crate) fn set(&self, id: Identifier, contents: NbtCompound) {
93 let mut entries = self.entries.write();
94 if contents.is_empty() {
95 entries.remove(&id);
96 } else {
97 entries.insert(id, contents);
98 }
99 self.revision.fetch_add(1, Ordering::Release);
100 }
101
102 #[must_use]
104 pub(crate) fn keys(&self) -> Vec<Identifier> {
105 let mut keys = self.entries.read().keys().cloned().collect::<Vec<_>>();
106 keys.sort_by_cached_key(ToString::to_string);
107 keys
108 }
109
110 fn pending_save(&self) -> Option<CommandStorageSaveSnapshot> {
111 let entries = self.entries.read();
112 let revision = self.revision.load(Ordering::Acquire);
113 if revision == self.saved_revision.load(Ordering::Acquire) {
114 return None;
115 }
116
117 let entries = entries
118 .iter()
119 .map(|(key, compound)| {
120 let mut bytes = Vec::new();
121 compound.write(&mut bytes);
122 (key.to_string(), bytes)
123 })
124 .collect();
125 Some(CommandStorageSaveSnapshot {
126 revision,
127 state: PersistentCommandStorage { entries },
128 })
129 }
130
131 fn mark_saved(&self, revision: u64) {
132 self.saved_revision.fetch_max(revision, Ordering::Release);
133 }
134}
135
136impl Default for CommandStorage {
137 fn default() -> Self {
138 Self::new()
139 }
140}
141
142pub(crate) struct DomainCommandStorage {
144 storages: BTreeMap<String, CommandStorage>,
145 save_lock: AsyncMutex<()>,
146}
147
148impl DomainCommandStorage {
149 pub(crate) async fn load(worlds: &WorldMap) -> io::Result<Self> {
151 let mut domains = worlds.domain_names().collect::<Vec<_>>();
152 domains.sort_unstable();
153 let mut storages = BTreeMap::new();
154 for domain in domains {
155 let world = domain_default_world(worlds, domain)?;
156 let persistent: PersistentCommandStorage = world
157 .saved_data
158 .load_or_default(saved_data_names::COMMAND_STORAGE)
159 .await
160 .map_err(|error| storage_io_error(domain, error))?;
161 let storage = CommandStorage::from_persistent(persistent)
162 .map_err(|error| storage_io_error(domain, error))?;
163 storages.insert(domain.to_owned(), storage);
164 }
165 Ok(Self {
166 storages,
167 save_lock: AsyncMutex::new(()),
168 })
169 }
170
171 #[must_use]
173 pub(crate) fn get(&self, domain: &str) -> Option<&CommandStorage> {
174 self.storages.get(domain)
175 }
176
177 pub(crate) async fn save(&self, worlds: &WorldMap) -> io::Result<usize> {
179 let _save_guard = self.save_lock.lock().await;
180 let mut saved = 0;
181 for (domain, storage) in &self.storages {
182 let Some(snapshot) = storage.pending_save() else {
183 continue;
184 };
185 let world = domain_default_world(worlds, domain)?;
186 world
187 .saved_data
188 .save(saved_data_names::COMMAND_STORAGE, &snapshot.state)
189 .await
190 .map_err(|error| storage_io_error(domain, error))?;
191 storage.mark_saved(snapshot.revision);
192 saved += 1;
193 }
194 Ok(saved)
195 }
196}
197
198fn domain_default_world<'a>(worlds: &'a WorldMap, domain: &str) -> io::Result<&'a World> {
199 worlds
200 .default_world(domain)
201 .map(AsRef::as_ref)
202 .ok_or_else(|| {
203 io::Error::new(
204 io::ErrorKind::NotFound,
205 format!("domain '{domain}' has no loaded default world"),
206 )
207 })
208}
209
210fn storage_io_error(domain: &str, error: io::Error) -> io::Error {
211 io::Error::new(
212 error.kind(),
213 format!("command storage I/O failed for domain '{domain}': {error}"),
214 )
215}
216
217#[cfg(test)]
218mod tests {
219 use std::{
220 env::temp_dir,
221 slice,
222 time::{SystemTime, UNIX_EPOCH},
223 };
224
225 use simdnbt::owned::{NbtCompound, NbtTag};
226 use steel_utils::Identifier;
227 use tokio::fs;
228
229 use steel_utils::saved_data::SavedDataManager;
230
231 use super::*;
232
233 #[test]
234 fn missing_and_empty_values_match_vanilla_storage_semantics() {
235 let storage = CommandStorage::new();
236 let key = Identifier::from_steel("data");
237
238 assert!(storage.get(&key).is_empty());
239 let mut value = NbtCompound::new();
240 value.insert("value", 3);
241 storage.set(key.clone(), value);
242 assert_eq!(storage.keys(), slice::from_ref(&key));
243
244 storage.set(key.clone(), NbtCompound::new());
245 assert!(storage.get(&key).is_empty());
246 assert!(storage.keys().is_empty());
247 }
248
249 #[tokio::test]
250 async fn persistent_storage_round_trips_binary_nbt() {
251 let unique = SystemTime::now()
252 .duration_since(UNIX_EPOCH)
253 .expect("system time should be after Unix epoch")
254 .as_nanos();
255 let path = temp_dir().join(format!("steel-command-storage-{unique}"));
256 let manager = SavedDataManager::new(Some(&path));
257 let storage = CommandStorage::new();
258 let key = Identifier::from_steel("nested");
259 let mut nested = NbtCompound::new();
260 nested.insert("bytes", NbtTag::ByteArray(vec![1, 2, 255]));
261 let mut value = NbtCompound::new();
262 value.insert("nested", nested);
263 storage.set(key.clone(), value.clone());
264
265 let snapshot = storage.pending_save().expect("storage should become dirty");
266 manager
267 .save(saved_data_names::COMMAND_STORAGE, &snapshot.state)
268 .await
269 .expect("command storage should save");
270 storage.mark_saved(snapshot.revision);
271 assert!(storage.pending_save().is_none());
272
273 let persistent: PersistentCommandStorage = manager
274 .load_or_default(saved_data_names::COMMAND_STORAGE)
275 .await
276 .expect("command storage should load");
277 let restored =
278 CommandStorage::from_persistent(persistent).expect("stored NBT should validate");
279 assert_eq!(restored.get(&key), value);
280
281 fs::remove_dir_all(path)
282 .await
283 .expect("temporary command storage directory should be removed");
284 }
285
286 #[test]
287 fn mutation_after_snapshot_remains_dirty() {
288 let storage = CommandStorage::new();
289 let first = Identifier::from_steel("first");
290 let second = Identifier::from_steel("second");
291 let mut value = NbtCompound::new();
292 value.insert("value", 1);
293 storage.set(first, value.clone());
294 let snapshot = storage.pending_save().expect("storage should become dirty");
295
296 storage.set(second.clone(), value);
297 storage.mark_saved(snapshot.revision);
298
299 let pending = storage
300 .pending_save()
301 .expect("newer mutation should remain dirty");
302 assert!(pending.revision > snapshot.revision);
303 assert!(pending.state.entries.contains_key(&second.to_string()));
304 }
305
306 #[test]
307 fn domains_keep_independent_command_storage() {
308 let storages = DomainCommandStorage {
309 storages: [
310 ("alpha".to_owned(), CommandStorage::new()),
311 ("beta".to_owned(), CommandStorage::new()),
312 ]
313 .into_iter()
314 .collect(),
315 save_lock: AsyncMutex::new(()),
316 };
317 let key = Identifier::from_steel("data");
318 let mut value = NbtCompound::new();
319 value.insert("value", 1);
320 storages
321 .get("alpha")
322 .expect("alpha storage should exist")
323 .set(key.clone(), value);
324
325 assert!(
326 storages
327 .get("beta")
328 .expect("beta storage should exist")
329 .get(&key)
330 .is_empty()
331 );
332 }
333
334 #[test]
335 fn invalid_persisted_nbt_is_rejected() {
336 let persistent = PersistentCommandStorage {
337 entries: [("steel:data".to_owned(), vec![10])].into_iter().collect(),
338 };
339
340 let error = CommandStorage::from_persistent(persistent)
341 .err()
342 .expect("invalid NBT should fail loading");
343 assert_eq!(error.kind(), io::ErrorKind::InvalidData);
344 }
345}