game/maps: honour can_place_on when using a map on a banner in adventure mode

In adventure mode, vanilla allows ItemStack.useOn if the item's can_place_on component matches the clicked block. Previously, use_map_on_banner checked only !self.may_build() and returned early without checking can_place_on.

This honours can_place_on_in_adventure on the banner's block position, allowing adventure mode players with matching maps to toggle banner markers.

Co-Authored-By: Antigravity <[email protected]>
This commit is contained in:
AxxioandAntigravity committed 2026-11-06 02:18:05 +01:00
1 parent 3c7bb44aa1
commit 2948fb86ed
1 file changed
+104 -3
+104 -3
View File
@@ -1171,9 +1171,7 @@ impl Game<'_> {
let Some(id) = map_id(stack) else {
return false;
};
// ItemStack.useOn passes in adventure mode (its `can_place_on`
// exception is not ported).
if !self.may_build() {
if !self.may_build() && !self.can_place_on_in_adventure(hit.pos) {
return false;
}
let [x, y, z] = hit.pos;
@@ -2012,4 +2010,107 @@ mod tests {
assert_eq!(clamp_map_coordinate(-70.0), -128);
assert_eq!(clamp_map_coordinate(10.25), 21);
}
/// `ItemStack.useOn` / `MapItem.useOn`: in adventure mode, a map whose
/// `can_place_on` component includes the banner block toggles that banner's
/// marker, while one without does not.
#[test]
fn map_on_banner_in_adventure_mode_honours_can_place_on() {
use crate::game::{Audio, Game, Options, Progress, Settings, load_resources};
use horizon_client::{chunk::Chunk, command::args::GameMode};
use std::sync::Arc;
let Some(root) = std::env::var_os("HORIZON_TEST_RESOURCES") else {
return;
};
let root = std::path::PathBuf::from(root);
let settings = Settings::default();
let loaded = load_resources(
root.clone(),
None,
&root,
&settings,
std::time::Instant::now(),
false,
true,
&Progress::new(),
&mut |_, _| unreachable!(),
)
.unwrap();
let mut options = Options::parse(std::iter::empty()).unwrap().unwrap();
options.resources = root.clone();
options.execution_role = crate::game::execution_role::ExecutionRole::IntegratedServer;
let mut game = Game::new(
&loaded.resources,
loaded.pack,
loaded.gui.unwrap(),
options,
settings,
Audio::silent(&root),
None,
)
.unwrap();
let pos = [0, 64, 0];
game.world
.chunks
.insert((0, 0), Arc::new(Chunk::empty(0, 0)));
let banner_state = game
.pack
.assets
.registry
.default_state("minecraft:red_banner")
.unwrap();
game.world
.set(pos[0], pos[1], pos[2], banner_state)
.unwrap();
game.world.block_entities.insert(
(pos[0], pos[1], pos[2]),
horizon_client::containers::BlockEntity::Banner(horizon_client::containers::Banner {
layers: Vec::new(),
name: None,
components: Default::default(),
}),
);
let map_id = 42;
game.maps
.data
.insert(map_id, MapData::fresh(0.0, 0.0, 0, "minecraft:overworld"));
game.set_mode(GameMode::Adventure);
let hit = super::super::Hit {
pos,
face: [0, 1, 0],
point: [
pos[0] as f64 + 0.5,
pos[1] as f64 + 0.5,
pos[2] as f64 + 0.5,
],
};
let mut stack = ItemStack::new("minecraft:filled_map", 1);
stack.data_mut().map_id = Some(map_id);
// Without can_place_on, use_map_on_banner fails in adventure mode.
*game.held_mut() = Some(stack.clone());
assert!(!game.use_map_on_banner(&hit, &stack));
assert!(game.map(map_id).unwrap().banners.is_empty());
// With can_place_on listing the banner block, the marker toggles.
stack.data_mut().other.push((
"minecraft:can_place_on".into(),
Tag::compound([(
"blocks",
Tag::List(vec![Tag::String("minecraft:red_banner".into())]),
)]),
));
*game.held_mut() = Some(stack.clone());
assert!(game.use_map_on_banner(&hit, &stack));
assert_eq!(game.map(map_id).unwrap().banners.len(), 1);
assert_eq!(game.map(map_id).unwrap().banners[0].pos, pos);
assert_eq!(game.map(map_id).unwrap().banners[0].color, "red");
// Second use toggles the marker off.
assert!(game.use_map_on_banner(&hit, &stack));
assert!(game.map(map_id).unwrap().banners.is_empty());
}
}