Merge pull request #3064 from iLostName/trial-spawner-spawn-egg

🤖 Agent: read the diff at 557a82738: a spawn egg on a trial spawner follows `TrialSpawner.overrideEntityToSpawn` -> `FullConfig.overrideEntity` in the 26.3 source: data reset, normal and ominous configs replaced by `Holder.direct(withSpawning(type))` (saved inline through the new `TrialConfig::encode`, `optionalFieldOf` defaults left out), then `setState(INACTIVE)`; plain spawners unchanged. Round-trip test included. verify and parity green on this head. Queued for the merger.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-10-28 16:35:54 +01:00
commit 6d3489d30b
3 files changed
+130 -16

No files matched your search

+25 -15
View File
@@ -10,6 +10,7 @@ use horizon_client::{
Mob, Who,
spawner::{Spawner, SpawnerAdded, SpawnerEvent, SpawnerSpawn},
spawning,
trial_spawner::TrialState,
},
particles::{ParticleOptions, ParticleType},
};
@@ -373,12 +374,11 @@ impl Game<'_> {
/// clicked block has no spawner.
pub(super) fn use_spawn_egg_on_spawner(&mut self, pos: [i32; 3], kind: &str) -> Option<bool> {
let key = (pos[0], pos[1], pos[2]);
if !matches!(
self.world.block_entities.get(&key),
Some(BlockEntity::Spawner(_))
) {
return None;
}
let trial = match self.world.block_entities.get(&key) {
Some(BlockEntity::Spawner(_)) => false,
Some(BlockEntity::TrialSpawner(_)) => true,
_ => return None,
};
if !self.ui.rules.bool("spawner_blocks_work") {
self.system_message(horizon_client::text::Text::translate(
"advMode.notEnabled.spawner",
@@ -386,16 +386,26 @@ impl Game<'_> {
));
return Some(false);
}
let mut events = Vec::new();
let mut random = std::mem::replace(
&mut self.world.random,
horizon_core::random::LegacyRandom::new(0),
);
if let Some(BlockEntity::Spawner(s)) = self.world.block_entities.get_mut(&key) {
s.set_entity_id(kind, &mut random, &mut events);
if trial {
// TrialSpawnerBlockEntity.setEntityId: overrideEntityToSpawn, which
// ends in setState(INACTIVE).
if let Some(BlockEntity::TrialSpawner(t)) = self.world.block_entities.get_mut(&key) {
t.override_entity(kind);
}
let ominous = self.trial_spawner_block(key).is_some_and(|(_, o)| o);
let _ = self.set_trial_spawner_state(key, TrialState::Inactive, ominous);
} else {
let mut events = Vec::new();
let mut random = std::mem::replace(
&mut self.world.random,
horizon_core::random::LegacyRandom::new(0),
);
if let Some(BlockEntity::Spawner(s)) = self.world.block_entities.get_mut(&key) {
s.set_entity_id(kind, &mut random, &mut events);
}
self.world.random = random;
let _ = self.apply_spawner_events(key, events, &HashMap::new());
}
self.world.random = random;
let _ = self.apply_spawner_events(key, events, &HashMap::new());
// sendBlockUpdated(3) and the player's block_change game event.
self.spawner_block_updated(key);
self.post_block_game_event("minecraft:block_change", pos, Some(Who::Player), None);
@@ -272,7 +272,7 @@ impl Game<'_> {
Ok(())
}
/// `setState` (and `removeOminous`): the block's state and ominous flag.
fn set_trial_spawner_state(
pub(super) fn set_trial_spawner_state(
&mut self,
key: Pos,
state: TrialState,
@@ -106,6 +106,67 @@ impl TrialConfig {
.unwrap_or(d.items_to_drop_when_ominous),
})
}
/// `TrialSpawnerConfig.DIRECT_CODEC`'s encoding: the fields that differ
/// from their defaults (`optionalFieldOf` with a default).
pub fn encode(&self) -> Tag {
let d = Self::default();
let mut out = Vec::new();
if self.spawn_range != d.spawn_range {
out.push(("spawn_range", Tag::Int(self.spawn_range)));
}
for (key, value, default) in [
("total_mobs", self.total_mobs, d.total_mobs),
(
"simultaneous_mobs",
self.simultaneous_mobs,
d.simultaneous_mobs,
),
(
"total_mobs_added_per_player",
self.total_mobs_added_per_player,
d.total_mobs_added_per_player,
),
(
"simultaneous_mobs_added_per_player",
self.simultaneous_mobs_added_per_player,
d.simultaneous_mobs_added_per_player,
),
] {
if value != default {
out.push((key, Tag::Float(value.to_bits())));
}
}
if self.ticks_between_spawn != d.ticks_between_spawn {
out.push(("ticks_between_spawn", Tag::Int(self.ticks_between_spawn)));
}
let weighted = |data: Tag, weight: i32| {
Tag::compound(vec![("data", data), ("weight", Tag::Int(weight))])
};
if !self.spawn_potentials.is_empty() {
let list = self.spawn_potentials.iter();
out.push((
"spawn_potentials",
Tag::List(list.map(|(data, w)| weighted(data.encode(), *w)).collect()),
));
}
if self.loot_tables_to_eject != d.loot_tables_to_eject {
let list = self.loot_tables_to_eject.iter();
out.push((
"loot_tables_to_eject",
Tag::List(
list.map(|(id, w)| weighted(Tag::String(id.as_str().into()), *w))
.collect(),
),
));
}
if self.items_to_drop_when_ominous != d.items_to_drop_when_ominous {
out.push((
"items_to_drop_when_ominous",
Tag::String(self.items_to_drop_when_ominous.as_str().into()),
));
}
Tag::compound(out)
}
/// `calculateTargetTotalMobs`.
pub fn target_total_mobs(&self, additional_players: i32) -> i32 {
(self.total_mobs + self.total_mobs_added_per_player * additional_players as f32).floor()
@@ -487,6 +548,18 @@ impl TrialSpawner {
self.next_spawn_data = None;
self.reset_statistics();
}
/// `overrideEntityToSpawn` (a spawn egg's `setEntityId`): the data resets
/// and both configs spawn only `id` (`withSpawning`), saved inline as
/// `Holder.direct`. The caller then sets the state to `inactive`.
pub fn override_entity(&mut self, id: &str) {
self.reset();
for slot in [&mut self.normal, &mut self.ominous] {
let mut entity = Compound::new();
entity.insert("id".into(), Tag::String(id.into()));
slot.config.spawn_potentials = vec![(SpawnData::new(entity, None, None), 1)];
slot.saved = Some(slot.config.encode());
}
}
/// `getOrCreateNextSpawnData`: a potential at random, else empty data.
fn next_or_create(
&mut self,
@@ -1030,6 +1103,37 @@ mod tests {
assert!(s.save().is_empty());
}
#[test]
fn a_spawn_egg_overrides_both_configs_inline() {
let breeze = TrialConfig {
total_mobs: 2.0,
..TrialConfig::default()
};
let registry = |_: &str| Some(breeze.clone());
let mut s = TrialSpawner::load_with(
&tag(
r#"{normal_config:"minecraft:trial_chamber/breeze/normal",spawn_data:{entity:{}},total_mobs_spawned:3}"#,
),
&registry,
);
s.override_entity("minecraft:zombie");
assert_eq!(s.next_spawn_data, None);
assert_eq!(s.total_mobs_spawned, 0);
for slot in [&s.normal, &s.ominous] {
assert_eq!(slot.config.spawn_potentials.len(), 1);
assert_eq!(
slot.config.spawn_potentials[0].0.id().as_deref(),
Some("minecraft:zombie")
);
let saved = slot.saved.as_ref().expect("saved inline");
assert_eq!(TrialConfig::decode(saved).as_ref(), Some(&slot.config));
}
assert_eq!(
s.normal.saved.as_ref().and_then(|t| t.get("total_mobs")),
Some(&Tag::Float(2.0f32.to_bits()))
);
}
#[test]
fn registry_configs_resolve_and_are_saved_by_id() {
let breeze = TrialConfig {