Merge pull request #471 from ludovic111/fix/lan-guest-containers
LAN guest: their own ender chest, and double chests as 9x6
This commit is contained in:
@@ -1031,6 +1031,9 @@ struct Game<'a> {
|
||||
saved_for_exit: bool,
|
||||
/// The block a guest is acting on (game/lan_actions.rs).
|
||||
forced_hit: Option<Hit>,
|
||||
/// A guest's `ServerboundUseItemPacket`: the held item alone, no block
|
||||
/// or entity (`ServerGamePacketListenerImpl.handleUseItem`).
|
||||
lan_item_only: bool,
|
||||
/// Level events of this tick for the guests, and the guest acting now.
|
||||
lan_events: Vec<(i32, [i32; 3], i32, Option<u32>)>,
|
||||
/// Block events (position, b0, b1, state) and entity events (id, event,
|
||||
@@ -2375,17 +2378,18 @@ impl Game<'_> {
|
||||
{
|
||||
return Ok(Some(Hand::Main));
|
||||
}
|
||||
if self.interact_frame()
|
||||
|| self.interact_armor_stand()
|
||||
|| self.interact_boat()
|
||||
|| self.interact_cart()
|
||||
|| self.interact_mob()?
|
||||
|| self.interact_knot()
|
||||
if !self.lan_item_only
|
||||
&& (self.interact_frame()
|
||||
|| self.interact_armor_stand()
|
||||
|| self.interact_boat()
|
||||
|| self.interact_cart()
|
||||
|| self.interact_mob()?
|
||||
|| self.interact_knot())
|
||||
{
|
||||
return Ok(Some(Hand::Main));
|
||||
}
|
||||
// An EntityHitResult may fall through to Item.use, never to useItemOn.
|
||||
let block_hit = if self.entity_targeted() {
|
||||
let block_hit = if self.lan_item_only || self.entity_targeted() {
|
||||
None
|
||||
} else {
|
||||
self.pick()
|
||||
@@ -10213,6 +10217,7 @@ pub fn run() -> Result<()> {
|
||||
text_selection: Default::default(),
|
||||
saved_for_exit: false,
|
||||
forced_hit: None,
|
||||
lan_item_only: false,
|
||||
lan_events: Vec::new(),
|
||||
lan_block_events: Vec::new(),
|
||||
lan_entity_events: Vec::new(),
|
||||
|
||||
@@ -11,7 +11,10 @@ use horizon_client::{
|
||||
containers::BlockEntity,
|
||||
entities::Experience,
|
||||
items::ItemStack,
|
||||
lan::{guest::SPECTATOR, menu::Rules},
|
||||
lan::{
|
||||
guest::SPECTATOR,
|
||||
menu::{Rules, Slots},
|
||||
},
|
||||
motion::{MotionEvent, MotionWorld, PlayerMotion},
|
||||
};
|
||||
use horizon_core::geometry::Vec3;
|
||||
@@ -145,10 +148,18 @@ impl Game<'_> {
|
||||
self.right = play.using.is_some() || self.using.is_some();
|
||||
let riding = self.riding.take();
|
||||
let host_station = self.station.take();
|
||||
// The guest's own ender inventory; one the host has open goes back
|
||||
// into the host's while the guest acts.
|
||||
let host_ender = self.ender_open;
|
||||
self.close_ender_chest();
|
||||
std::mem::swap(&mut self.ender_items, &mut g.ender_items);
|
||||
self.lan_acting = Some(conn);
|
||||
|
||||
let result = f(self);
|
||||
self.lan_acting = None;
|
||||
// An ender chest the guest opened: the menu shows their inventory
|
||||
// itself (lan_open_container), not a stand-in block entity.
|
||||
self.close_ender_chest();
|
||||
|
||||
// What the action opened: handed to the guest instead of the host.
|
||||
let opened = self.container.take();
|
||||
@@ -172,6 +183,13 @@ impl Game<'_> {
|
||||
self.keys.remove(&KeyCode::ShiftLeft);
|
||||
}
|
||||
let g = &mut host.hub.guests[i];
|
||||
std::mem::swap(&mut self.ender_items, &mut g.ender_items);
|
||||
if let Some(key) = host_ender {
|
||||
self.world
|
||||
.block_entities
|
||||
.insert(key, BlockEntity::Chest(self.ender_items.clone()));
|
||||
self.ender_open = Some(key);
|
||||
}
|
||||
let new_xp = std::mem::replace(&mut self.experience, xp);
|
||||
g.xp_level = new_xp.level;
|
||||
g.xp_progress = new_xp.progress;
|
||||
@@ -240,30 +258,51 @@ impl Game<'_> {
|
||||
/// Hand a container the guest opened over (ServerPlayer.openMenu);
|
||||
/// false when it is not one a guest can open.
|
||||
fn lan_open_container(&mut self, host: &mut LanHost, conn: u32, pos: (i32, i32, i32)) -> bool {
|
||||
let Some(entity) = self.world.block_entities.get(&pos) else {
|
||||
return false;
|
||||
};
|
||||
let (menu, title) = match entity {
|
||||
BlockEntity::Chest(items) => match items.len() {
|
||||
27 => ("minecraft:generic_9x3", "container.chest"),
|
||||
9 => ("minecraft:generic_3x3", "container.dispenser"),
|
||||
5 => ("minecraft:hopper", "container.hopper"),
|
||||
_ => return false,
|
||||
},
|
||||
_ => return false,
|
||||
};
|
||||
let name = &self
|
||||
.res
|
||||
.facts
|
||||
.get(self.world.block(pos.0, pos.1, pos.2))
|
||||
.name;
|
||||
let title = match &**name {
|
||||
"minecraft:barrel" => "container.barrel",
|
||||
"minecraft:dropper" => "container.dropper",
|
||||
n if n.ends_with("shulker_box") => "container.shulkerBox",
|
||||
_ => title,
|
||||
let (menu, title, slots) = if &**name == "minecraft:ender_chest" {
|
||||
// EnderChestBlock.useWithoutItem: the player's own inventory.
|
||||
(
|
||||
"minecraft:generic_9x3",
|
||||
"container.enderchest",
|
||||
Slots::Ender,
|
||||
)
|
||||
} else if let Some((other, right)) = self.chest_partner(pos) {
|
||||
// ChestBlock.combine: the right half's slots come first.
|
||||
let halves = if right {
|
||||
Slots::Double(pos, other)
|
||||
} else {
|
||||
Slots::Double(other, pos)
|
||||
};
|
||||
("minecraft:generic_9x6", "container.chestDouble", halves)
|
||||
} else {
|
||||
let Some(BlockEntity::Chest(items)) = self.world.block_entities.get(&pos) else {
|
||||
return false;
|
||||
};
|
||||
let (menu, title) = match items.len() {
|
||||
27 => ("minecraft:generic_9x3", "container.chest"),
|
||||
9 => ("minecraft:generic_3x3", "container.dispenser"),
|
||||
5 => ("minecraft:hopper", "container.hopper"),
|
||||
_ => return false,
|
||||
};
|
||||
let title = match &**name {
|
||||
"minecraft:barrel" => "container.barrel",
|
||||
"minecraft:dropper" => "container.dropper",
|
||||
n if n.ends_with("shulker_box") => "container.shulkerBox",
|
||||
_ => title,
|
||||
};
|
||||
(menu, title, Slots::Block(pos))
|
||||
};
|
||||
let Some(items) = host
|
||||
.hub
|
||||
.guest(conn)
|
||||
.and_then(|g| slots.read(&self.world, g))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let items = entity.slots().to_vec();
|
||||
// ServerPlayer.openMenu closes the menu that was open.
|
||||
self.guest_container_closed(host, conn);
|
||||
host.hub.open_container(
|
||||
@@ -271,6 +310,7 @@ impl Game<'_> {
|
||||
menu,
|
||||
horizon_protocol::text::translatable(title, &[]),
|
||||
pos,
|
||||
slots,
|
||||
items,
|
||||
);
|
||||
true
|
||||
@@ -401,8 +441,13 @@ impl Game<'_> {
|
||||
if spectator {
|
||||
return;
|
||||
}
|
||||
// handleUseItem: the client's rotation, then the item alone. What it
|
||||
// looked at a moment ago (a block, a mob) is UseItemOn's or Interact's.
|
||||
self.as_guest(host, conn, |g| {
|
||||
(g.yaw, g.pitch) = (p.yaw, p.pitch);
|
||||
g.lan_item_only = true;
|
||||
let _ = g.use_item();
|
||||
g.lan_item_only = false;
|
||||
});
|
||||
}
|
||||
S::Attack(p) => self.guest_attack(host, conn, p.id),
|
||||
|
||||
@@ -90,6 +90,8 @@ pub(super) struct Remote {
|
||||
loaded_sent: bool,
|
||||
/// Open container: (window id, menu name).
|
||||
menu: Option<(i32, String)>,
|
||||
/// The open menu's title as the server named it.
|
||||
menu_title: Option<String>,
|
||||
state_id: i32,
|
||||
use_target: Option<[i32; 3]>,
|
||||
entities: HashMap<i32, Tracked>,
|
||||
@@ -231,6 +233,7 @@ impl Game<'_> {
|
||||
carried: None,
|
||||
loaded_sent: false,
|
||||
menu: None,
|
||||
menu_title: None,
|
||||
state_id: 0,
|
||||
use_target: None,
|
||||
entities: HashMap::new(),
|
||||
@@ -2551,6 +2554,7 @@ impl Game<'_> {
|
||||
fn remote_close_local(&mut self) {
|
||||
if let Some(r) = self.remote.as_mut() {
|
||||
r.menu = None;
|
||||
r.menu_title = None;
|
||||
}
|
||||
if self
|
||||
.station
|
||||
@@ -2679,19 +2683,49 @@ impl Game<'_> {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let title = plain(
|
||||
&p.title,
|
||||
&self
|
||||
.remote
|
||||
.as_ref()
|
||||
.map(|r| r.lang.clone())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
if let Some(r) = self.remote.as_mut() {
|
||||
r.menu = Some((p.container, menu.clone()));
|
||||
r.menu_title = Some(title);
|
||||
}
|
||||
self.craft_size = if menu == "minecraft:crafting" { 3 } else { 2 };
|
||||
self.container = None;
|
||||
if let Some(entity) = entity {
|
||||
let key = target.unwrap_or((i32::MIN, 0, 0));
|
||||
// The clicked block shows the menu when it holds that container;
|
||||
// an ender chest's or a double chest's slots are no one block's,
|
||||
// so they go under a key no block has.
|
||||
let holds = |k: &(i32, i32, i32)| {
|
||||
let name = &self.res.facts.get(self.world.block(k.0, k.1, k.2)).name;
|
||||
BlockEntity::for_block(name).is_some_and(|e| {
|
||||
e.id() == entity.id() && e.slots().len() == entity.slots().len()
|
||||
})
|
||||
};
|
||||
let key = target.filter(holds).unwrap_or((i32::MIN, 0, 0));
|
||||
self.world.block_entities.insert(key, entity);
|
||||
self.container = Some(key);
|
||||
}
|
||||
self.screen = Screen::Inventory;
|
||||
self.capture(false);
|
||||
}
|
||||
/// A server's chest-like menu (`MenuType.GENERIC_9xN`): its rows and title.
|
||||
pub(super) fn remote_chest_menu(&self) -> Option<(usize, String)> {
|
||||
let r = self.remote.as_ref()?;
|
||||
let rows = r
|
||||
.menu
|
||||
.as_ref()?
|
||||
.1
|
||||
.strip_prefix("minecraft:generic_9x")?
|
||||
.parse()
|
||||
.ok()?;
|
||||
Some((rows, r.menu_title.clone().unwrap_or_default()))
|
||||
}
|
||||
pub(super) fn remote_respawn(&mut self) {
|
||||
self.send(S::ClientCommand(play::ClientCommand { action: 0 }));
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ fn facing_step(facing: &str) -> (i32, i32) {
|
||||
impl Game<'_> {
|
||||
/// The other half of the open double chest and whether the open one is
|
||||
/// the right half (`ChestBlock.getConnectedDirection`).
|
||||
fn chest_partner(&self, key: (i32, i32, i32)) -> Option<((i32, i32, i32), bool)> {
|
||||
pub(super) fn chest_partner(&self, key: (i32, i32, i32)) -> Option<((i32, i32, i32), bool)> {
|
||||
let registry = self.res.assets.registry;
|
||||
let state = registry.state(self.world.block(key.0, key.1, key.2))?;
|
||||
if !matches!(
|
||||
@@ -169,6 +169,9 @@ impl Game<'_> {
|
||||
/// Rows of the open generic 9-wide container (`ChestMenu`), or None for
|
||||
/// the other menus (hoppers, dispensers, shulker boxes, furnaces...).
|
||||
pub(super) fn chest_rows(&self) -> Option<usize> {
|
||||
if let Some((rows, _)) = self.remote_chest_menu() {
|
||||
return Some(rows);
|
||||
}
|
||||
if !matches!(self.container_kind()?, BlockEntity::Chest(_)) {
|
||||
return None;
|
||||
}
|
||||
@@ -185,6 +188,9 @@ impl Game<'_> {
|
||||
}
|
||||
/// The title of the open chest-like menu.
|
||||
fn chest_title(&self, rows: usize) -> String {
|
||||
if let Some((_, title)) = self.remote_chest_menu() {
|
||||
return title;
|
||||
}
|
||||
let (key, fallback) = match self.container_block().as_deref() {
|
||||
Some("minecraft:barrel") => ("container.barrel", "Barrel"),
|
||||
Some("minecraft:ender_chest") => ("container.enderchest", "Ender Chest"),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! level events, animations and block containers with their clicks.
|
||||
use super::{
|
||||
hub::Hub,
|
||||
menu::{self, Menu, Rules, View},
|
||||
menu::{self, Menu, Rules, Slots, View},
|
||||
};
|
||||
use crate::{items::ItemStack, world::World};
|
||||
use horizon_protocol::{
|
||||
@@ -143,6 +143,7 @@ impl Hub {
|
||||
menu_kind: &str,
|
||||
title: Tag,
|
||||
pos: (i32, i32, i32),
|
||||
slots: Slots,
|
||||
items: Vec<Option<ItemStack>>,
|
||||
) {
|
||||
let Some(kind) = self
|
||||
@@ -169,6 +170,7 @@ impl Hub {
|
||||
window,
|
||||
kind,
|
||||
pos: Some(pos),
|
||||
slots,
|
||||
size: items.len(),
|
||||
state: 0,
|
||||
sent: Vec::new(),
|
||||
@@ -265,10 +267,7 @@ impl Hub {
|
||||
fn view_slot(&self, i: usize, world: &World, slot: usize) -> Option<ItemStack> {
|
||||
let g = &self.guests[i];
|
||||
match &g.menu {
|
||||
Some(m) if slot < m.size => {
|
||||
let e = world.block_entities.get(&m.pos?)?;
|
||||
e.slots().get(slot).cloned().flatten()
|
||||
}
|
||||
Some(m) if slot < m.size => m.slots.get(world, g, slot),
|
||||
Some(m) => g
|
||||
.inventory
|
||||
.get(player_slot(slot - m.size))
|
||||
@@ -299,32 +298,32 @@ impl Hub {
|
||||
if !window_ok || self.guests[i].spectator() {
|
||||
return Vec::new();
|
||||
}
|
||||
let pos = self.guests[i].menu.as_ref().and_then(|m| m.pos);
|
||||
let source = self.guests[i].menu.as_ref().map(|m| m.slots);
|
||||
let mut drag = if in_menu {
|
||||
self.guests[i].menu.as_mut().and_then(|m| m.drag.take())
|
||||
} else {
|
||||
self.guests[i].drag.take()
|
||||
};
|
||||
let (drops, changed) = {
|
||||
let guest = &mut self.guests[i];
|
||||
let mut entity = pos.and_then(|k| world.block_entities.get_mut(&k));
|
||||
let slots = entity.as_mut().map(|e| e.slots_mut());
|
||||
if in_menu && slots.is_none() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut view = View {
|
||||
guest,
|
||||
container: slots,
|
||||
};
|
||||
menu::click(
|
||||
&mut view,
|
||||
&mut drag,
|
||||
i32::from(p.slot),
|
||||
i32::from(p.button),
|
||||
p.input,
|
||||
rules,
|
||||
)
|
||||
let guest = &mut self.guests[i];
|
||||
let mut items = source.and_then(|s| s.read(world, guest));
|
||||
if in_menu && items.is_none() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut view = View {
|
||||
guest,
|
||||
container: items.as_deref_mut(),
|
||||
};
|
||||
let (drops, changed) = menu::click(
|
||||
&mut view,
|
||||
&mut drag,
|
||||
i32::from(p.slot),
|
||||
i32::from(p.button),
|
||||
p.input,
|
||||
rules,
|
||||
);
|
||||
if let (Some(source), Some(items)) = (source, items) {
|
||||
source.write(world, guest, items);
|
||||
}
|
||||
if in_menu {
|
||||
if let Some(m) = self.guests[i].menu.as_mut() {
|
||||
m.drag = drag;
|
||||
@@ -427,8 +426,8 @@ impl Hub {
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let (size, window, pos) = (menu.size, menu.window, menu.pos);
|
||||
if !pos.is_some_and(|k| world.block_entities.contains_key(&k)) {
|
||||
let (size, window, slots) = (menu.size, menu.window, menu.slots);
|
||||
if !slots.valid(world) {
|
||||
self.close_container_at(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -84,6 +84,8 @@ pub struct Guest {
|
||||
/// (`InventoryMenu.craftSlots`, `resultSlots`), which the game computes.
|
||||
pub craft_grid: [Option<ItemStack>; 4],
|
||||
pub craft_result: Option<ItemStack>,
|
||||
/// The ender chest inventory (`PlayerEnderChestContainer`, 27 slots).
|
||||
pub ender_items: Vec<Option<ItemStack>>,
|
||||
pub health: f32,
|
||||
pub food: i32,
|
||||
pub saturation: f32,
|
||||
@@ -165,6 +167,7 @@ impl Guest {
|
||||
carried: None,
|
||||
craft_grid: Default::default(),
|
||||
craft_result: None,
|
||||
ender_items: vec![None; 27],
|
||||
health: 20.0,
|
||||
food: 20,
|
||||
saturation: 5.0,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! clone, throw, quick craft (drag) and pickup all; items thrown out of a
|
||||
//! menu are returned for the game to drop.
|
||||
use super::guest::Guest;
|
||||
use crate::items::ItemStack;
|
||||
use crate::{items::ItemStack, world::World};
|
||||
|
||||
/// What the menus need to know about items.
|
||||
pub trait Rules {
|
||||
@@ -19,6 +19,8 @@ pub struct Menu {
|
||||
/// The `minecraft:menu` registry id.
|
||||
pub kind: i32,
|
||||
pub pos: Option<(i32, i32, i32)>,
|
||||
/// Where the container's slots live.
|
||||
pub slots: Slots,
|
||||
/// Slots of the container itself (without the 36 inventory slots).
|
||||
pub size: usize,
|
||||
pub state: i32,
|
||||
@@ -28,6 +30,75 @@ pub struct Menu {
|
||||
pub drag: Option<(u8, Vec<usize>)>,
|
||||
}
|
||||
|
||||
/// Where the slots of a container menu live.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Slots {
|
||||
/// One block entity.
|
||||
Block((i32, i32, i32)),
|
||||
/// A double chest (`CompoundContainer`): the first half's slots, then
|
||||
/// the second's.
|
||||
Double((i32, i32, i32), (i32, i32, i32)),
|
||||
/// The player's own ender chest inventory (`PlayerEnderChestContainer`).
|
||||
Ender,
|
||||
}
|
||||
|
||||
impl Slots {
|
||||
fn halves(self) -> Vec<(i32, i32, i32)> {
|
||||
match self {
|
||||
Slots::Block(k) => vec![k],
|
||||
Slots::Double(a, b) => vec![a, b],
|
||||
Slots::Ender => Vec::new(),
|
||||
}
|
||||
}
|
||||
/// Whether the block entities behind the slots are still there
|
||||
/// (`Container.stillValid`).
|
||||
pub fn valid(self, world: &World) -> bool {
|
||||
self.halves()
|
||||
.iter()
|
||||
.all(|k| world.block_entities.contains_key(k))
|
||||
}
|
||||
/// Container slot `n`.
|
||||
pub fn get(self, world: &World, guest: &Guest, mut n: usize) -> Option<ItemStack> {
|
||||
if self == Slots::Ender {
|
||||
return guest.ender_items.get(n).cloned().flatten();
|
||||
}
|
||||
for k in self.halves() {
|
||||
let slots = world.block_entities.get(&k)?.slots();
|
||||
if n < slots.len() {
|
||||
return slots[n].clone();
|
||||
}
|
||||
n -= slots.len();
|
||||
}
|
||||
None
|
||||
}
|
||||
/// Every container slot, in menu order.
|
||||
pub fn read(self, world: &World, guest: &Guest) -> Option<Vec<Option<ItemStack>>> {
|
||||
if self == Slots::Ender {
|
||||
return Some(guest.ender_items.clone());
|
||||
}
|
||||
let mut items = Vec::new();
|
||||
for k in self.halves() {
|
||||
items.extend_from_slice(world.block_entities.get(&k)?.slots());
|
||||
}
|
||||
Some(items)
|
||||
}
|
||||
/// Store what `read` gave, changed.
|
||||
pub fn write(self, world: &mut World, guest: &mut Guest, items: Vec<Option<ItemStack>>) {
|
||||
if self == Slots::Ender {
|
||||
guest.ender_items = items;
|
||||
return;
|
||||
}
|
||||
let mut rest = items.into_iter();
|
||||
for k in self.halves() {
|
||||
if let Some(e) = world.block_entities.get_mut(&k) {
|
||||
for slot in e.slots_mut() {
|
||||
*slot = rest.next().flatten();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Slots of a menu seen as one list.
|
||||
pub struct View<'a> {
|
||||
pub guest: &'a mut Guest,
|
||||
|
||||
@@ -62,6 +62,9 @@ pub fn load(guest: &mut Guest, world: &Path) -> bool {
|
||||
guest.game_mode = level.mode();
|
||||
guest.selected = (level.selected_slot.max(0) as usize).min(8);
|
||||
guest.saved = Some(tag);
|
||||
if level.ender_items.len() == 27 {
|
||||
guest.ender_items = level.ender_items.clone();
|
||||
}
|
||||
for (slot, item, count, damage) in &level.inventory {
|
||||
let mut stack = ItemStack {
|
||||
item: Arc::from(item.as_str()),
|
||||
@@ -103,6 +106,7 @@ pub fn save(guest: &Guest, world: &Path) {
|
||||
on_ground: guest.on_ground,
|
||||
dimension: "minecraft:overworld".into(),
|
||||
player_extra: guest.saved.clone(),
|
||||
ender_items: guest.ender_items.clone(),
|
||||
..Level::default()
|
||||
};
|
||||
let mut put = |slot: u8, stack: &Option<ItemStack>| {
|
||||
|
||||
+68
-3
@@ -16,7 +16,9 @@ address. The script then checks, from both sides, what a guest's game used to ge
|
||||
- mobs: with the host in creative, a zombie and a skeleton go for the guest and hurt them;
|
||||
- hazards: a guest in survival gets hungry sprint-jumping and is hurt by lava, a block at the eyes,
|
||||
drowning and the void (then respawns);
|
||||
- pvp: the guest and the host hit each other with a sword; with the pvp game rule off, nothing.
|
||||
- pvp: the guest and the host hit each other with a sword; with the pvp game rule off, nothing;
|
||||
- containers: the guest's ender chest holds the guest's items, not the host's, and a double chest
|
||||
opens as 9x6 with the right half first.
|
||||
|
||||
Each check is reported on its own (a failing one doesn't stop the others) and the screenshots of
|
||||
both sides go to --out (host-*.png, guest-*.png).
|
||||
@@ -43,7 +45,7 @@ from agent_control_e2e import Cli, Failed, game_env # noqa: E402
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
GUEST_NAME = 'Guest'
|
||||
CHECKS = ['tab', 'self', 'armour', 'nametags', 'chest', 'crafting', 'damage', 'mobs', 'hazards', 'pvp']
|
||||
CHECKS = ['tab', 'self', 'armour', 'nametags', 'chest', 'crafting', 'damage', 'mobs', 'hazards', 'pvp', 'containers']
|
||||
|
||||
|
||||
class Game:
|
||||
@@ -389,9 +391,72 @@ def check_pvp(host, guest, out):
|
||||
return hits
|
||||
|
||||
|
||||
def container_items(game):
|
||||
"""The open container's stacks by slot (Horizon's container slots are 300 and up) and its size."""
|
||||
slots = [s for s in game('screen').get('slots', []) if s.get('slot', 0) >= 300]
|
||||
return {s['slot'] - 300: s['item'] for s in slots if s.get('item')}, len(slots)
|
||||
|
||||
|
||||
def open_container(guest, pos, out, label, take=None):
|
||||
"""Open the container at `pos` and read it; `take` shift-clicks that container slot out first."""
|
||||
guest.raw('interact', *pos)
|
||||
guest.raw('wait-for', 'screen', 'container', '--timeout', 10)
|
||||
# The server's menu (an ender or double chest has no stand-in block entity: key i32::MIN),
|
||||
# then its contents, can come a few ticks after the screen.
|
||||
for _ in range(20):
|
||||
guest.wait(2)
|
||||
screen = guest('screen')
|
||||
items = {s['slot']: s.get('item') for s in screen.get('slots', [])}
|
||||
if screen.get('container') == [-2**31, 0, 0] and (take is None or items.get(300 + take)):
|
||||
break
|
||||
guest.wait(5)
|
||||
if take is not None:
|
||||
guest.raw('click-slot', 300 + take, 'shift')
|
||||
guest.wait(5)
|
||||
items = container_items(guest)
|
||||
guest.shot(out, label)
|
||||
guest.raw('key', 'esc', 'tap')
|
||||
guest.wait(5)
|
||||
return items
|
||||
|
||||
|
||||
def check_containers(host, guest, out):
|
||||
host('command', command='item replace entity @s enderchest.0 with minecraft:diamond')
|
||||
guest('command', command='item replace entity @s enderchest.4 with minecraft:emerald')
|
||||
x, y, z = (int(v // 1) for v in pos_of(guest))
|
||||
guest('command', command=f'setblock {x} {y} {z + 2} minecraft:ender_chest')
|
||||
guest.wait(10)
|
||||
ender, ender_size = open_container(guest, (x, y, z + 2), out, 'ender-chest')
|
||||
# Facing north, a left half pairs with the block to its east (ChestBlock.getConnectedDirection).
|
||||
guest('command', command=f'setblock {x} {y} {z - 2} minecraft:chest[facing=north,type=left]')
|
||||
guest('command', command=f'setblock {x + 1} {y} {z - 2} minecraft:chest[facing=north,type=right]')
|
||||
guest('command', command=f'item replace block {x + 1} {y} {z - 2} container.0 with minecraft:gold_ingot')
|
||||
guest('command', command=f'item replace block {x} {y} {z - 2} container.0 with minecraft:iron_ingot')
|
||||
guest.wait(10)
|
||||
double, double_size = open_container(guest, (x, y, z - 2), out, 'double-chest')
|
||||
# Taking from each writes back to the guest's ender inventory and to the right half.
|
||||
guest('command', command='clear @s')
|
||||
open_container(guest, (x, y, z + 2), out, 'ender-chest-take', take=4)
|
||||
open_container(guest, (x, y, z - 2), out, 'double-chest-take', take=0)
|
||||
taken = guest('inventory')['counts']
|
||||
after = (open_container(guest, (x, y, z + 2), out, 'ender-chest-after')[0],
|
||||
open_container(guest, (x, y, z - 2), out, 'double-chest-after')[0])
|
||||
host_ender = host('command', command='data get entity @s EnderItems')
|
||||
for at in ((x, y, z + 2), (x, y, z - 2), (x + 1, y, z - 2)):
|
||||
guest('command', command=f'setblock {at[0]} {at[1]} {at[2]} minecraft:air')
|
||||
assert ender == {4: 'minecraft:emerald'} and ender_size == 27, f'the guest\'s ender chest: {ender} ({ender_size})'
|
||||
assert double_size == 54, f'the double chest opened with {double_size} slots'
|
||||
assert double == {0: 'minecraft:gold_ingot', 27: 'minecraft:iron_ingot'}, f'the double chest: {double}'
|
||||
assert 'emerald' not in json.dumps(host_ender), f'the guest\'s item went to the host: {host_ender}'
|
||||
assert taken.get('minecraft:emerald') == 1 and taken.get('minecraft:gold_ingot') == 1, f'taken: {taken}'
|
||||
assert after == ({}, {27: 'minecraft:iron_ingot'}), f'after taking: {after}'
|
||||
return {'ender': ender, 'double': double, 'taken': taken}
|
||||
|
||||
|
||||
CHECK_FNS = {'tab': check_tab, 'self': check_self, 'armour': check_armour, 'nametags': check_nametags,
|
||||
'chest': check_chest, 'crafting': check_crafting, 'damage': check_damage,
|
||||
'mobs': check_mobs, 'hazards': check_hazards, 'pvp': check_pvp}
|
||||
'mobs': check_mobs, 'hazards': check_hazards, 'pvp': check_pvp,
|
||||
'containers': check_containers}
|
||||
|
||||
|
||||
def run(ctl, out, resources, checks):
|
||||
|
||||
Reference in New Issue
Block a user