Skip to main content

steel_registry/
game_rules.rs

1use std::{
2    fmt::{self, Debug, Display, Formatter},
3    ptr::from_ref,
4};
5
6use rustc_hash::FxHashMap;
7use serde_json::Value;
8use steel_utils::{Downcast as _, DowncastType, DowncastTypeKey, ErasedType, Identifier};
9
10/// Categories for game rules, used for organization in the UI.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum GameRuleCategory {
13    Chat,
14    Drops,
15    Misc,
16    Mobs,
17    Player,
18    Spawning,
19    Updates,
20}
21
22/// The type of a game rule value.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum GameRuleType {
25    Bool,
26    Int,
27}
28
29/// Behavior required from a typed game rule value.
30pub trait GameRuleValueType: DowncastType + Clone + Debug + Display + Send + Sync {}
31
32impl<T> GameRuleValueType for T where T: DowncastType + Clone + Debug + Display + Send + Sync {}
33
34trait ErasedGameRuleValue: ErasedType + Debug + Display + Send + Sync {
35    fn clone_value(&self) -> Box<dyn ErasedGameRuleValue>;
36}
37
38impl<T: GameRuleValueType> ErasedGameRuleValue for T {
39    fn clone_value(&self) -> Box<dyn ErasedGameRuleValue> {
40        Box::new(self.clone())
41    }
42}
43
44/// A keyed type-erased game rule value.
45pub struct GameRuleValue {
46    value: Box<dyn ErasedGameRuleValue>,
47}
48
49impl GameRuleValue {
50    #[must_use]
51    pub fn new(value: impl GameRuleValueType) -> Self {
52        Self {
53            value: Box::new(value),
54        }
55    }
56
57    #[must_use]
58    pub fn downcast_ref<T: DowncastType>(&self) -> Option<&T> {
59        self.value.downcast_ref::<T>()
60    }
61
62    #[must_use]
63    pub fn type_key(&self) -> DowncastTypeKey {
64        self.value.downcast_type_key()
65    }
66}
67
68impl Clone for GameRuleValue {
69    fn clone(&self) -> Self {
70        Self {
71            value: self.value.clone_value(),
72        }
73    }
74}
75
76impl Debug for GameRuleValue {
77    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
78        formatter
79            .debug_struct("GameRuleValue")
80            .field("type_key", &self.type_key())
81            .field("value", &self.value)
82            .finish()
83    }
84}
85
86impl Display for GameRuleValue {
87    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
88        Display::fmt(self.value.as_ref(), formatter)
89    }
90}
91
92#[derive(Debug)]
93struct GameRuleCodec<T: GameRuleValueType> {
94    serialize: fn(&T) -> Value,
95    deserialize: fn(&Value) -> Option<T>,
96    command_result: fn(&T) -> i32,
97    validates: fn(&GameRule<T>, &T) -> bool,
98}
99
100/// A typed game rule definition.
101#[derive(Debug)]
102pub struct GameRule<T: GameRuleValueType> {
103    key: Identifier,
104    category: GameRuleCategory,
105    value_type: GameRuleType,
106    default_value: T,
107    min_value: Option<i32>,
108    max_value: Option<i32>,
109    codec: GameRuleCodec<T>,
110}
111
112impl<T: GameRuleValueType> GameRule<T> {
113    #[must_use]
114    pub const fn key(&self) -> &Identifier {
115        &self.key
116    }
117
118    #[must_use]
119    pub const fn category(&self) -> GameRuleCategory {
120        self.category
121    }
122
123    #[must_use]
124    pub const fn value_type(&self) -> GameRuleType {
125        self.value_type
126    }
127
128    #[must_use]
129    pub const fn default_value(&self) -> &T {
130        &self.default_value
131    }
132
133    #[must_use]
134    pub const fn min_value(&self) -> Option<i32> {
135        self.min_value
136    }
137
138    #[must_use]
139    pub const fn max_value(&self) -> Option<i32> {
140        self.max_value
141    }
142
143    fn validates(&self, value: &T) -> bool {
144        (self.codec.validates)(self, value)
145    }
146}
147
148impl GameRule<bool> {
149    #[must_use]
150    pub const fn boolean(key: Identifier, category: GameRuleCategory, default_value: bool) -> Self {
151        Self {
152            key,
153            category,
154            value_type: GameRuleType::Bool,
155            default_value,
156            min_value: None,
157            max_value: None,
158            codec: GameRuleCodec {
159                serialize: |value| Value::Bool(*value),
160                deserialize: Value::as_bool,
161                command_result: |value| i32::from(*value),
162                validates: |_, _| true,
163            },
164        }
165    }
166}
167
168impl GameRule<i32> {
169    #[must_use]
170    pub const fn integer(
171        key: Identifier,
172        category: GameRuleCategory,
173        default_value: i32,
174        min_value: Option<i32>,
175        max_value: Option<i32>,
176    ) -> Self {
177        if let Some(min) = min_value {
178            assert!(
179                default_value >= min,
180                "game rule default is below its minimum"
181            );
182        }
183        if let Some(max) = max_value {
184            assert!(
185                default_value <= max,
186                "game rule default is above its maximum"
187            );
188        }
189        if let (Some(min), Some(max)) = (min_value, max_value) {
190            assert!(min <= max, "game rule minimum is above its maximum");
191        }
192        Self {
193            key,
194            category,
195            value_type: GameRuleType::Int,
196            default_value,
197            min_value,
198            max_value,
199            codec: GameRuleCodec {
200                serialize: |value| Value::from(*value),
201                deserialize: |value| {
202                    let value = value.as_i64()?;
203                    i32::try_from(value).ok()
204                },
205                command_result: |value| *value,
206                validates: |rule, value| {
207                    rule.min_value.is_none_or(|min| *value >= min)
208                        && rule.max_value.is_none_or(|max| *value <= max)
209                },
210            },
211        }
212    }
213}
214
215impl<T: GameRuleValueType> PartialEq for GameRule<T> {
216    fn eq(&self, other: &Self) -> bool {
217        self.key == other.key
218    }
219}
220
221impl<T: GameRuleValueType> Eq for GameRule<T> {}
222
223/// Object-safe access to game rule metadata and value codecs.
224pub trait ErasedGameRule: Debug + Send + Sync {
225    fn key(&self) -> &Identifier;
226    fn category(&self) -> GameRuleCategory;
227    fn value_type(&self) -> GameRuleType;
228    fn min_value(&self) -> Option<i32>;
229    fn max_value(&self) -> Option<i32>;
230    fn default_erased_value(&self) -> GameRuleValue;
231    fn validates_erased_value(&self, value: &GameRuleValue) -> bool;
232    fn serialize_erased_value(&self, value: &GameRuleValue) -> Value;
233    fn deserialize_erased_value(&self, value: &Value) -> Option<GameRuleValue>;
234    fn erased_command_result(&self, value: &GameRuleValue) -> i32;
235}
236
237impl<T: GameRuleValueType> ErasedGameRule for GameRule<T> {
238    fn key(&self) -> &Identifier {
239        self.key()
240    }
241
242    fn category(&self) -> GameRuleCategory {
243        self.category()
244    }
245
246    fn value_type(&self) -> GameRuleType {
247        self.value_type()
248    }
249
250    fn min_value(&self) -> Option<i32> {
251        self.min_value()
252    }
253
254    fn max_value(&self) -> Option<i32> {
255        self.max_value()
256    }
257
258    fn default_erased_value(&self) -> GameRuleValue {
259        GameRuleValue::new(self.default_value.clone())
260    }
261
262    fn validates_erased_value(&self, value: &GameRuleValue) -> bool {
263        value
264            .downcast_ref::<T>()
265            .is_some_and(|value| self.validates(value))
266    }
267
268    fn serialize_erased_value(&self, value: &GameRuleValue) -> Value {
269        let Some(value) = value.downcast_ref::<T>() else {
270            panic!("stored value type does not match game rule {}", self.key);
271        };
272        (self.codec.serialize)(value)
273    }
274
275    fn deserialize_erased_value(&self, value: &Value) -> Option<GameRuleValue> {
276        (self.codec.deserialize)(value).map(GameRuleValue::new)
277    }
278
279    fn erased_command_result(&self, value: &GameRuleValue) -> i32 {
280        let Some(value) = value.downcast_ref::<T>() else {
281            panic!("stored value type does not match game rule {}", self.key);
282        };
283        (self.codec.command_result)(value)
284    }
285}
286
287pub type GameRuleRef<T> = &'static GameRule<T>;
288pub type ErasedGameRuleRef = &'static dyn ErasedGameRule;
289
290pub struct GameRuleRegistry {
291    game_rules_by_id: Vec<ErasedGameRuleRef>,
292    game_rules_by_key: FxHashMap<Identifier, usize>,
293    allows_registering: bool,
294}
295
296impl GameRuleRegistry {
297    #[must_use]
298    pub fn new() -> Self {
299        Self {
300            game_rules_by_id: Vec::new(),
301            game_rules_by_key: FxHashMap::default(),
302            allows_registering: true,
303        }
304    }
305
306    pub fn register<T: GameRuleValueType>(&mut self, rule: GameRuleRef<T>) -> usize {
307        assert!(
308            self.allows_registering,
309            "Cannot register game rules after registry has been frozen"
310        );
311        assert!(
312            !self.game_rules_by_key.contains_key(rule.key()),
313            "Cannot register duplicate game rule {}",
314            rule.key()
315        );
316
317        let id = self.game_rules_by_id.len();
318        self.game_rules_by_key.insert(rule.key().clone(), id);
319        self.game_rules_by_id.push(rule);
320        id
321    }
322
323    pub const fn freeze(&mut self) {
324        self.allows_registering = false;
325    }
326
327    #[must_use]
328    pub fn by_id(&self, id: usize) -> Option<ErasedGameRuleRef> {
329        self.game_rules_by_id.get(id).copied()
330    }
331
332    #[must_use]
333    pub fn by_key(&self, key: &Identifier) -> Option<ErasedGameRuleRef> {
334        self.id_from_key(key).and_then(|id| self.by_id(id))
335    }
336
337    #[must_use]
338    pub fn id_from_key(&self, key: &Identifier) -> Option<usize> {
339        self.game_rules_by_key.get(key).copied()
340    }
341
342    #[must_use]
343    pub fn len(&self) -> usize {
344        self.game_rules_by_id.len()
345    }
346
347    #[must_use]
348    pub fn is_empty(&self) -> bool {
349        self.game_rules_by_id.is_empty()
350    }
351
352    pub fn iter(&self) -> impl Iterator<Item = (usize, ErasedGameRuleRef)> + '_ {
353        self.game_rules_by_id.iter().copied().enumerate()
354    }
355}
356
357impl Default for GameRuleRegistry {
358    fn default() -> Self {
359        Self::new()
360    }
361}
362
363/// Stores per-world game rule values.
364#[derive(Debug, Clone, Default)]
365pub struct GameRuleValues {
366    values: Vec<GameRuleValue>,
367}
368
369impl GameRuleValues {
370    /// Creates values initialized from every registered game rule's default.
371    #[must_use]
372    pub fn new(registry: &GameRuleRegistry) -> Self {
373        let values = registry
374            .iter()
375            .map(|(_, rule)| rule.default_erased_value())
376            .collect();
377        Self { values }
378    }
379
380    /// Gets the typed value of a game rule.
381    #[must_use]
382    pub fn get<T: GameRuleValueType>(&self, rule: &GameRule<T>, registry: &GameRuleRegistry) -> T {
383        let value = self.get_erased(rule, registry);
384        let Some(value) = value.downcast_ref::<T>() else {
385            panic!("stored value type does not match game rule {}", rule.key());
386        };
387        value.clone()
388    }
389
390    /// Sets the typed value of a game rule.
391    pub fn set<T: GameRuleValueType>(
392        &mut self,
393        rule: &GameRule<T>,
394        value: T,
395        registry: &GameRuleRegistry,
396    ) -> bool {
397        if !rule.validates(&value) {
398            return false;
399        }
400        let id = Self::rule_id(rule, registry);
401        self.values[id] = GameRuleValue::new(value);
402        true
403    }
404
405    /// Gets a type-erased value for a dynamically selected rule.
406    #[must_use]
407    pub fn get_erased(
408        &self,
409        rule: &dyn ErasedGameRule,
410        registry: &GameRuleRegistry,
411    ) -> &GameRuleValue {
412        let id = Self::rule_id(rule, registry);
413        let Some(value) = self.values.get(id) else {
414            panic!("game rule {} has no stored value", rule.key());
415        };
416        value
417    }
418
419    /// Sets a type-erased value for a dynamically selected rule.
420    pub fn set_erased(
421        &mut self,
422        rule: &dyn ErasedGameRule,
423        value: GameRuleValue,
424        registry: &GameRuleRegistry,
425    ) -> bool {
426        if !rule.validates_erased_value(&value) {
427            return false;
428        }
429        let id = Self::rule_id(rule, registry);
430        self.values[id] = value;
431        true
432    }
433
434    /// Loads a serialized value for a game rule selected by name.
435    pub fn set_serialized_by_name(
436        &mut self,
437        name: &str,
438        value: &Value,
439        registry: &GameRuleRegistry,
440    ) -> bool {
441        let Ok(key) = name.parse::<Identifier>() else {
442            return false;
443        };
444        let Some(rule) = registry.by_key(&key) else {
445            return false;
446        };
447        let Some(value) = rule.deserialize_erased_value(value) else {
448            return false;
449        };
450        self.set_erased(rule, value, registry)
451    }
452
453    fn rule_id(rule: &dyn ErasedGameRule, registry: &GameRuleRegistry) -> usize {
454        let Some(id) = registry.id_from_key(rule.key()) else {
455            panic!("game rule {} is not registered", rule.key());
456        };
457        let Some(registered_rule) = registry.by_id(id) else {
458            panic!("game rule registry has no entry for {}", rule.key());
459        };
460        assert!(
461            from_ref(rule).cast::<()>() == from_ref(registered_rule).cast::<()>(),
462            "game rule {} is not the registered definition",
463            rule.key()
464        );
465        id
466    }
467}
468
469#[cfg(test)]
470mod tests {
471    use serde_json::Value;
472
473    use super::{GameRule, GameRuleCategory, GameRuleRegistry, GameRuleValue, GameRuleValues};
474    use steel_utils::Identifier;
475
476    static BOOL_RULE: GameRule<bool> = GameRule::boolean(
477        Identifier::vanilla_static("test_bool"),
478        GameRuleCategory::Misc,
479        true,
480    );
481    static INT_RULE: GameRule<i32> = GameRule::integer(
482        Identifier::vanilla_static("test_int"),
483        GameRuleCategory::Misc,
484        3,
485        Some(0),
486        Some(10),
487    );
488    static CUSTOM_BOOL_RULE: GameRule<bool> = GameRule::boolean(
489        Identifier::new_static("steel", "test_bool"),
490        GameRuleCategory::Misc,
491        true,
492    );
493
494    fn registry() -> GameRuleRegistry {
495        let mut registry = GameRuleRegistry::new();
496        registry.register(&BOOL_RULE);
497        registry.register(&INT_RULE);
498        registry.register(&CUSTOM_BOOL_RULE);
499        registry.freeze();
500        registry
501    }
502
503    #[test]
504    fn typed_values_use_defaults_and_update_without_callsite_downcasts() {
505        let registry = registry();
506        let mut values = GameRuleValues::new(&registry);
507
508        assert!(values.get(&BOOL_RULE, &registry));
509        assert_eq!(values.get(&INT_RULE, &registry), 3);
510        assert!(values.set(&BOOL_RULE, false, &registry));
511        assert!(values.set(&INT_RULE, 7, &registry));
512        assert!(!values.get(&BOOL_RULE, &registry));
513        assert_eq!(values.get(&INT_RULE, &registry), 7);
514    }
515
516    #[test]
517    fn typed_and_erased_sets_enforce_rule_bounds_and_value_type() {
518        let registry = registry();
519        let mut values = GameRuleValues::new(&registry);
520
521        assert!(!values.set(&INT_RULE, 11, &registry));
522        assert_eq!(values.get(&INT_RULE, &registry), 3);
523
524        let Some(int_rule) = registry.by_key(INT_RULE.key()) else {
525            panic!("test int rule should be registered");
526        };
527        assert!(!values.set_erased(int_rule, GameRuleValue::new(false), &registry));
528        assert_eq!(values.get(&INT_RULE, &registry), 3);
529    }
530
531    #[test]
532    fn erased_rule_codecs_round_trip_serialized_values() {
533        let registry = registry();
534        let mut values = GameRuleValues::new(&registry);
535
536        assert!(values.set_serialized_by_name("test_bool", &Value::Bool(false), &registry));
537        assert!(values.set_serialized_by_name("test_int", &Value::from(8), &registry));
538        assert!(values.set_serialized_by_name("steel:test_bool", &Value::Bool(false), &registry,));
539        assert!(!values.set_serialized_by_name("test_int", &Value::from(12), &registry));
540
541        for (_, rule) in registry.iter() {
542            let serialized = rule.serialize_erased_value(values.get_erased(rule, &registry));
543            let Some(decoded) = rule.deserialize_erased_value(&serialized) else {
544                panic!("serialized game rule should decode");
545            };
546            assert!(rule.validates_erased_value(&decoded));
547        }
548
549        assert!(!values.get(&BOOL_RULE, &registry));
550        assert_eq!(values.get(&INT_RULE, &registry), 8);
551        assert!(!values.get(&CUSTOM_BOOL_RULE, &registry));
552    }
553
554    #[test]
555    #[should_panic(expected = "is not the registered definition")]
556    fn same_key_does_not_allow_forging_different_rule_metadata() {
557        let registry = registry();
558        let values = GameRuleValues::new(&registry);
559        let forged_rule = GameRule::integer(
560            INT_RULE.key().clone(),
561            GameRuleCategory::Misc,
562            3,
563            None,
564            None,
565        );
566
567        let _ = values.get(&forged_rule, &registry);
568    }
569}