Skip to main content

steel_core/block_entity/entities/spawner/
base_spawner.rs

1//! Shared spawn-cycle state and ticking used by spawner block entities.
2
3use std::mem;
4use std::sync::Arc;
5
6use glam::DVec3;
7use rand::{Rng, RngExt, rng};
8use simdnbt::borrow::{BaseNbtCompound as BorrowedNbtCompound, NbtCompound as NbtCompoundView};
9use simdnbt::owned::{NbtCompound, NbtList};
10use steel_math::DEGREE_360;
11use steel_registry::data_components::components::EntityData;
12use steel_registry::entity_type::{EntityTypeRef, MobCategory};
13use steel_registry::loot_table::{LootContext, LootTableRef};
14use steel_registry::{
15    REGISTRY, RegistryExt, level_events, vanilla_blocks, vanilla_game_events, vanilla_game_rules,
16};
17use steel_utils::entity_events::EntityStatus;
18use steel_utils::locks::SyncMutex;
19use steel_utils::nbt::{NbtNumeric, merge_nbt_compounds};
20use steel_utils::types::Difficulty;
21use steel_utils::{BlockPos, Identifier, WorldAabb};
22
23use super::spawn_data::{EquipmentTable, SpawnData, WeightedSpawnData};
24use crate::entity::{
25    Entity, EntitySpawnReason, Mob, SpawnPlacements, entity_loot_ref, living_loot_equipment,
26    load_entity_recursive_owned, nbt_dvec3,
27};
28use crate::inventory::equipment::EquipmentSlot;
29use crate::physics::{WorldCollisionProvider, has_collision};
30use crate::world::World;
31
32const EVENT_SPAWN: i32 = 1;
33const DEFAULT_SPAWN_DELAY: i32 = 20;
34const DEFAULT_MIN_SPAWN_DELAY: i32 = 200;
35const DEFAULT_MAX_SPAWN_DELAY: i32 = 800;
36const DEFAULT_SPAWN_COUNT: i32 = 4;
37const DEFAULT_MAX_NEARBY_ENTITIES: i32 = 6;
38const DEFAULT_REQUIRED_PLAYER_RANGE: i32 = 16;
39const DEFAULT_SPAWN_RANGE: i32 = 4;
40
41/// Spawn-cycle limits of a [`BaseSpawner`]: delays, mob counts and ranges.
42///
43/// Vanilla keeps these as separate fields on `BaseSpawner`.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct BaseSpawnerConfig {
46    /// Lower bound of the delay between spawn cycles, in ticks.
47    pub min_spawn_delay: i32,
48    /// Upper bound of the delay between spawn cycles, in ticks.
49    pub max_spawn_delay: i32,
50    /// Spawn attempts per cycle.
51    pub spawn_count: i32,
52    /// Same-type entities allowed around the spawner before a cycle is skipped.
53    pub max_nearby_entities: i32,
54    /// Distance a player must be within for the spawner to tick.
55    pub required_player_range: i32,
56    /// Horizontal radius of the spawn area.
57    pub spawn_range: i32,
58}
59
60impl Default for BaseSpawnerConfig {
61    fn default() -> Self {
62        Self {
63            min_spawn_delay: DEFAULT_MIN_SPAWN_DELAY,
64            max_spawn_delay: DEFAULT_MAX_SPAWN_DELAY,
65            spawn_count: DEFAULT_SPAWN_COUNT,
66            max_nearby_entities: DEFAULT_MAX_NEARBY_ENTITIES,
67            required_player_range: DEFAULT_REQUIRED_PLAYER_RANGE,
68            spawn_range: DEFAULT_SPAWN_RANGE,
69        }
70    }
71}
72
73/// Active spawn data of a spawner.
74///
75/// Vanilla holds the `SpawnData` picked from `spawnPotentials` by reference, so spawn egg
76/// edits through it also change that potential.
77#[derive(Debug)]
78enum NextSpawnData {
79    /// Index of the picked `spawn_potentials` entry.
80    Potential(usize),
81    /// Spawn data not shared with any potential.
82    Own(SpawnData),
83}
84
85#[derive(Debug)]
86struct SpawnerState {
87    spawn_delay: i32,
88    spawn_potentials: Vec<WeightedSpawnData>,
89    next_spawn_data: Option<NextSpawnData>,
90    config: BaseSpawnerConfig,
91}
92
93#[derive(Debug, Default)]
94pub(super) struct SpawnerTickResult {
95    pub(super) state_changed: bool,
96    pub(super) next_spawn_data_changed: bool,
97}
98
99impl Default for SpawnerState {
100    fn default() -> Self {
101        Self {
102            spawn_delay: DEFAULT_SPAWN_DELAY,
103            spawn_potentials: Vec::new(),
104            next_spawn_data: None,
105            config: BaseSpawnerConfig::default(),
106        }
107    }
108}
109
110impl SpawnerState {
111    fn next_spawn_data(&self) -> Option<&SpawnData> {
112        match self.next_spawn_data.as_ref()? {
113            NextSpawnData::Potential(index) => Some(&self.spawn_potentials[*index].data),
114            NextSpawnData::Own(data) => Some(data),
115        }
116    }
117
118    fn get_or_create_next_spawn_data<R: Rng + ?Sized>(
119        &mut self,
120        random: &mut R,
121    ) -> (&mut SpawnData, bool) {
122        let created = self.next_spawn_data.is_none();
123        let next = self.next_spawn_data.get_or_insert_with(|| {
124            choose_weighted(&self.spawn_potentials, random).map_or_else(
125                || NextSpawnData::Own(SpawnData::default()),
126                NextSpawnData::Potential,
127            )
128        });
129        let data = match next {
130            NextSpawnData::Potential(index) => &mut self.spawn_potentials[*index].data,
131            NextSpawnData::Own(data) => data,
132        };
133        (data, created)
134    }
135
136    fn replace_spawn_potentials(&mut self, potentials: Vec<WeightedSpawnData>) {
137        let mut old_potentials = mem::replace(&mut self.spawn_potentials, potentials);
138        if let Some(NextSpawnData::Potential(index)) = self.next_spawn_data {
139            self.next_spawn_data = Some(NextSpawnData::Own(old_potentials.swap_remove(index).data));
140        }
141    }
142}
143
144/// Vanilla-style mutable spawner state shared by normal spawner block entities.
145#[derive(Debug)]
146pub struct BaseSpawner {
147    state: SyncMutex<SpawnerState>,
148}
149
150impl Default for BaseSpawner {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156impl BaseSpawner {
157    /// Creates a spawner with vanilla defaults.
158    #[must_use]
159    pub fn new() -> Self {
160        Self {
161            state: SyncMutex::new(SpawnerState::default()),
162        }
163    }
164
165    /// Loads the vanilla spawner fields from NBT.
166    pub fn load(&self, nbt: &BorrowedNbtCompound<'_>) {
167        let nbt: NbtCompoundView<'_, '_> = nbt.into();
168        let mut state = self.state.lock();
169        state.spawn_delay = nbt
170            .get("Delay")
171            .and_then(|tag| tag.short_value())
172            .map_or(DEFAULT_SPAWN_DELAY, i32::from);
173        if let Some(data) = nbt
174            .compound("SpawnData")
175            .and_then(|data| SpawnData::from_nbt(&data))
176        {
177            state.next_spawn_data = Some(NextSpawnData::Own(data));
178        }
179        let loaded_spawn_potentials = nbt.list("SpawnPotentials").map(|entries| {
180            entries
181                .compounds()
182                .unwrap_or_default()
183                .into_iter()
184                .filter_map(|entry| {
185                    let weight = entry.get("weight")?.codec_i32()?;
186                    if weight < 0 {
187                        return None;
188                    }
189                    let data = entry.compound("data")?;
190                    Some(WeightedSpawnData {
191                        weight,
192                        data: SpawnData::from_nbt(&data)?,
193                    })
194                })
195                .collect::<Vec<_>>()
196        });
197        if let Some(potentials) = loaded_spawn_potentials {
198            state.replace_spawn_potentials(potentials);
199        } else {
200            // Vanilla wraps the active spawn data itself, so both stay shared.
201            state.replace_spawn_potentials(vec![WeightedSpawnData {
202                weight: 1,
203                data: SpawnData::default(),
204            }]);
205            if let Some(NextSpawnData::Own(data)) = state.next_spawn_data.take() {
206                state.spawn_potentials[0].data = data;
207                state.next_spawn_data = Some(NextSpawnData::Potential(0));
208            }
209        }
210        state.config.min_spawn_delay = nbt
211            .get("MinSpawnDelay")
212            .and_then(|tag| tag.int_value())
213            .unwrap_or(DEFAULT_MIN_SPAWN_DELAY);
214        state.config.max_spawn_delay = nbt
215            .get("MaxSpawnDelay")
216            .and_then(|tag| tag.int_value())
217            .unwrap_or(DEFAULT_MAX_SPAWN_DELAY);
218        state.config.spawn_count = nbt
219            .get("SpawnCount")
220            .and_then(|tag| tag.int_value())
221            .unwrap_or(DEFAULT_SPAWN_COUNT);
222        state.config.max_nearby_entities = nbt
223            .get("MaxNearbyEntities")
224            .and_then(|tag| tag.int_value())
225            .unwrap_or(DEFAULT_MAX_NEARBY_ENTITIES);
226        state.config.required_player_range = nbt
227            .get("RequiredPlayerRange")
228            .and_then(|tag| tag.int_value())
229            .unwrap_or(DEFAULT_REQUIRED_PLAYER_RANGE);
230        state.config.spawn_range = nbt
231            .get("SpawnRange")
232            .and_then(|tag| tag.int_value())
233            .unwrap_or(DEFAULT_SPAWN_RANGE);
234    }
235
236    /// Saves the vanilla spawner fields to NBT.
237    pub fn save(&self, nbt: &mut NbtCompound) {
238        let state = self.state.lock();
239        nbt.insert("Delay", state.spawn_delay as i16);
240        nbt.insert("MinSpawnDelay", state.config.min_spawn_delay as i16);
241        nbt.insert("MaxSpawnDelay", state.config.max_spawn_delay as i16);
242        nbt.insert("SpawnCount", state.config.spawn_count as i16);
243        nbt.insert("MaxNearbyEntities", state.config.max_nearby_entities as i16);
244        nbt.insert(
245            "RequiredPlayerRange",
246            state.config.required_player_range as i16,
247        );
248        nbt.insert("SpawnRange", state.config.spawn_range as i16);
249        if let Some(next_spawn_data) = state.next_spawn_data() {
250            nbt.insert("SpawnData", next_spawn_data.to_nbt());
251        }
252        let potentials = state
253            .spawn_potentials
254            .iter()
255            .map(|entry| {
256                let mut nbt = NbtCompound::new();
257                nbt.insert("data", entry.data.to_nbt());
258                nbt.insert("weight", entry.weight);
259                nbt
260            })
261            .collect::<Vec<_>>();
262        nbt.insert("SpawnPotentials", NbtList::Compound(potentials));
263    }
264
265    /// Selects or returns the active spawn data.
266    #[cfg(test)]
267    pub(super) fn get_or_create_next_spawn_data(&self) -> SpawnData {
268        let mut random = rng();
269        self.get_or_create_next_spawn_data_with_rng(&mut random).0
270    }
271
272    fn get_or_create_next_spawn_data_with_rng<R: Rng + ?Sized>(
273        &self,
274        random: &mut R,
275    ) -> (SpawnData, bool) {
276        let mut state = self.state.lock();
277        let (data, created) = state.get_or_create_next_spawn_data(random);
278        (data.clone(), created)
279    }
280
281    /// Configures the active spawn entity.
282    pub fn set_entity_id(&self, entity_type: EntityTypeRef) {
283        let mut random = rng();
284        let mut state = self.state.lock();
285        let (data, _) = state.get_or_create_next_spawn_data(&mut random);
286        replace_entity_id(&mut data.entity_to_spawn, entity_type);
287    }
288
289    /// Sets the active spawn entity type and merges the rest of `entity_data` into it.
290    pub fn set_entity_data(&self, entity_data: &EntityData) {
291        let mut random = rng();
292        let mut state = self.state.lock();
293        let (data, _) = state.get_or_create_next_spawn_data(&mut random);
294        replace_entity_id(&mut data.entity_to_spawn, entity_data.entity_type());
295        merge_nbt_compounds(&mut data.entity_to_spawn, entity_data.data().as_compound());
296    }
297
298    /// Returns whether this spawner handles the given block event.
299    #[must_use]
300    #[expect(
301        clippy::unused_self,
302        reason = "The event method mirrors the vanilla BaseSpawner delegation API."
303    )]
304    pub const fn on_event_triggered(&self, event: i32) -> bool {
305        event == EVENT_SPAWN
306    }
307
308    /// Returns the ticks left until the next spawn cycle. `-1` means a new delay is rolled
309    /// on the next tick.
310    #[must_use]
311    pub fn spawn_delay(&self) -> i32 {
312        self.state.lock().spawn_delay
313    }
314
315    /// Sets the ticks left until the next spawn cycle.
316    pub fn set_spawn_delay(&self, delay: i32) {
317        self.state.lock().spawn_delay = delay;
318    }
319
320    /// Returns the spawn-cycle limits.
321    #[must_use]
322    pub fn config(&self) -> BaseSpawnerConfig {
323        self.state.lock().config
324    }
325
326    /// Replaces the spawn-cycle limits.
327    pub fn set_config(&self, config: BaseSpawnerConfig) {
328        self.state.lock().config = config;
329    }
330
331    /// Returns the weighted entries the next spawn data is picked from.
332    #[must_use]
333    pub fn spawn_potentials(&self) -> Vec<WeightedSpawnData> {
334        self.state.lock().spawn_potentials.clone()
335    }
336
337    /// Replaces the weighted entries the next spawn data is picked from. The active
338    /// spawn data is kept until the next delay roll.
339    pub fn set_spawn_potentials(&self, potentials: Vec<WeightedSpawnData>) {
340        self.state.lock().replace_spawn_potentials(potentials);
341    }
342
343    /// Returns the active spawn data, if one has been selected.
344    #[must_use]
345    pub fn next_spawn_data(&self) -> Option<SpawnData> {
346        self.state.lock().next_spawn_data().cloned()
347    }
348
349    /// Replaces the active spawn data.
350    pub fn set_next_spawn_data(&self, data: SpawnData) {
351        self.state.lock().next_spawn_data = Some(NextSpawnData::Own(data));
352    }
353
354    #[expect(
355        clippy::too_many_lines,
356        reason = "The order of this method mirrors vanilla BaseSpawner.serverTick."
357    )]
358    pub(super) fn server_tick(&self, world: &Arc<World>, pos: BlockPos) -> SpawnerTickResult {
359        let center = DVec3::from(pos.get_center());
360        let config = self.config();
361        let Some(_) =
362            world.nearest_player(center, f64::from(config.required_player_range), |player| {
363                player.is_alive() && !player.is_spectator()
364            })
365        else {
366            return SpawnerTickResult::default();
367        };
368        if !world.get_game_rule(&vanilla_game_rules::SPAWNER_BLOCKS_WORK) {
369            return SpawnerTickResult::default();
370        }
371
372        let mut random = rng();
373        let mut result = SpawnerTickResult::default();
374        if self.spawn_delay() == -1 {
375            result = self.delay(world, pos, &mut random);
376        }
377
378        if self.decrement_spawn_delay() {
379            return result;
380        }
381
382        let (next_spawn_data, selected_data_changed) =
383            self.get_or_create_next_spawn_data_with_rng(&mut random);
384        result.next_spawn_data_changed |= selected_data_changed;
385        result.state_changed |= selected_data_changed;
386        let BaseSpawnerConfig {
387            spawn_count,
388            spawn_range,
389            max_nearby_entities,
390            ..
391        } = config;
392        let mut spawned_any = false;
393
394        for _ in 0..spawn_count {
395            let Some(entity_type) = entity_type_from_owned_nbt(&next_spawn_data.entity_to_spawn)
396            else {
397                return self.delay_result(world, pos, &mut random, result);
398            };
399
400            let spawn_position = configured_or_random_position(
401                &next_spawn_data.entity_to_spawn,
402                pos,
403                spawn_range,
404                &mut random,
405            );
406            let dimensions = entity_type.dimensions;
407            let spawn_box = WorldAabb::entity_box(
408                spawn_position.x,
409                spawn_position.y,
410                spawn_position.z,
411                f64::from(dimensions.half_width()),
412                f64::from(dimensions.height),
413            );
414            if has_collision(&WorldCollisionProvider::new(world), spawn_box) {
415                continue;
416            }
417
418            let spawn_block_pos = BlockPos::from(spawn_position);
419            if let Some(custom_rules) = next_spawn_data.custom_spawn_rules {
420                if entity_type.mob_category == MobCategory::Monster
421                    && world.difficulty() == Difficulty::Peaceful
422                {
423                    continue;
424                }
425                if !custom_rules.is_valid_position(world, spawn_block_pos) {
426                    continue;
427                }
428            } else if !SpawnPlacements::check_spawner_spawn_rules(
429                entity_type,
430                world,
431                spawn_block_pos,
432                &mut random,
433            ) {
434                continue;
435            }
436
437            let Some(loaded) = load_entity_recursive_owned(
438                world,
439                &next_spawn_data.entity_to_spawn,
440                EntitySpawnReason::Spawner,
441                |entity| {
442                    entity.snap_to(spawn_position, entity.rotation().0, entity.rotation().1);
443                },
444            ) else {
445                return self.delay_result(world, pos, &mut random, result);
446            };
447            let entity = &loaded.root;
448
449            let entity_class = entity.downcast_type_key();
450            let nearby_box = WorldAabb::new(
451                f64::from(pos.x()),
452                f64::from(pos.y()),
453                f64::from(pos.z()),
454                f64::from(pos.x() + 1),
455                f64::from(pos.y() + 1),
456                f64::from(pos.z() + 1),
457            )
458            .inflate(f64::from(spawn_range));
459            let nearby_count = world
460                .get_entities_in_aabb_matching(&nearby_box, |candidate| {
461                    !candidate.is_spectator() && candidate.downcast_type_key() == entity_class
462                })
463                .len();
464            if nearby_count as i32 >= max_nearby_entities {
465                return self.delay_result(world, pos, &mut random, result);
466            }
467
468            let yaw = random.random::<f32>() * DEGREE_360;
469            entity.snap_to(entity.position(), yaw, 0.0);
470            if let Some(mob) = entity.as_mob() {
471                if (next_spawn_data.custom_spawn_rules.is_none()
472                    && !mob.check_spawn_rules(world, EntitySpawnReason::Spawner))
473                    || !mob.check_spawn_obstruction(world)
474                {
475                    continue;
476                }
477
478                if next_spawn_data.entity_to_spawn.len() == 1 {
479                    let _ = mob.finalize_spawn(world, EntitySpawnReason::Spawner, None);
480                }
481                if let Some(equipment) = &next_spawn_data.equipment {
482                    equip_from_table(mob, equipment);
483                }
484            }
485
486            if world
487                .try_add_fresh_entity_with_passengers(Arc::clone(entity))
488                .is_err()
489            {
490                return self.delay_result(world, pos, &mut random, result);
491            }
492
493            world.level_event(level_events::PARTICLES_MOBBLOCK_SPAWN, pos, 0, None);
494            world.game_event(
495                &vanilla_game_events::ENTITY_PLACE,
496                spawn_block_pos,
497                &crate::world::game_event::GameEventContext::new(Some(entity.as_ref()), None),
498            );
499            if entity.as_mob().is_some() {
500                entity.broadcast_entity_event(EntityStatus::SilverfishMergeAnim);
501            }
502            spawned_any = true;
503        }
504
505        if spawned_any {
506            result = self.delay_result(world, pos, &mut random, result);
507        }
508        result
509    }
510
511    fn delay_result<R: Rng + ?Sized>(
512        &self,
513        world: &Arc<World>,
514        pos: BlockPos,
515        random: &mut R,
516        mut result: SpawnerTickResult,
517    ) -> SpawnerTickResult {
518        let delay_result = self.delay(world, pos, random);
519        result.state_changed |= delay_result.state_changed;
520        result.next_spawn_data_changed |= delay_result.next_spawn_data_changed;
521        result
522    }
523
524    fn delay<R: Rng + ?Sized>(
525        &self,
526        world: &Arc<World>,
527        pos: BlockPos,
528        random: &mut R,
529    ) -> SpawnerTickResult {
530        let mut result = SpawnerTickResult {
531            state_changed: true,
532            next_spawn_data_changed: false,
533        };
534        {
535            let mut state = self.state.lock();
536            let BaseSpawnerConfig {
537                min_spawn_delay,
538                max_spawn_delay,
539                ..
540            } = state.config;
541            let delay = max_spawn_delay.saturating_sub(min_spawn_delay);
542            state.spawn_delay = if delay <= 0 {
543                min_spawn_delay
544            } else {
545                min_spawn_delay + random.random_range(0..delay)
546            };
547            if let Some(index) = choose_weighted(&state.spawn_potentials, random) {
548                result.next_spawn_data_changed =
549                    state.next_spawn_data() != Some(&state.spawn_potentials[index].data);
550                state.next_spawn_data = Some(NextSpawnData::Potential(index));
551            }
552        }
553        world.block_event(pos, &vanilla_blocks::SPAWNER, EVENT_SPAWN, 0);
554        result
555    }
556
557    fn decrement_spawn_delay(&self) -> bool {
558        let mut state = self.state.lock();
559        if state.spawn_delay > 0 {
560            state.spawn_delay -= 1;
561            true
562        } else {
563            false
564        }
565    }
566}
567
568/// Returns the index of a weighted pick from `entries`.
569pub(super) fn choose_weighted<R: Rng + ?Sized>(
570    entries: &[WeightedSpawnData],
571    random: &mut R,
572) -> Option<usize> {
573    let total_weight = entries
574        .iter()
575        .map(|entry| i64::from(entry.weight))
576        .sum::<i64>();
577    if total_weight == 0 {
578        return None;
579    }
580
581    let mut selection = random.random_range(0..total_weight);
582    for (index, entry) in entries.iter().enumerate() {
583        let weight = i64::from(entry.weight);
584        if selection < weight {
585            return Some(index);
586        }
587        selection -= weight;
588    }
589    None
590}
591
592fn entity_type_from_owned_nbt(entity: &NbtCompound) -> Option<EntityTypeRef> {
593    let id = entity.string("id")?.to_str().parse::<Identifier>().ok()?;
594    REGISTRY.entity_types.by_key(&id)
595}
596
597fn configured_or_random_position<R: Rng + ?Sized>(
598    entity: &NbtCompound,
599    spawner_pos: BlockPos,
600    spawn_range: i32,
601    random: &mut R,
602) -> DVec3 {
603    entity
604        .get("Pos")
605        .and_then(nbt_dvec3)
606        .filter(|position| position.is_finite())
607        .unwrap_or_else(|| random_spawn_position(spawner_pos, spawn_range, random))
608}
609
610fn random_spawn_position<R: Rng + ?Sized>(
611    spawner_pos: BlockPos,
612    spawn_range: i32,
613    random: &mut R,
614) -> DVec3 {
615    DVec3::new(
616        f64::from(spawner_pos.x())
617            + (random.random::<f64>() - random.random::<f64>()) * f64::from(spawn_range)
618            + 0.5,
619        f64::from(spawner_pos.y() + random.random_range(-1..=1)),
620        f64::from(spawner_pos.z())
621            + (random.random::<f64>() - random.random::<f64>()) * f64::from(spawn_range)
622            + 0.5,
623    )
624}
625
626fn equip_from_table(mob: &dyn Mob, equipment: &EquipmentTable) {
627    let Some(table): Option<LootTableRef> = REGISTRY.loot_tables.by_key(&equipment.loot_table)
628    else {
629        return;
630    };
631
632    let mut random = rng();
633    let mob_equipment = living_loot_equipment(mob);
634    let entity_ref = entity_loot_ref(mob.as_entity_event_source(), Some(&mob_equipment));
635    let mut context = LootContext::new(&mut random)
636        .with_origin(mob.position().x, mob.position().y, mob.position().z)
637        .with_this_entity(entity_ref);
638    let mut inserted = [false; EquipmentSlot::ALL.len()];
639    for mut item in table.get_random_items(&mut context) {
640        if item.is_empty() {
641            continue;
642        }
643        let slot = item
644            .get_equippable_slot()
645            .unwrap_or(EquipmentSlot::MainHand);
646        if inserted[slot.index()] {
647            continue;
648        }
649
650        let equipped = slot.limit(&mut item);
651        if equipped.is_empty() {
652            continue;
653        }
654        mob.living_base().equipment().lock().set(slot, equipped);
655        if let Some(chance) = equipment.drop_chance(slot) {
656            mob.set_equipment_drop_chance(slot, chance);
657        }
658        inserted[slot.index()] = true;
659    }
660}
661
662fn replace_entity_id(entity: &mut NbtCompound, entity_type: EntityTypeRef) {
663    while entity.remove("id").is_some() {}
664    entity.insert("id", entity_type.key.to_string());
665}