Skip to main content

steel_registry/poi/
mod.rs

1//! Point of Interest (POI) type registry.
2//!
3//! POI types track special blocks (beds, workstations, bells, nether portals, etc.)
4//! so game systems can efficiently query for nearby points of interest
5//! without scanning every block.
6#![cfg_attr(
7    test,
8    expect(
9        clippy::unwrap_used,
10        reason = "poi tests assert extracted vanilla state references exist"
11    )
12)]
13
14use rustc_hash::FxHashMap;
15use steel_utils::{BlockStateId, Identifier};
16
17use crate::RegistryTags;
18use crate::blocks::{BlockRef, BlockRegistry};
19
20/// A block whose states belong to a POI type, with optional property constraints.
21///
22/// An empty `properties` filter matches every state of `block` (vanilla's
23/// `getStatesOfBlock`); a non-empty filter keeps only states where each listed property
24/// equals the given value (vanilla's `BED_HEADS`-style predicate, used by `home`).
25#[derive(Debug)]
26pub struct PoiBlockMatcher {
27    pub block: BlockRef,
28    pub properties: &'static [(&'static str, &'static str)],
29}
30
31/// A type of point of interest (e.g., bed, workstation, bell, nether portal).
32///
33/// Each type matches a set of blocks (optionally filtered by properties) and defines how
34/// many entities can claim it via tickets (e.g., a bed has 1 ticket for 1 villager).
35#[derive(Debug)]
36pub struct PointOfInterestType {
37    pub key: Identifier,
38    pub blocks: &'static [PoiBlockMatcher],
39    pub ticket_count: u32,
40    pub search_distance: u32,
41}
42
43/// Static reference to a POI type definition.
44pub type PoiTypeRef = &'static PointOfInterestType;
45
46/// Registry of all POI types, with reverse lookup from block state to type.
47pub struct PoiTypeRegistry {
48    types_by_id: Vec<PoiTypeRef>,
49    types_by_key: FxHashMap<Identifier, usize>,
50    /// O(1) block state -> POI type ID lookup.
51    state_to_type: FxHashMap<BlockStateId, usize>,
52    tags: RegistryTags,
53    allows_registering: bool,
54}
55
56impl Default for PoiTypeRegistry {
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62impl PoiTypeRegistry {
63    #[must_use]
64    pub fn new() -> Self {
65        Self {
66            types_by_id: Vec::new(),
67            types_by_key: FxHashMap::default(),
68            state_to_type: FxHashMap::default(),
69            tags: RegistryTags::default(),
70            allows_registering: true,
71        }
72    }
73
74    pub fn register(&mut self, poi_type: PoiTypeRef) -> usize {
75        assert!(
76            self.allows_registering,
77            "Cannot register POI types after the registry has been frozen"
78        );
79
80        let id = self.types_by_id.len();
81        self.types_by_key.insert(poi_type.key.clone(), id);
82        self.types_by_id.push(poi_type);
83        id
84    }
85
86    /// Expands every registered POI type's block matchers into the `state -> type`
87    /// lookup map. Must be called once after the block registry is fully populated, since
88    /// resolving matchers to state ids requires the block registry.
89    pub fn build_state_index(&mut self, blocks: &BlockRegistry) {
90        self.state_to_type.clear();
91        for (id, poi_type) in self.types_by_id.iter().enumerate() {
92            for matcher in poi_type.blocks {
93                for state_id in blocks.matching_states(matcher.block, matcher.properties) {
94                    self.state_to_type.insert(state_id, id);
95                }
96            }
97        }
98    }
99
100    #[must_use]
101    pub fn type_for_state(&self, state_id: BlockStateId) -> Option<PoiTypeRef> {
102        use crate::RegistryExt;
103        self.state_to_type
104            .get(&state_id)
105            .and_then(|id| self.by_id(*id))
106    }
107
108    #[must_use]
109    pub fn type_id_for_state(&self, state_id: BlockStateId) -> Option<usize> {
110        self.state_to_type.get(&state_id).copied()
111    }
112
113    #[must_use]
114    pub fn is_poi_state(&self, state_id: BlockStateId) -> bool {
115        self.state_to_type.contains_key(&state_id)
116    }
117
118    pub fn iter(&self) -> impl Iterator<Item = (usize, PoiTypeRef)> + '_ {
119        self.types_by_id
120            .iter()
121            .enumerate()
122            .map(|(id, &poi_type)| (id, poi_type))
123    }
124}
125
126crate::impl_registry!(
127    PoiTypeRegistry,
128    PointOfInterestType,
129    types_by_id,
130    types_by_key,
131    poi_types
132);
133crate::impl_tagged_registry!(PoiTypeRegistry, types_by_key, "POI type");
134
135#[cfg(test)]
136mod tests {
137    use std::collections::{BTreeMap, BTreeSet};
138
139    use steel_utils::BlockStateId;
140
141    use crate::init_vanilla_registry;
142    use crate::{REGISTRY, vanilla_blocks};
143
144    #[test]
145    fn matching_states_respects_property_filter() {
146        init_vanilla_registry();
147        let blocks = &REGISTRY.blocks;
148
149        let furnace = blocks.matching_states(&vanilla_blocks::BLAST_FURNACE, &[]);
150        assert_eq!(
151            furnace.len(),
152            usize::from(vanilla_blocks::BLAST_FURNACE.state_count())
153        );
154
155        let heads = blocks.matching_states(&vanilla_blocks::WHITE_BED, &[("part", "head")]);
156        let all_beds = blocks.matching_states(&vanilla_blocks::WHITE_BED, &[]);
157        assert_eq!(heads.len() * 2, all_beds.len());
158        for state in heads {
159            assert!(blocks.get_properties(state).contains(&("part", "head")));
160        }
161    }
162
163    /// The registry's resolved `state -> type` mapping must exactly equal the state sets
164    /// extracted from the vanilla server jar (`build_assets/poi_types.json`) — proving the
165    /// block matchers neither under- nor over-match vanilla.
166    #[test]
167    fn matchers_reproduce_extracted_vanilla_states() {
168        init_vanilla_registry();
169
170        #[derive(serde::Deserialize)]
171        struct PoiFile {
172            poi_types: Vec<PoiJson>,
173        }
174        #[derive(serde::Deserialize)]
175        struct PoiJson {
176            name: String,
177            block_states: Vec<StateJson>,
178        }
179        #[derive(serde::Deserialize)]
180        struct StateJson {
181            state_id: u16,
182        }
183
184        let json = include_str!(concat!(
185            env!("CARGO_MANIFEST_DIR"),
186            "/build_assets/poi_types.json"
187        ));
188        let file: PoiFile = serde_json::from_str(json).unwrap();
189
190        let mut expected: BTreeMap<String, BTreeSet<u16>> = BTreeMap::new();
191        for poi in &file.poi_types {
192            let set = expected.entry(poi.name.clone()).or_default();
193            set.extend(poi.block_states.iter().map(|s| s.state_id));
194        }
195
196        let mut actual: BTreeMap<String, BTreeSet<u16>> = BTreeMap::new();
197        for raw in 0..REGISTRY.blocks.next_state_id {
198            if let Some(poi) = REGISTRY.poi_types.type_for_state(BlockStateId(raw)) {
199                actual
200                    .entry(poi.key.path.to_string())
201                    .or_default()
202                    .insert(raw);
203            }
204        }
205
206        assert_eq!(actual, expected);
207    }
208}