Read the physical Shift keys in container screens and drop a stack clicked outside
Quick move, the recipe book's place-all, its search box and the hold-Shift tooltips read self.keys, which holds remapped and toggled keys: with toggle sneak, a Left Shift press behind a screen is dropped by the sneak mapping and sync_toggle then removes ShiftLeft. Vanilla reads the event's own modifiers (InputWithModifiers.hasShiftDown) or Screen.hasShiftDown: the physical keys. Track them in physical_keys and read them through has_shift_down. Clicking outside the window did nothing. AbstractContainerScreen.mouseClicked sends slot -999 when hasClickedOutside (AbstractRecipeBookScreen's rule when the screen has a book): a held stack starts a quick craft drag whose release sends PICKUP -999, which AbstractContainerMenu.doClick turns into a drop of the stack (left) or one item (right), thrown from the hand with a swing. The mouse handling is now horizon_client::container_screen, checked against the real screens by tools/test_container_screen_parity.py; servers get the same ContainerClick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
#![forbid(unsafe_code)]
|
||||
//! Runs tools/test_container_screen_parity.py's screens and mouse events
|
||||
//! through `horizon_client::container_screen`.
|
||||
use horizon_client::{
|
||||
Result,
|
||||
container_screen::{Book, Button, Clicks, Image, Release, Target, clicked_outside},
|
||||
menu::Input,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, BufWriter, Write},
|
||||
};
|
||||
|
||||
fn input_name(input: Input) -> &'static str {
|
||||
match input {
|
||||
Input::Pickup => "PICKUP",
|
||||
Input::QuickMove => "QUICK_MOVE",
|
||||
Input::Swap => "SWAP",
|
||||
Input::Clone => "CLONE",
|
||||
Input::Throw => "THROW",
|
||||
Input::QuickCraft => "QUICK_CRAFT",
|
||||
Input::PickupAll => "PICKUP_ALL",
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let input = BufReader::new(File::open(&args[1])?);
|
||||
let mut out = BufWriter::new(File::create(&args[2])?);
|
||||
for (n, line) in input.lines().enumerate() {
|
||||
let c: Value = serde_json::from_str(&line?)?;
|
||||
writeln!(out, "== {n}")?;
|
||||
let int = |v: &Value| v.as_i64().unwrap() as i32;
|
||||
let image = Image {
|
||||
left: int(&c["image"][0]),
|
||||
top: int(&c["image"][1]),
|
||||
width: int(&c["image"][2]),
|
||||
height: int(&c["image"][3]),
|
||||
};
|
||||
let book = c.get("book").map(|b| Book {
|
||||
visible: b[0].as_bool().unwrap(),
|
||||
selected_tab_hovered: b[1].as_bool().unwrap(),
|
||||
});
|
||||
let mut clicks = Clicks::default();
|
||||
for ev in c["events"].as_array().unwrap() {
|
||||
let name = ev[0].as_str().unwrap();
|
||||
let (x, y) = (ev[1].as_f64().unwrap(), ev[2].as_f64().unwrap());
|
||||
let outside = clicked_outside(x, y, image, book);
|
||||
let mut line = format!("{name} {outside}");
|
||||
if name != "point" {
|
||||
// The case says whether the event is over the menu's slot (7).
|
||||
let on_slot = ev[6].as_bool().unwrap();
|
||||
line += &format!(" {on_slot}");
|
||||
let target = Target::of(on_slot.then_some(7), outside);
|
||||
let button = match ev[3].as_i64().unwrap() {
|
||||
1 => Button::Left,
|
||||
3 => Button::Right,
|
||||
_ => Button::Middle,
|
||||
};
|
||||
let shift = ev[4].as_i64().unwrap() & 3 != 0;
|
||||
let carried_empty = !ev[5].as_bool().unwrap();
|
||||
let click = if name == "press" {
|
||||
clicks.press(&target, button, carried_empty, shift, false)
|
||||
} else {
|
||||
match clicks.release(&target, button, carried_empty, false, shift, false) {
|
||||
Release::Click(input) => Some(input),
|
||||
Release::QuickCraft => panic!("no drags in these cases"),
|
||||
Release::Nothing => None,
|
||||
}
|
||||
};
|
||||
if let Some(input) = click {
|
||||
let id = match target {
|
||||
Target::Slot(s) => s,
|
||||
Target::Outside => -999,
|
||||
Target::Nothing => -1,
|
||||
};
|
||||
line += &format!(" click {id} {} {}", button.menu_button(), input_name(input));
|
||||
}
|
||||
}
|
||||
writeln!(out, "{line}")?;
|
||||
}
|
||||
}
|
||||
out.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
//! The mouse handling every survival and container screen shares
|
||||
//! (`AbstractContainerScreen.mouseClicked` / `mouseReleased`): which slot id
|
||||
//! a click targets (-999 outside the window, `hasClickedOutside` with the
|
||||
//! recipe book's own rule) and which `ContainerInput` it sends. Shift is
|
||||
//! the event's own modifiers (`InputWithModifiers.hasShiftDown`: the
|
||||
//! physical Shift keys, never the remapped or toggled sneak key).
|
||||
|
||||
use crate::menu::Input;
|
||||
|
||||
/// The screen's window: `leftPos`, `topPos`, `imageWidth`, `imageHeight`.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct Image {
|
||||
pub left: i32,
|
||||
pub top: i32,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
}
|
||||
|
||||
/// The recipe book of an `AbstractRecipeBookScreen`.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct Book {
|
||||
/// `RecipeBookComponent.isVisible`.
|
||||
pub visible: bool,
|
||||
/// `selectedTab.isHoveredOrFocused()`.
|
||||
pub selected_tab_hovered: bool,
|
||||
}
|
||||
|
||||
/// `RecipeBookComponent.IMAGE_WIDTH`.
|
||||
const BOOK_WIDTH: i32 = 147;
|
||||
|
||||
fn outside(mx: f64, my: f64, image: Image) -> bool {
|
||||
mx < f64::from(image.left)
|
||||
|| my < f64::from(image.top)
|
||||
|| mx >= f64::from(image.left + image.width)
|
||||
|| my >= f64::from(image.top + image.height)
|
||||
}
|
||||
|
||||
/// `AbstractContainerScreen.hasClickedOutside`, or
|
||||
/// `AbstractRecipeBookScreen.hasClickedOutside` when the screen has a book
|
||||
/// (`RecipeBookComponent.hasClickedOutside`: a click on the book, or with
|
||||
/// its selected tab hovered, is not outside).
|
||||
pub fn clicked_outside(mx: f64, my: f64, image: Image, book: Option<Book>) -> bool {
|
||||
let out = outside(mx, my, image);
|
||||
let Some(book) = book else {
|
||||
return out;
|
||||
};
|
||||
let book_outside = !book.visible || {
|
||||
let on_book = f64::from(image.left - BOOK_WIDTH) < mx
|
||||
&& mx < f64::from(image.left)
|
||||
&& f64::from(image.top) < my
|
||||
&& my < f64::from(image.top + image.height);
|
||||
out && !on_book && !book.selected_tab_hovered
|
||||
};
|
||||
book_outside && out
|
||||
}
|
||||
|
||||
/// What a click points at: a slot, outside the window (slot id -999), or
|
||||
/// neither (slot id -1).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Target<S> {
|
||||
Slot(S),
|
||||
Outside,
|
||||
Nothing,
|
||||
}
|
||||
|
||||
impl<S> Target<S> {
|
||||
/// Outside wins over a hovered slot, as in `mouseClicked`.
|
||||
pub fn of(slot: Option<S>, outside: bool) -> Self {
|
||||
match slot {
|
||||
_ if outside => Self::Outside,
|
||||
Some(s) => Self::Slot(s),
|
||||
None => Self::Nothing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The left, right and middle buttons (SDL's 1, 3 and 2).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Button {
|
||||
Left,
|
||||
Right,
|
||||
Middle,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
/// `getContainerClickButton`: the button a menu click carries.
|
||||
pub fn menu_button(self) -> i32 {
|
||||
match self {
|
||||
Self::Left => 0,
|
||||
Self::Right => 1,
|
||||
Self::Middle => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The input state `mouseClicked`/`mouseReleased` keep between events.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Clicks {
|
||||
/// `isQuickCrafting` with `quickCraftingButton`.
|
||||
quick_crafting: Option<Button>,
|
||||
skip_next_release: bool,
|
||||
}
|
||||
|
||||
/// What a release does.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Release {
|
||||
/// `slotClicked` with this input on the release's target.
|
||||
Click(Input),
|
||||
/// `quickCraftToSlots` over the dragged slots.
|
||||
QuickCraft,
|
||||
Nothing,
|
||||
}
|
||||
|
||||
impl Clicks {
|
||||
/// A quick craft drag in progress, and the button that started it.
|
||||
pub fn quick_crafting(&self) -> Option<Button> {
|
||||
self.quick_crafting
|
||||
}
|
||||
|
||||
/// A press Horizon handled itself (a double click's `PICKUP_ALL`): its
|
||||
/// release clicks nothing more.
|
||||
pub fn skip_release(&mut self) {
|
||||
self.quick_crafting = None;
|
||||
self.skip_next_release = true;
|
||||
}
|
||||
|
||||
/// `mouseClicked` (no double click): the `slotClicked` input it sends on
|
||||
/// `target`, if any. A press holding a stack starts a quick craft drag
|
||||
/// instead. `cloning` is `keyPickItem` with infinite materials.
|
||||
pub fn press<S>(
|
||||
&mut self,
|
||||
target: &Target<S>,
|
||||
button: Button,
|
||||
carried_empty: bool,
|
||||
shift: bool,
|
||||
cloning: bool,
|
||||
) -> Option<Input> {
|
||||
self.skip_next_release = false;
|
||||
if button == Button::Middle && !cloning {
|
||||
return None;
|
||||
}
|
||||
if matches!(target, Target::Nothing) || self.quick_crafting.is_some() {
|
||||
return None;
|
||||
}
|
||||
if !carried_empty {
|
||||
self.quick_crafting = Some(button);
|
||||
return None;
|
||||
}
|
||||
self.skip_next_release = true;
|
||||
Some(if cloning {
|
||||
Input::Clone
|
||||
} else if shift && !matches!(target, Target::Outside) {
|
||||
Input::QuickMove
|
||||
} else if matches!(target, Target::Outside) {
|
||||
Input::Throw
|
||||
} else {
|
||||
Input::Pickup
|
||||
})
|
||||
}
|
||||
|
||||
/// `mouseReleased` (no double click). `dragged` is whether the quick
|
||||
/// craft drag collected any slots; `pick_item` is the release of the
|
||||
/// `keyPickItem` button.
|
||||
pub fn release<S>(
|
||||
&mut self,
|
||||
target: &Target<S>,
|
||||
button: Button,
|
||||
carried_empty: bool,
|
||||
dragged: bool,
|
||||
shift: bool,
|
||||
pick_item: bool,
|
||||
) -> Release {
|
||||
if self.quick_crafting.is_some_and(|b| b != button) {
|
||||
self.quick_crafting = None;
|
||||
self.skip_next_release = true;
|
||||
return Release::Nothing;
|
||||
}
|
||||
if self.skip_next_release {
|
||||
self.skip_next_release = false;
|
||||
return Release::Nothing;
|
||||
}
|
||||
let quick_crafting = self.quick_crafting.take().is_some();
|
||||
if quick_crafting && dragged {
|
||||
Release::QuickCraft
|
||||
} else if carried_empty {
|
||||
Release::Nothing
|
||||
} else if pick_item {
|
||||
Release::Click(Input::Clone)
|
||||
} else if shift && !matches!(target, Target::Outside) {
|
||||
Release::Click(Input::QuickMove)
|
||||
} else {
|
||||
Release::Click(Input::Pickup)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const INV: Image = Image {
|
||||
left: 100,
|
||||
top: 50,
|
||||
width: 176,
|
||||
height: 166,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn window_edges() {
|
||||
assert!(!clicked_outside(100.0, 50.0, INV, None));
|
||||
assert!(clicked_outside(99.9, 60.0, INV, None));
|
||||
assert!(clicked_outside(276.0, 60.0, INV, None));
|
||||
assert!(!clicked_outside(275.9, 215.9, INV, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_open_book_is_not_outside() {
|
||||
let book = Book {
|
||||
visible: true,
|
||||
selected_tab_hovered: false,
|
||||
};
|
||||
assert!(!clicked_outside(10.0, 100.0, INV, Some(book)));
|
||||
assert!(clicked_outside(-60.0, 100.0, INV, Some(book)));
|
||||
assert!(!clicked_outside(
|
||||
-60.0,
|
||||
100.0,
|
||||
INV,
|
||||
Some(Book {
|
||||
selected_tab_hovered: true,
|
||||
..book
|
||||
})
|
||||
));
|
||||
let closed = Book {
|
||||
visible: false,
|
||||
..book
|
||||
};
|
||||
assert!(clicked_outside(10.0, 100.0, INV, Some(closed)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_carried_stack_thrown_outside() {
|
||||
let mut clicks = Clicks::default();
|
||||
let out = Target::<usize>::Outside;
|
||||
assert_eq!(clicks.press(&out, Button::Left, false, false, false), None);
|
||||
assert_eq!(
|
||||
clicks.release(&out, Button::Left, false, false, false, false),
|
||||
Release::Click(Input::Pickup)
|
||||
);
|
||||
// Shift never quick-moves outside, and an empty hand throws nothing.
|
||||
assert_eq!(
|
||||
clicks.press(&out, Button::Right, true, true, false),
|
||||
Some(Input::Throw)
|
||||
);
|
||||
assert_eq!(
|
||||
clicks.release(&out, Button::Right, true, false, true, false),
|
||||
Release::Nothing
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn another_button_cancels_the_drag() {
|
||||
let mut clicks = Clicks::default();
|
||||
let slot = Target::Slot(3);
|
||||
clicks.press(&slot, Button::Left, false, false, false);
|
||||
assert_eq!(
|
||||
clicks.release(&slot, Button::Right, false, true, false, false),
|
||||
Release::Nothing
|
||||
);
|
||||
assert_eq!(
|
||||
clicks.release(&slot, Button::Left, false, true, false, false),
|
||||
Release::Nothing
|
||||
);
|
||||
assert_eq!(
|
||||
clicks.release(&slot, Button::Left, false, true, true, false),
|
||||
Release::Click(Input::QuickMove)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -595,6 +595,8 @@ struct Game<'a> {
|
||||
craft_size: usize,
|
||||
/// A quick craft drag over inventory slots: (button, game slots so far).
|
||||
inv_drag: Option<(i32, Vec<usize>)>,
|
||||
/// `AbstractContainerScreen`'s quick craft and skip-release state.
|
||||
inv_clicks: horizon_client::container_screen::Clicks,
|
||||
/// The last inventory click, for the double click that gathers a stack.
|
||||
last_inv_click: Option<(std::time::Instant, usize, bool)>,
|
||||
/// Open container block entity, if any.
|
||||
@@ -611,6 +613,8 @@ struct Game<'a> {
|
||||
/// The game mode; `creative` mirrors `mode == Creative`.
|
||||
mode: horizon_client::command::args::GameMode,
|
||||
keys: HashSet<KeyCode>,
|
||||
// Physical input is separate from remapped and toggled movement keys.
|
||||
physical_keys: HashSet<KeyCode>,
|
||||
left: bool,
|
||||
right: bool,
|
||||
cursor: (f32, f32),
|
||||
@@ -4076,13 +4080,30 @@ impl Game<'_> {
|
||||
/// The plain inventory mouse press: starts a quick craft drag when a
|
||||
/// stack is held, gathers on a double click, else the normal click.
|
||||
fn inventory_press(&mut self, right: bool) {
|
||||
use horizon_client::container_screen::Target;
|
||||
use horizon_client::menu::Input;
|
||||
let slot = self.renderer.as_ref().and_then(|r| {
|
||||
let batch = HudBatch::new([r.config.width, r.config.height]);
|
||||
self.slot_at(&batch, self.cursor.0, self.cursor.1)
|
||||
});
|
||||
let shift =
|
||||
self.keys.contains(&KeyCode::ShiftLeft) || self.keys.contains(&KeyCode::ShiftRight);
|
||||
let (slot, outside) = self.inventory_target();
|
||||
let shift = self.has_shift_down();
|
||||
let target = Target::of(slot, outside);
|
||||
let input = self.inv_clicks.press(
|
||||
&target,
|
||||
Self::screen_button(right),
|
||||
self.carried.is_none(),
|
||||
shift,
|
||||
false,
|
||||
);
|
||||
if outside {
|
||||
// slotId -999: an empty hand throws nothing; a carried stack
|
||||
// starts a drag whose release drops it.
|
||||
match input {
|
||||
Some(input) => self.throw_click(right, input),
|
||||
None if self.inv_clicks.quick_crafting().is_some() => {
|
||||
self.inv_drag = Some((i32::from(right), Vec::new()));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Some(slot) = slot
|
||||
&& !shift
|
||||
&& slot != 200
|
||||
@@ -4096,6 +4117,8 @@ impl Game<'_> {
|
||||
self.last_inv_click = Some((now, slot, right));
|
||||
if double && self.menu_click(Some(slot), 0, Input::PickupAll) {
|
||||
self.last_inv_click = None;
|
||||
// The double click's own release (`mouseReleased`'s PICKUP_ALL).
|
||||
self.inv_clicks.skip_release();
|
||||
return;
|
||||
}
|
||||
if self.carried.is_some() {
|
||||
@@ -4122,6 +4145,125 @@ impl Game<'_> {
|
||||
slots.push(slot);
|
||||
}
|
||||
}
|
||||
/// The hovered slot and whether the cursor is outside the window.
|
||||
fn inventory_target(&self) -> (Option<usize>, bool) {
|
||||
let Some(r) = self.renderer.as_ref() else {
|
||||
return (None, false);
|
||||
};
|
||||
let batch = HudBatch::new([r.config.width, r.config.height]);
|
||||
(
|
||||
self.slot_at(&batch, self.cursor.0, self.cursor.1),
|
||||
self.clicked_outside(&batch),
|
||||
)
|
||||
}
|
||||
fn screen_button(right: bool) -> horizon_client::container_screen::Button {
|
||||
use horizon_client::container_screen::Button;
|
||||
if right { Button::Right } else { Button::Left }
|
||||
}
|
||||
/// `Screen.hasShiftDown` / `InputWithModifiers.hasShiftDown`: the
|
||||
/// physical Shift keys, whatever the sneak key is bound or toggled to.
|
||||
pub(super) fn has_shift_down(&self) -> bool {
|
||||
self.physical_keys.contains(&KeyCode::ShiftLeft)
|
||||
|| self.physical_keys.contains(&KeyCode::ShiftRight)
|
||||
}
|
||||
/// `hasClickedOutside` of the open screen at the cursor.
|
||||
fn clicked_outside(&self, batch: &HudBatch) -> bool {
|
||||
use horizon_client::container_screen::{Book, Image, clicked_outside};
|
||||
let (ox, oy) = self.inventory_origin(batch);
|
||||
let (shift, width) = match self.station.as_ref().map(|s| s.kind) {
|
||||
Some(stations::Kind::Merchant) => (-50.0, 276),
|
||||
Some(stations::Kind::Beacon) => (beacon::SHIFT, 230),
|
||||
_ => (0.0, 176),
|
||||
};
|
||||
let image = Image {
|
||||
left: (ox + shift) as i32,
|
||||
top: oy as i32,
|
||||
width,
|
||||
height: self.screen_height() as i32,
|
||||
};
|
||||
let book = self
|
||||
.recipe_book_area(batch)
|
||||
.map(|(visible, tab_hovered)| Book {
|
||||
visible,
|
||||
selected_tab_hovered: tab_hovered,
|
||||
});
|
||||
clicked_outside(
|
||||
f64::from(self.cursor.0),
|
||||
f64::from(self.cursor.1),
|
||||
image,
|
||||
book,
|
||||
)
|
||||
}
|
||||
/// A click outside the window (slot id -999): `THROW` does nothing; a
|
||||
/// `PICKUP` drops the carried stack (left) or one of it (right), thrown
|
||||
/// from the hand (`AbstractContainerMenu.doClick`, `LivingEntity.drop`).
|
||||
fn throw_click(&mut self, right: bool, input: horizon_client::menu::Input) {
|
||||
use horizon_client::menu::Input;
|
||||
let button = i32::from(right);
|
||||
if self.remote.is_some() {
|
||||
self.remote_throw(right, input);
|
||||
return;
|
||||
}
|
||||
let before = self.carried.clone();
|
||||
if !self.menu_click(None, button, input) && input == Input::Pickup {
|
||||
// A screen the menu engine doesn't model: the same doClick.
|
||||
if let Some(c) = self.carried.as_mut() {
|
||||
let thrown = if right {
|
||||
let one = ItemStack {
|
||||
count: 1,
|
||||
..c.clone()
|
||||
};
|
||||
c.count -= 1;
|
||||
if c.count == 0 {
|
||||
self.carried = None;
|
||||
}
|
||||
one
|
||||
} else {
|
||||
self.carried.take().expect("some")
|
||||
};
|
||||
self.player_drop(thrown);
|
||||
}
|
||||
}
|
||||
if self.carried != before {
|
||||
self.swing_arm();
|
||||
}
|
||||
if let Some(key) = self.container {
|
||||
self.container_changed(key);
|
||||
}
|
||||
}
|
||||
/// Mouse release over an inventory screen: `mouseReleased` ends or
|
||||
/// cancels the drag, drops a stack released outside, or clicks.
|
||||
fn inventory_mouse_release(&mut self, right: bool) {
|
||||
use horizon_client::container_screen::{Release, Target};
|
||||
let (slot, outside) = self.inventory_target();
|
||||
let dragged = self.inv_drag.as_ref().is_some_and(|(_, s)| !s.is_empty());
|
||||
let release = self.inv_clicks.release(
|
||||
&Target::of(slot, outside),
|
||||
Self::screen_button(right),
|
||||
self.carried.is_none(),
|
||||
dragged,
|
||||
self.has_shift_down(),
|
||||
false,
|
||||
);
|
||||
match (release, Target::of(slot, outside)) {
|
||||
(Release::QuickCraft, _) => self.inventory_release(),
|
||||
(Release::Click(input), Target::Outside) => {
|
||||
self.inv_drag = None;
|
||||
self.throw_click(right, input);
|
||||
}
|
||||
(Release::Click(input), Target::Slot(slot)) if self.inv_drag.is_some() => {
|
||||
// The drag never reached a slot: a plain click where it ends.
|
||||
self.inv_drag = None;
|
||||
if !self.menu_click(Some(slot), i32::from(right), input) {
|
||||
self.click_inventory(right);
|
||||
}
|
||||
if let Some(key) = self.container {
|
||||
self.container_changed(key);
|
||||
}
|
||||
}
|
||||
_ => self.inv_drag = None,
|
||||
}
|
||||
}
|
||||
/// Mouse release: finishes a quick craft drag (one slot is a plain click).
|
||||
fn inventory_release(&mut self) {
|
||||
use horizon_client::menu::Input;
|
||||
@@ -4174,8 +4316,7 @@ impl Game<'_> {
|
||||
let Some(slot) = self.slot_at(&batch, self.cursor.0, self.cursor.1) else {
|
||||
return;
|
||||
};
|
||||
let held_shift =
|
||||
self.keys.contains(&KeyCode::ShiftLeft) || self.keys.contains(&KeyCode::ShiftRight);
|
||||
let held_shift = self.has_shift_down();
|
||||
if self.crafter_click(slot, held_shift, right) {
|
||||
return;
|
||||
}
|
||||
@@ -4191,8 +4332,7 @@ impl Game<'_> {
|
||||
return;
|
||||
}
|
||||
let max = |items: &Items, s: &ItemStack| items.get(&s.item).map_or(64, |i| i.max_stack);
|
||||
let shift =
|
||||
self.keys.contains(&KeyCode::ShiftLeft) || self.keys.contains(&KeyCode::ShiftRight);
|
||||
let shift = held_shift;
|
||||
if slot >= 300
|
||||
&& let Some(key) = self.container
|
||||
{
|
||||
@@ -7803,6 +7943,18 @@ impl ApplicationHandler for Game<'_> {
|
||||
if self.window.as_ref().is_none_or(|w| w.id() != id) {
|
||||
return;
|
||||
}
|
||||
if let WindowEvent::KeyboardInput { event, .. } = &event
|
||||
&& let PhysicalKey::Code(key) = event.physical_key
|
||||
{
|
||||
if event.state == ElementState::Pressed {
|
||||
self.physical_keys.insert(key);
|
||||
} else {
|
||||
self.physical_keys.remove(&key);
|
||||
}
|
||||
}
|
||||
if let WindowEvent::Focused(false) = &event {
|
||||
self.physical_keys.clear();
|
||||
}
|
||||
if matches!(
|
||||
event,
|
||||
WindowEvent::KeyboardInput { .. }
|
||||
@@ -8017,7 +8169,7 @@ impl ApplicationHandler for Game<'_> {
|
||||
}
|
||||
}
|
||||
Screen::Inventory => {
|
||||
self.inventory_release();
|
||||
self.inventory_mouse_release(button == MouseButton::Right);
|
||||
self.loom_release();
|
||||
}
|
||||
Screen::Chat if pressed && button == MouseButton::Left => self.chat_click(),
|
||||
@@ -8528,6 +8680,7 @@ pub fn run() -> Result<()> {
|
||||
craft_grid: vec![None; 9],
|
||||
craft_size: 2,
|
||||
inv_drag: None,
|
||||
inv_clicks: Default::default(),
|
||||
last_inv_click: None,
|
||||
container: None,
|
||||
station: None,
|
||||
@@ -8543,6 +8696,7 @@ pub fn run() -> Result<()> {
|
||||
horizon_client::command::args::GameMode::Survival
|
||||
},
|
||||
keys: HashSet::new(),
|
||||
physical_keys: HashSet::new(),
|
||||
left: false,
|
||||
right: false,
|
||||
cursor: (0.0, 0.0),
|
||||
|
||||
@@ -166,10 +166,12 @@ impl Game<'_> {
|
||||
"down" => self.control_key_down(key)?,
|
||||
"up" => {
|
||||
self.keys.remove(&key);
|
||||
self.physical_keys.remove(&key);
|
||||
}
|
||||
"tap" => {
|
||||
self.control_key_down(key)?;
|
||||
self.keys.remove(&key);
|
||||
self.physical_keys.remove(&key);
|
||||
}
|
||||
other => {
|
||||
return Err(
|
||||
@@ -181,6 +183,7 @@ impl Game<'_> {
|
||||
}
|
||||
"release" => {
|
||||
self.keys.clear();
|
||||
self.physical_keys.clear();
|
||||
self.left = false;
|
||||
self.right = false;
|
||||
ok()
|
||||
@@ -506,6 +509,7 @@ impl Game<'_> {
|
||||
}
|
||||
|
||||
fn control_key_down(&mut self, key: KeyCode) -> Result<()> {
|
||||
self.physical_keys.insert(key);
|
||||
if self.screen == Screen::Chat {
|
||||
self.chat_key_code(key, None);
|
||||
return Ok(());
|
||||
|
||||
@@ -15,7 +15,6 @@ use horizon_client::{
|
||||
mesher::Vertex,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use winit::keyboard::KeyCode;
|
||||
|
||||
/// The game's answers to what the resolver cannot read from the stack.
|
||||
pub(super) struct Env<'a, 'g> {
|
||||
@@ -59,8 +58,7 @@ impl ItemEnv for Env<'_, '_> {
|
||||
total as f32
|
||||
}
|
||||
fn has_shift_down(&self) -> bool {
|
||||
self.game.keys.contains(&KeyCode::ShiftLeft)
|
||||
|| self.game.keys.contains(&KeyCode::ShiftRight)
|
||||
self.game.has_shift_down()
|
||||
}
|
||||
fn local_time(&self, pattern: &str, _locale: &str, _time_zone: Option<&str>) -> String {
|
||||
local_time(pattern)
|
||||
|
||||
@@ -169,7 +169,7 @@ impl Game<'_> {
|
||||
pub(super) fn recipe_key(&mut self, event: &winit::event::KeyEvent) {
|
||||
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||
let ctrl = self.keys.contains(&KeyCode::ControlLeft);
|
||||
let shift = self.keys.contains(&KeyCode::ShiftLeft);
|
||||
let shift = self.has_shift_down();
|
||||
let Some(search) = self.recipe_ui.search.as_mut() else {
|
||||
return;
|
||||
};
|
||||
@@ -620,8 +620,23 @@ impl Game<'_> {
|
||||
false
|
||||
}
|
||||
fn shift_down(&self) -> bool {
|
||||
use winit::keyboard::KeyCode;
|
||||
self.keys.contains(&KeyCode::ShiftLeft) || self.keys.contains(&KeyCode::ShiftRight)
|
||||
self.has_shift_down()
|
||||
}
|
||||
/// The open screen's book for `hasClickedOutside`: (visible, selected tab
|
||||
/// hovered); None when the screen has no book.
|
||||
pub(super) fn recipe_book_area(&self, batch: &HudBatch) -> Option<(bool, bool)> {
|
||||
let book = self.active_book()?;
|
||||
if !self.book_open(book) {
|
||||
return Some((false, false));
|
||||
}
|
||||
let (xo, yo) = self.book_origin(batch);
|
||||
let tabs = &self.recipe_ui.view.tabs;
|
||||
let selected = self.recipe_ui.tab.unwrap_or(Tab::Search);
|
||||
let hovered = tabs.iter().position(|t| *t == selected).is_some_and(|n| {
|
||||
let (x, y) = self.tab_pos(xo, yo, n);
|
||||
inside(self.cursor, x, y, 35.0, 27.0)
|
||||
});
|
||||
Some((true, hovered))
|
||||
}
|
||||
/// The grid, result and furnace slots clicks on which end a ghost recipe.
|
||||
fn is_book_slot(&self, book: BookType, slot: usize) -> bool {
|
||||
|
||||
@@ -2282,8 +2282,7 @@ impl Game<'_> {
|
||||
};
|
||||
let slot = (0..64).find(|&s| Self::menu_slot(&menu, s) == Some(internal));
|
||||
let Some(slot) = slot else { return };
|
||||
let shift =
|
||||
self.keys.contains(&KeyCode::ShiftLeft) || self.keys.contains(&KeyCode::ShiftRight);
|
||||
let shift = self.has_shift_down();
|
||||
let carried = self.to_network(&self.carried).map(|s| Hashed::of(&s));
|
||||
let state = self.remote.as_ref().map_or(0, |r| r.state_id);
|
||||
self.send(S::ContainerClick(play::ContainerClick {
|
||||
@@ -2296,6 +2295,38 @@ impl Game<'_> {
|
||||
carried,
|
||||
}));
|
||||
}
|
||||
/// A click outside a server's window (slot -999): predicted like
|
||||
/// `MultiPlayerGameMode.handleContainerInput` (the carried stack after
|
||||
/// `doClick`; the server drops the stack and swings the hand).
|
||||
pub(super) fn remote_throw(&mut self, right: bool, input: horizon_client::menu::Input) {
|
||||
use horizon_client::menu::Input;
|
||||
let container = self
|
||||
.remote
|
||||
.as_ref()
|
||||
.and_then(|r| r.menu.as_ref())
|
||||
.map_or(0, |m| m.0);
|
||||
if input == Input::Pickup
|
||||
&& let Some(c) = self.carried.as_mut()
|
||||
{
|
||||
if right && c.count > 1 {
|
||||
c.count -= 1;
|
||||
} else {
|
||||
self.carried = None;
|
||||
}
|
||||
self.swing_arm();
|
||||
}
|
||||
let carried = self.to_network(&self.carried).map(|s| Hashed::of(&s));
|
||||
let state = self.remote.as_ref().map_or(0, |r| r.state_id);
|
||||
self.send(S::ContainerClick(play::ContainerClick {
|
||||
container,
|
||||
state,
|
||||
slot: -999,
|
||||
button: i8::from(right),
|
||||
input: if input == Input::Throw { 4 } else { 0 },
|
||||
changed: Vec::new(),
|
||||
carried,
|
||||
}));
|
||||
}
|
||||
pub(super) fn remote_close_container(&mut self) {
|
||||
let id = self
|
||||
.remote
|
||||
|
||||
@@ -46,13 +46,13 @@ impl Game<'_> {
|
||||
use winit::keyboard::KeyCode;
|
||||
let saved = self.cursor;
|
||||
self.cursor = (x, y);
|
||||
let had_shift = self.keys.contains(&KeyCode::ShiftLeft);
|
||||
let had_shift = self.physical_keys.contains(&KeyCode::ShiftLeft);
|
||||
if shift {
|
||||
self.keys.insert(KeyCode::ShiftLeft);
|
||||
self.physical_keys.insert(KeyCode::ShiftLeft);
|
||||
}
|
||||
self.click_inventory(right);
|
||||
if shift && !had_shift {
|
||||
self.keys.remove(&KeyCode::ShiftLeft);
|
||||
self.physical_keys.remove(&KeyCode::ShiftLeft);
|
||||
}
|
||||
self.cursor = saved;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ pub mod collisions;
|
||||
pub mod command;
|
||||
pub mod command_storage;
|
||||
pub mod component;
|
||||
pub mod container_screen;
|
||||
pub mod containers;
|
||||
pub mod control;
|
||||
pub mod crafting;
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package dev.horizonlab.parity;
|
||||
|
||||
import com.google.gson.*;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.Options;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.*;
|
||||
import net.minecraft.client.gui.screens.recipebook.*;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.input.MouseButtonInfo;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.world.SimpleContainer;
|
||||
import net.minecraft.world.inventory.*;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
/**
|
||||
* Original oracle for the mouse handling of the survival and container
|
||||
* screens: the real AbstractContainerScreen.mouseClicked/mouseReleased of
|
||||
* ContainerScreen, HopperScreen, ShulkerBoxScreen and BeaconScreen (one slot
|
||||
* in the menu, the carried stack and the event's Shift modifiers from the
|
||||
* case) recording every slotClicked call they make, and the real
|
||||
* hasClickedOutside of those screens, MerchantScreen and the recipe book
|
||||
* screens (InventoryScreen, CraftingScreen, FurnaceScreen) with their
|
||||
* RecipeBookComponent's visibility and selected tab hover from the case.
|
||||
*/
|
||||
public final class VanillaContainerScreenOracle {
|
||||
static Object unsafe;
|
||||
static Method allocate;
|
||||
static final StringBuilder clicks = new StringBuilder();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> T alloc(Class<T> type) throws Exception {
|
||||
return (T) allocate.invoke(unsafe, type);
|
||||
}
|
||||
|
||||
static void put(Object target, Class<?> owner, String name, Object value) throws Exception {
|
||||
Field f = owner.getDeclaredField(name);
|
||||
long offset = (long) unsafe.getClass().getMethod("objectFieldOffset", Field.class).invoke(unsafe, f);
|
||||
Class<?> t = f.getType();
|
||||
String setter = t == int.class ? "putInt" : t == boolean.class ? "putBoolean" : "putObject";
|
||||
Class<?> param = t.isPrimitive() ? t : Object.class;
|
||||
unsafe.getClass().getMethod(setter, Object.class, long.class, param).invoke(unsafe, target, offset, value);
|
||||
}
|
||||
|
||||
static void record(int slotId, int button, ContainerInput input) {
|
||||
clicks.append(" click ").append(slotId).append(' ').append(button).append(' ').append(input.name());
|
||||
}
|
||||
|
||||
// The screens with slotClicked recording what the real mouse handling sends.
|
||||
public static final class Chest extends ContainerScreen {
|
||||
Chest() { super(null, null, null); }
|
||||
@Override protected void slotClicked(Slot slot, int slotId, int button, ContainerInput input) { record(slotId, button, input); }
|
||||
}
|
||||
|
||||
public static final class Hopper extends HopperScreen {
|
||||
Hopper() { super(null, null, null); }
|
||||
@Override protected void slotClicked(Slot slot, int slotId, int button, ContainerInput input) { record(slotId, button, input); }
|
||||
}
|
||||
|
||||
public static final class Shulker extends ShulkerBoxScreen {
|
||||
Shulker() { super(null, null, null); }
|
||||
@Override protected void slotClicked(Slot slot, int slotId, int button, ContainerInput input) { record(slotId, button, input); }
|
||||
}
|
||||
|
||||
public static final class Beacon extends BeaconScreen {
|
||||
Beacon() { super(null, null, null); }
|
||||
@Override protected void slotClicked(Slot slot, int slotId, int button, ContainerInput input) { record(slotId, button, input); }
|
||||
}
|
||||
|
||||
static AbstractContainerScreen<?> screen(String kind) throws Exception {
|
||||
return switch (kind) {
|
||||
case "chest" -> alloc(Chest.class);
|
||||
case "hopper" -> alloc(Hopper.class);
|
||||
case "shulker" -> alloc(Shulker.class);
|
||||
case "beacon" -> alloc(Beacon.class);
|
||||
case "merchant" -> alloc(MerchantScreen.class);
|
||||
case "inventory" -> alloc(InventoryScreen.class);
|
||||
case "crafting" -> alloc(CraftingScreen.class);
|
||||
case "furnace" -> alloc(FurnaceScreen.class);
|
||||
default -> throw new IllegalArgumentException(kind);
|
||||
};
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
ParityData.init();
|
||||
var u = Class.forName("sun.misc.Unsafe");
|
||||
Field theUnsafe = u.getDeclaredField("theUnsafe");
|
||||
theUnsafe.setAccessible(true);
|
||||
unsafe = theUnsafe.get(null);
|
||||
allocate = u.getMethod("allocateInstance", Class.class);
|
||||
Minecraft shell = alloc(Minecraft.class);
|
||||
put(shell, Minecraft.class, "fixerUpper", net.minecraft.util.datafix.DataFixers.getDataFixer());
|
||||
Field instance = Minecraft.class.getDeclaredField("instance");
|
||||
instance.setAccessible(true);
|
||||
instance.set(null, shell);
|
||||
Options options = new Options(shell, Files.createTempDirectory("opts").toFile());
|
||||
put(shell, Minecraft.class, "options", options);
|
||||
Method outside = AbstractContainerScreen.class.getDeclaredMethod("hasClickedOutside", double.class, double.class, int.class, int.class);
|
||||
outside.setAccessible(true);
|
||||
Method hovered = AbstractContainerScreen.class.getDeclaredMethod("getHoveredSlot", double.class, double.class);
|
||||
hovered.setAccessible(true);
|
||||
Field carried = AbstractContainerMenu.class.getDeclaredField("carried");
|
||||
carried.setAccessible(true);
|
||||
try (var out = new PrintWriter(Files.newBufferedWriter(Path.of(args[1]), StandardCharsets.UTF_8));
|
||||
var lines = Files.lines(Path.of(args[0]))) {
|
||||
int n = 0;
|
||||
for (var line : (Iterable<String>) lines::iterator) {
|
||||
var c = JsonParser.parseString(line).getAsJsonObject();
|
||||
out.println("== " + n++);
|
||||
String kind = c.get("kind").getAsString();
|
||||
var screen = screen(kind);
|
||||
var image = c.getAsJsonArray("image");
|
||||
put(screen, AbstractContainerScreen.class, "leftPos", image.get(0).getAsInt());
|
||||
put(screen, AbstractContainerScreen.class, "topPos", image.get(1).getAsInt());
|
||||
put(screen, AbstractContainerScreen.class, "imageWidth", image.get(2).getAsInt());
|
||||
put(screen, AbstractContainerScreen.class, "imageHeight", image.get(3).getAsInt());
|
||||
put(screen, Screen.class, "minecraft", shell);
|
||||
put(screen, Screen.class, "children", new ArrayList<>());
|
||||
put(screen, AbstractContainerScreen.class, "quickCraftSlots", new HashSet<Slot>());
|
||||
put(screen, AbstractContainerScreen.class, "lastQuickMoved", ItemStack.EMPTY);
|
||||
if (c.has("book")) {
|
||||
var book = c.getAsJsonArray("book");
|
||||
var component = alloc(CraftingRecipeBookComponent.class);
|
||||
put(component, RecipeBookComponent.class, "visible", book.get(0).getAsBoolean());
|
||||
var tab = alloc(RecipeBookTabButton.class);
|
||||
put(tab, net.minecraft.client.gui.components.AbstractWidget.class, "isHovered", book.get(1).getAsBoolean());
|
||||
put(component, RecipeBookComponent.class, "selectedTab", tab);
|
||||
put(screen, AbstractRecipeBookScreen.class, "recipeBookComponent", component);
|
||||
}
|
||||
var menu = alloc(ChestMenu.class);
|
||||
NonNullList<Slot> slots = NonNullList.create();
|
||||
var pos = c.getAsJsonArray("slot");
|
||||
var slot = new Slot(new SimpleContainer(1), 0, pos.get(0).getAsInt(), pos.get(1).getAsInt());
|
||||
slot.index = 7;
|
||||
slots.add(slot);
|
||||
put(menu, AbstractContainerMenu.class, "slots", slots);
|
||||
carried.set(menu, ItemStack.EMPTY);
|
||||
put(screen, AbstractContainerScreen.class, "menu", menu);
|
||||
int left = image.get(0).getAsInt(), top = image.get(1).getAsInt();
|
||||
for (var e : c.getAsJsonArray("events")) {
|
||||
var ev = e.getAsJsonArray();
|
||||
String name = ev.get(0).getAsString();
|
||||
double x = ev.get(1).getAsDouble(), y = ev.get(2).getAsDouble();
|
||||
var sb = new StringBuilder(name);
|
||||
sb.append(' ').append(outside.invoke(screen, x, y, left, top));
|
||||
if (!name.equals("point")) {
|
||||
sb.append(' ').append(hovered.invoke(screen, x, y) != null);
|
||||
carried.set(menu, ev.get(5).getAsBoolean() ? new ItemStack(Items.STONE, 5) : ItemStack.EMPTY);
|
||||
var event = new MouseButtonEvent(x, y, new MouseButtonInfo(ev.get(3).getAsInt(), ev.get(4).getAsInt()));
|
||||
clicks.setLength(0);
|
||||
if (name.equals("press")) {
|
||||
screen.mouseClicked(event, false);
|
||||
} else {
|
||||
screen.mouseReleased(event);
|
||||
}
|
||||
sb.append(clicks);
|
||||
}
|
||||
out.println(sb);
|
||||
}
|
||||
}
|
||||
}
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare the survival and container screens' mouse handling with the pinned
|
||||
client: the real AbstractContainerScreen.mouseClicked/mouseReleased (the slot
|
||||
id a click targets, -999 outside the window, and the ContainerInput it sends:
|
||||
PICKUP, QUICK_MOVE from the event's own Shift modifiers, THROW, the quick
|
||||
craft drag a held stack starts and the drop its release outside makes) and
|
||||
hasClickedOutside, with AbstractRecipeBookScreen's and RecipeBookComponent's
|
||||
rule for the recipe book screens."""
|
||||
import json
|
||||
import random
|
||||
|
||||
try:
|
||||
from . import lab, oracle_harness as harness
|
||||
except ImportError:
|
||||
import lab, oracle_harness as harness
|
||||
|
||||
# imageWidth, imageHeight; the recipe book screens only compare hasClickedOutside.
|
||||
SCREENS = {'chest': None, 'hopper': (176, 133), 'shulker': (176, 167), 'beacon': (230, 219),
|
||||
'merchant': (276, 166), 'inventory': (176, 166), 'crafting': (176, 166), 'furnace': (176, 166)}
|
||||
CLICKING = ('chest', 'hopper', 'shulker', 'beacon')
|
||||
BOOKS = ('inventory', 'crafting', 'furnace')
|
||||
|
||||
|
||||
def quarter(rng, lo, hi):
|
||||
return rng.randrange(int(lo * 4), int(hi * 4)) / 4
|
||||
|
||||
|
||||
def point(rng, left, top, width, height):
|
||||
"""Mostly near the window's edges and the recipe book's area."""
|
||||
pick = rng.random()
|
||||
if pick < 0.5:
|
||||
x = rng.choice([left, left + width, left - 147]) + quarter(rng, -3, 3)
|
||||
y = quarter(rng, top - 20, top + height + 20)
|
||||
elif pick < 0.8:
|
||||
x = quarter(rng, left - 30, left + width + 30)
|
||||
y = rng.choice([top, top + height]) + quarter(rng, -3, 3)
|
||||
else:
|
||||
x = quarter(rng, left - 200, left + width + 60)
|
||||
y = quarter(rng, top - 40, top + height + 40)
|
||||
return x, y
|
||||
|
||||
|
||||
def cases(rng):
|
||||
for _ in range(4000):
|
||||
kind = rng.choice(list(SCREENS))
|
||||
width, height = SCREENS[kind] or (176, 114 + 18 * rng.randint(1, 6))
|
||||
left, top = rng.randrange(0, 300), rng.randrange(0, 120)
|
||||
sx, sy = rng.randrange(0, width - 16), rng.randrange(0, height - 16)
|
||||
case = dict(kind=kind, image=[left, top, width, height], slot=[sx, sy], events=[])
|
||||
if kind in BOOKS:
|
||||
case['book'] = [rng.random() < 0.7, rng.random() < 0.2]
|
||||
for _ in range(rng.randrange(4, 24)):
|
||||
if kind not in CLICKING:
|
||||
case['events'].append(['point', *point(rng, left, top, width, height)])
|
||||
continue
|
||||
on_slot = rng.random() < 0.3
|
||||
if on_slot:
|
||||
x = quarter(rng, left + sx - 1, left + sx + 17)
|
||||
y = quarter(rng, top + sy - 1, top + sy + 17)
|
||||
else:
|
||||
while True:
|
||||
x, y = point(rng, left, top, width, height)
|
||||
if not (left + sx - 1 <= x < left + sx + 17 and top + sy - 1 <= y < top + sy + 17):
|
||||
break
|
||||
case['events'].append([rng.choice(['press', 'release']), x, y, rng.choice([1, 1, 3]),
|
||||
rng.choice([0, 0, 1, 2, 3]), rng.random() < 0.5, on_slot])
|
||||
yield case
|
||||
|
||||
|
||||
def make_cases(build):
|
||||
path = build / 'cases.jsonl'
|
||||
count = 0
|
||||
with path.open('w') as out:
|
||||
for case in cases(random.Random(0x434c4b53)):
|
||||
out.write(json.dumps(case, separators=(',', ':')) + '\n')
|
||||
count += 1
|
||||
return path, count
|
||||
|
||||
|
||||
def main():
|
||||
scope = ('The real AbstractContainerScreen.mouseClicked and mouseReleased of ContainerScreen (one to six rows), '
|
||||
'HopperScreen, ShulkerBoxScreen and BeaconScreen with one menu slot, press and release of the left and right '
|
||||
'buttons with the carried stack empty or not and the event Shift modifiers (left, right, both, none), '
|
||||
'recording every slotClicked (slot id, menu button, ContainerInput); and hasClickedOutside of those screens, '
|
||||
'MerchantScreen and the recipe book screens (InventoryScreen, CraftingScreen, FurnaceScreen) with the book '
|
||||
'visible or not and its selected tab hovered or not. Drags (mouseDragged, quickCraftToSlots), double '
|
||||
'clicks and keyPickItem cloning are not exercised; the menu clicks themselves are menu_click\'s.')
|
||||
harness.differential(
|
||||
'rust-container-screen-parity', 'VanillaContainerScreenOracle', 'container_screen_probe', make_cases, scope,
|
||||
sources=[lab.ROOT / 'crates/horizon-client/src/container_screen.rs',
|
||||
lab.ROOT / 'crates/horizon-client/src/menu.rs'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user