mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #5321 from sirrayi/fix/jukebox-hopper-disc
🤖 Agent: new head (author merged main) keeps the hopper→jukebox fix and jukebox_taken (song stops when a hopper takes the disc); verify passes on aa68b6653, merges cleanly with current main (no overlap in jukebox/machines/spill/save)
This commit is contained in:
8 files changed
+110
-18
No files matched your search
@@ -291,11 +291,19 @@ pub struct Beacon {
|
||||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Jukebox {
|
||||
pub record: Option<ItemStack>,
|
||||
/// `JukeboxBlockEntity`'s one slot (`ContainerSingleItem`), so a hopper or
|
||||
/// a dropper reaches it as it reaches any container.
|
||||
pub items: [Option<ItemStack>; 1],
|
||||
/// `ticks_since_song_started`, while a song plays.
|
||||
pub ticks: i64,
|
||||
pub playing: bool,
|
||||
}
|
||||
impl Jukebox {
|
||||
/// `JukeboxBlockEntity.getTheItem`: the disc it holds.
|
||||
pub fn record(&self) -> Option<&ItemStack> {
|
||||
self.items[0].as_ref()
|
||||
}
|
||||
}
|
||||
/// `BrewingStandBlockEntity` state. Slots: 0-2 bottles, 3 ingredient, 4 fuel.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct BrewingStand {
|
||||
|
||||
@@ -421,7 +421,7 @@ impl BlockEntity {
|
||||
Self::Furnace(f) => f.items.iter().flatten().cloned().collect(),
|
||||
Self::Brewing(b) => b.items.iter().flatten().cloned().collect(),
|
||||
Self::Sign(_) => Vec::new(),
|
||||
Self::Jukebox(j) => j.record.iter().cloned().collect(),
|
||||
Self::Jukebox(j) => j.items.iter().flatten().cloned().collect(),
|
||||
Self::Beacon(_)
|
||||
| Self::CreakingHeart(_)
|
||||
| Self::Spawner(_)
|
||||
@@ -454,7 +454,6 @@ impl BlockEntity {
|
||||
Self::Furnace(f) => &f.items,
|
||||
Self::Brewing(b) => &b.items,
|
||||
Self::Sign(_)
|
||||
| Self::Jukebox(_)
|
||||
| Self::Beacon(_)
|
||||
| Self::Lectern(_)
|
||||
| Self::Sculk(_)
|
||||
@@ -468,6 +467,7 @@ impl BlockEntity {
|
||||
| Self::TestInstanceBlock(_)
|
||||
| Self::DaylightDetector
|
||||
| Self::Brushable(_) => &[],
|
||||
Self::Jukebox(j) => &j.items,
|
||||
Self::Crafter(c) => &c.items,
|
||||
Self::ChiseledBookshelf(b) => &b.items,
|
||||
Self::Shelf(b) => &b.items,
|
||||
@@ -488,7 +488,6 @@ impl BlockEntity {
|
||||
Self::Furnace(f) => &mut f.items,
|
||||
Self::Brewing(b) => &mut b.items,
|
||||
Self::Sign(_)
|
||||
| Self::Jukebox(_)
|
||||
| Self::Beacon(_)
|
||||
| Self::Lectern(_)
|
||||
| Self::Sculk(_)
|
||||
@@ -502,6 +501,7 @@ impl BlockEntity {
|
||||
| Self::TestInstanceBlock(_)
|
||||
| Self::DaylightDetector
|
||||
| Self::Brushable(_) => &mut [],
|
||||
Self::Jukebox(j) => &mut j.items,
|
||||
Self::Crafter(c) => &mut c.items,
|
||||
Self::ChiseledBookshelf(b) => &mut b.items,
|
||||
Self::Shelf(b) => &mut b.items,
|
||||
@@ -645,6 +645,26 @@ mod tests {
|
||||
assert_eq!(fifth.count, 1);
|
||||
}
|
||||
|
||||
/// `JukeboxBlockEntity`'s single slot is a `Container` slot, so a hopper
|
||||
/// (`HopperBlockEntity.getContainerAt`) can reach the disc in it.
|
||||
#[test]
|
||||
fn a_jukebox_offers_its_record_as_its_one_container_slot() {
|
||||
let mut entity = BlockEntity::Jukebox(Jukebox::default());
|
||||
assert_eq!(entity.slots().len(), 1);
|
||||
assert!(entity.slots()[0].is_none(), "empty until a disc lands");
|
||||
let disc = ItemStack::new("minecraft:cat", 1);
|
||||
entity.slots_mut()[0] = Some(disc.clone());
|
||||
let slots = entity.slots();
|
||||
assert_eq!(slots[0].as_ref().map(|s| &*s.item), Some("minecraft:cat"));
|
||||
match &entity {
|
||||
BlockEntity::Jukebox(j) => {
|
||||
assert_eq!(j.record().map(|s| &*s.item), Some("minecraft:cat"));
|
||||
}
|
||||
_ => panic!("a jukebox"),
|
||||
}
|
||||
assert_eq!(entity.contents().len(), 1);
|
||||
}
|
||||
|
||||
/// A whole amount pays itself without a draw; a fraction is a chance of one more.
|
||||
#[test]
|
||||
fn create_experience_rounds_the_fraction_by_chance() {
|
||||
|
||||
@@ -357,7 +357,7 @@ impl Game<'_> {
|
||||
for p in containers {
|
||||
let entity = &self.world.block_entities[&p];
|
||||
let single = match entity {
|
||||
BlockEntity::Jukebox(j) => std::slice::from_ref(&j.record),
|
||||
BlockEntity::Jukebox(j) => &j.items,
|
||||
_ => entity.slots(),
|
||||
};
|
||||
for (i, slot) in single.iter().enumerate() {
|
||||
@@ -744,7 +744,7 @@ impl Game<'_> {
|
||||
let jukebox =
|
||||
&*self.res.facts().get(self.world.block(p.0, p.1, p.2)).name == "minecraft:jukebox";
|
||||
match self.world.block_entities.get_mut(&p) {
|
||||
Some(BlockEntity::Jukebox(j)) => j.record = stack,
|
||||
Some(BlockEntity::Jukebox(j)) => j.items[0] = stack,
|
||||
Some(BlockEntity::Lectern(l)) => l.book = stack,
|
||||
Some(BlockEntity::Campfire(c)) => {
|
||||
if let Some(cell) = c.items.get_mut(slot) {
|
||||
@@ -761,7 +761,7 @@ impl Game<'_> {
|
||||
self.world.block_entities.insert(
|
||||
p,
|
||||
BlockEntity::Jukebox(horizon_client::containers::Jukebox {
|
||||
record: stack,
|
||||
items: [stack],
|
||||
ticks: 0,
|
||||
playing: false,
|
||||
}),
|
||||
|
||||
@@ -53,6 +53,47 @@ impl Game<'_> {
|
||||
self.container_changed((pos[0], pos[1], pos[2]));
|
||||
self.world.modified.insert((pos[0] >> 4, pos[2] >> 4));
|
||||
}
|
||||
/// `JukeboxBlockEntity.setTheItem` from a container (a hopper or a dropper
|
||||
/// feeding the slot): a disc that lands in a jukebox that is not playing
|
||||
/// starts its song, as the player's own use does, with the block state,
|
||||
/// the song's level event and the neighbour and comparator updates.
|
||||
pub(super) fn jukebox_inserted(&mut self, pos: [i32; 3]) {
|
||||
let Some(song) = self
|
||||
.jukebox(pos)
|
||||
.filter(|j| !j.playing)
|
||||
.and_then(|j| j.record())
|
||||
.and_then(|record| self.res.items().get(&record.item))
|
||||
.and_then(|info| info.jukebox_song.clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if let Some(BlockEntity::Jukebox(j)) =
|
||||
self.world.block_entities.get_mut(&(pos[0], pos[1], pos[2]))
|
||||
{
|
||||
j.playing = true;
|
||||
j.ticks = 0;
|
||||
}
|
||||
self.set_has_record(pos, true);
|
||||
self.start_song(pos, &song);
|
||||
self.jukebox_changed(pos);
|
||||
}
|
||||
/// `JukeboxBlockEntity.setTheItem(EMPTY)` from a container (a hopper
|
||||
/// below taking the disc): the song stops, the block loses its record,
|
||||
/// and the neighbours and comparators hear it.
|
||||
pub(super) fn jukebox_taken(&mut self, pos: [i32; 3]) {
|
||||
if let Some(BlockEntity::Jukebox(j)) =
|
||||
self.world.block_entities.get_mut(&(pos[0], pos[1], pos[2]))
|
||||
{
|
||||
if j.record().is_some() {
|
||||
return;
|
||||
}
|
||||
j.playing = false;
|
||||
j.ticks = 0;
|
||||
}
|
||||
self.stop_song(pos);
|
||||
self.set_has_record(pos, false);
|
||||
self.jukebox_changed(pos);
|
||||
}
|
||||
/// `JukeboxBlock.useItemOn` / `useWithoutItem` on the jukebox at `pos`.
|
||||
pub(super) fn use_jukebox(&mut self, pos: [i32; 3], held: Option<&ItemStack>) -> bool {
|
||||
if &*self
|
||||
@@ -64,7 +105,7 @@ impl Game<'_> {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if self.jukebox(pos).is_some_and(|j| j.record.is_some()) {
|
||||
if self.jukebox(pos).is_some_and(|j| j.record().is_some()) {
|
||||
// TRY_WITH_EMPTY_HAND: only the main hand goes on to useWithoutItem.
|
||||
if self.hand != Hand::Main {
|
||||
return false;
|
||||
@@ -95,7 +136,7 @@ impl Game<'_> {
|
||||
self.world.block_entities.insert(
|
||||
(pos[0], pos[1], pos[2]),
|
||||
BlockEntity::Jukebox(Jukebox {
|
||||
record: Some(record),
|
||||
items: [Some(record)],
|
||||
ticks: 0,
|
||||
playing: true,
|
||||
}),
|
||||
@@ -200,7 +241,7 @@ impl Game<'_> {
|
||||
Some(BlockEntity::Jukebox(j)) => {
|
||||
j.playing = false;
|
||||
j.ticks = 0;
|
||||
j.record.take()
|
||||
j.items[0].take()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
@@ -224,7 +265,7 @@ impl Game<'_> {
|
||||
/// `JukeboxSongPlayer.tick` of the jukebox at `key`, while it plays.
|
||||
pub(super) fn tick_jukebox(&mut self, key: (i32, i32, i32)) {
|
||||
let playing = match self.world.block_entities.get(&key) {
|
||||
Some(BlockEntity::Jukebox(j)) if j.playing => j.record.as_ref().and_then(|r| {
|
||||
Some(BlockEntity::Jukebox(j)) if j.playing => j.record().and_then(|r| {
|
||||
let song = self.items().get(&r.item)?.jukebox_song.clone()?;
|
||||
Some((song, j.ticks))
|
||||
}),
|
||||
@@ -314,8 +355,7 @@ pub(super) fn jukebox_signals(
|
||||
match world.block_entities.get(&(pos.x, pos.y, pos.z)) {
|
||||
Some(BlockEntity::Jukebox(j)) => {
|
||||
let comparator = j
|
||||
.record
|
||||
.as_ref()
|
||||
.record()
|
||||
.and_then(|r| items.get(&r.item)?.jukebox_song.as_ref())
|
||||
.and_then(|s| items.songs.get(s))
|
||||
.map_or(0, |s| s.comparator);
|
||||
|
||||
@@ -46,6 +46,8 @@ enum Kind {
|
||||
Brewing,
|
||||
/// `ChiseledBookShelfBlockEntity`: books only, one to a slot.
|
||||
Bookshelf,
|
||||
/// `JukeboxBlockEntity`'s single slot: a playable disc only.
|
||||
Jukebox,
|
||||
/// A mod block's block entity (`ModContainer`).
|
||||
Mod,
|
||||
/// `ShulkerBoxBlockEntity`: a shulker box doesn't go through a face into one.
|
||||
@@ -209,6 +211,7 @@ impl Containers<'_> {
|
||||
(BlockEntity::Crafter(_), _) => Kind::Crafter,
|
||||
(BlockEntity::Brewing(_), _) => Kind::Brewing,
|
||||
(BlockEntity::ChiseledBookshelf(_), _) => Kind::Bookshelf,
|
||||
(BlockEntity::Jukebox(_), _) => Kind::Jukebox,
|
||||
(_, "minecraft:hopper") => Kind::Hopper,
|
||||
(_, n) if is_shulker_box(n) => Kind::Shulker,
|
||||
_ => Kind::Generic,
|
||||
@@ -302,6 +305,15 @@ impl Containers<'_> {
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
if view.kind == Kind::Jukebox {
|
||||
// JukeboxBlockEntity.canPlaceItem: a playable disc, into the one
|
||||
// slot, which has to be empty first (its maximum stack is 1).
|
||||
return self
|
||||
.items
|
||||
.get(&stack.item)
|
||||
.is_some_and(|i| i.jukebox_song.is_some())
|
||||
&& self.get(view, slot).is_none();
|
||||
}
|
||||
if view.kind == Kind::Bookshelf {
|
||||
// ListBackedContainer.canPlaceItem: `acceptsItemType` (the books)
|
||||
// and a free slot, as `getMaxStackSize` is 1.
|
||||
@@ -572,6 +584,12 @@ impl Game<'_> {
|
||||
let (rest, moved) = self.move_in_item(from_hopper, view, stack, slot);
|
||||
if moved {
|
||||
self.view_changed(view);
|
||||
// JukeboxBlockEntity.setTheItem through a hopper's or a dropper's
|
||||
// transfer: the disc it lands on starts playing.
|
||||
if view.kind == Kind::Jukebox {
|
||||
let (x, y, z) = view.parts[0].0;
|
||||
self.jukebox_inserted([x, y, z]);
|
||||
}
|
||||
}
|
||||
rest
|
||||
}
|
||||
@@ -868,6 +886,12 @@ impl Game<'_> {
|
||||
match self.add_item(source_hopper, hopper, one, None) {
|
||||
None => {
|
||||
self.view_changed(source);
|
||||
// JukeboxBlockEntity.removeItem -> setTheItem(EMPTY): a
|
||||
// disc a hopper takes out stops the song.
|
||||
if source.kind == Kind::Jukebox {
|
||||
let (x, y, z) = source.parts[0].0;
|
||||
self.jukebox_taken([x, y, z]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Some(rest) => self.put_back(source, slot, rest),
|
||||
|
||||
@@ -476,7 +476,7 @@ impl GameLevel<'_> {
|
||||
// events and a neighbour update), the record pops, and
|
||||
// onSongChanged updates the neighbours again.
|
||||
if let Some(BlockEntity::Jukebox(j)) = &entity
|
||||
&& let Some(record) = j.record.clone()
|
||||
&& let Some(record) = j.record().cloned()
|
||||
{
|
||||
if j.playing {
|
||||
self.jukebox_stopped(pos, old);
|
||||
|
||||
@@ -667,7 +667,7 @@ pub fn encode_block_entity(
|
||||
}
|
||||
BlockEntity::Sign(sign) => entries.extend(sign.encode()),
|
||||
BlockEntity::Jukebox(j) => {
|
||||
if let Some(record) = &j.record {
|
||||
if let Some(record) = j.record() {
|
||||
entries.push(("RecordItem", item_to_nbt(record)));
|
||||
}
|
||||
entries.push(("ticks_since_song_started", Tag::Long(j.ticks)));
|
||||
@@ -912,7 +912,7 @@ pub fn decode_block_entity(tag: &Tag) -> Option<((i32, i32, i32), crate::contain
|
||||
return Some((
|
||||
pos,
|
||||
BlockEntity::Jukebox(crate::containers::Jukebox {
|
||||
record,
|
||||
items: [record],
|
||||
ticks,
|
||||
playing,
|
||||
}),
|
||||
|
||||
@@ -83,8 +83,8 @@ JukeboxBlockEntity.notifyItemChangedInJukebox port crates/horizon-client/src/gam
|
||||
JukeboxBlockEntity.getComparatorOutput port crates/horizon-client/src/game/jukebox.rs::jukebox_signals comparator value of the song
|
||||
JukeboxBlockEntity.loadAdditional port crates/horizon-client/src/save.rs::decode_block_entity RecordItem and ticks
|
||||
JukeboxBlockEntity.saveAdditional port crates/horizon-client/src/save.rs::encode_block_entity RecordItem and ticks
|
||||
JukeboxBlockEntity.setTheItem inline crates/horizon-client/src/game/jukebox.rs::Game::use_jukebox gap: player: hoppers and other containers cannot insert a disc
|
||||
JukeboxBlockEntity.canPlaceItem port crates/horizon-client/src/game/jukebox.rs::Game::use_jukebox gap: player: hopper insertion of discs is missing
|
||||
JukeboxBlockEntity.setTheItem port crates/horizon-client/src/game/jukebox.rs::Game::jukebox_inserted a disc a hopper or dropper feeds into the slot starts the song, with the block state, the level event and the neighbour and comparator updates
|
||||
JukeboxBlockEntity.canPlaceItem port crates/horizon-client/src/game/machines.rs::Containers::can_place a playable disc into the empty single slot (`Jukebox.items`, which `BlockEntity.slots` now exposes), so a hopper reaches a jukebox
|
||||
JukeboxBlockEntity.setSongItemWithoutPlaying skip:other @VisibleForTesting setter; no player behaviour
|
||||
JukeboxBlockEntity.tryForcePlaySong skip:other @VisibleForTesting hook; no player behaviour
|
||||
LecternBlockEntity.hasBook inline crates/horizon-client/src/game/books.rs::Game::use_lectern book-present check before a book is placed
|
||||
|
||||
|
Reference in new issue
Block a user