use bytestring::ByteString;
use crate::crafting::RecipeBook;
use crate::rules::Version;
use crate::sim::{Layout, Loc, Model, OUTSIDE, Screen, Sim};
use crate::stack::Stack;
fn is_grid(lay: Layout, l: Loc) -> bool {
match (lay.screen, l) {
(Screen::Player, Loc::Craft(_)) => true,
(Screen::Container(WindowKind::Crafting), Loc::Container(k)) => (1..=9).contains(&k),
_ => false,
}
}
use crate::window::{SlotKind, WindowKind};
pub const DEFAULT_CLICK_TIMEOUT_TICKS: u32 = 40;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Click {
Pickup {
slot: i16,
right: bool,
},
QuickMove {
slot: i16,
right: bool,
},
Swap {
slot: i16,
hotbar: u8,
},
Drop {
slot: i16,
all: bool,
},
PickupAll {
slot: i16,
},
}
impl Click {
#[must_use]
pub fn wire(self) -> (i16, u8, u8) {
match self {
Click::Pickup { slot, right } => (slot, u8::from(right), 0),
Click::QuickMove { slot, right } => (slot, u8::from(right), 1),
Click::Swap { slot, hotbar } => (slot, hotbar, 2),
Click::Drop { slot, all } => (slot, u8::from(all), 4),
Click::PickupAll { slot } => (slot, 0, 6),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ClickError {
Pending,
Resyncing,
NoScreen,
WindowOpen,
SameTick,
InvalidSlot,
InvalidButton,
Unpredictable,
}
impl std::fmt::Display for ClickError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
ClickError::Pending => "a click is waiting for its confirm",
ClickError::Resyncing => "waiting for the window resync after a rejected click",
ClickError::NoScreen => "no inventory screen is open",
ClickError::WindowOpen => "a container window is open",
ClickError::SameTick => "not allowed in the same tick",
ClickError::InvalidSlot => "invalid slot",
ClickError::InvalidButton => "invalid button",
ClickError::Unpredictable => "click outcome cannot be predicted",
})
}
}
impl std::error::Error for ClickError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClickPacket {
pub window_id: u8,
pub slot: i16,
pub button: u8,
pub mode: u8,
pub action: i16,
pub item: Option<Stack>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outgoing {
Click(ClickPacket),
Confirm {
window_id: i8,
action: i16,
},
CloseWindow(u8),
OpenInventory,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ClickOutcome {
Accepted,
Rejected,
TimedOut,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ClickResult {
pub window_id: i32,
pub action: i16,
pub outcome: ClickOutcome,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Events {
pub player_changed: bool,
pub window_changed: bool,
pub cursor_changed: bool,
pub opened: Option<i32>,
pub closed: bool,
pub clicks: Vec<ClickResult>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Window {
id: i32,
kind: WindowKind,
title: ByteString,
action: i16,
properties: Vec<(i16, i16)>,
}
impl Window {
#[must_use]
pub fn id(&self) -> i32 {
self.id
}
#[must_use]
pub fn kind(&self) -> WindowKind {
self.kind
}
#[must_use]
pub fn title(&self) -> &ByteString {
&self.title
}
#[must_use]
pub fn property(&self, id: i16) -> Option<i16> {
self.properties.iter().find(|p| p.0 == id).map(|p| p.1)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ScreenState {
None,
Inventory,
Window(Window),
}
#[derive(Debug, Clone, Copy)]
struct PendingClick {
window_id: i32,
action: i16,
age: u32,
}
#[derive(Debug, Clone, Copy, Default)]
struct TickFlags {
clicked: bool,
opened: bool,
closed: bool,
}
#[derive(Debug, Clone)]
pub struct Inventory {
version: Version,
m: Model,
screen: ScreenState,
player_action: i16,
pending: Option<PendingClick>,
resync: Option<(i32, u32)>,
flags: TickFlags,
timeout: u32,
events: Events,
book: RecipeBook,
}
impl Inventory {
#[must_use]
pub fn new(version: Version) -> Inventory {
Inventory {
version,
m: Model::default(),
screen: ScreenState::None,
player_action: 0,
pending: None,
resync: None,
flags: TickFlags::default(),
timeout: DEFAULT_CLICK_TIMEOUT_TICKS,
events: Events::default(),
book: RecipeBook::default(),
}
}
pub fn set_click_timeout(&mut self, ticks: u32) {
self.timeout = ticks.max(1);
}
#[must_use]
pub fn version(&self) -> Version {
self.version
}
pub fn reset(&mut self) {
*self = Inventory {
timeout: self.timeout,
book: std::mem::take(&mut self.book),
..Inventory::new(self.version)
};
self.events.player_changed = true;
}
pub fn tick(&mut self) {
self.flags = TickFlags::default();
if let Some(p) = &mut self.pending {
p.age += 1;
if p.age >= self.timeout {
self.events.clicks.push(ClickResult {
window_id: p.window_id,
action: p.action,
outcome: ClickOutcome::TimedOut,
});
self.pending = None;
}
}
if let Some((_, age)) = &mut self.resync {
*age += 1;
if *age >= self.timeout {
self.resync = None;
}
}
}
pub fn drain_events(&mut self) -> Events {
std::mem::take(&mut self.events)
}
#[must_use]
pub fn cursor(&self) -> Option<&Stack> {
self.m.cursor.as_ref()
}
#[must_use]
pub fn player(&self, index: usize) -> Option<&Stack> {
self.m.player.get(index)?.as_ref()
}
#[must_use]
pub fn hotbar(&self, k: usize) -> Option<&Stack> {
if k < 9 { self.player(k) } else { None }
}
#[must_use]
pub fn selected(&self) -> u8 {
self.m.selected
}
#[must_use]
pub fn held(&self) -> Option<&Stack> {
self.player(usize::from(self.m.selected))
}
#[must_use]
pub fn armor(&self, a: usize) -> Option<&Stack> {
if a < 4 { self.player(39 - a) } else { None }
}
#[must_use]
pub fn offhand(&self) -> Option<&Stack> {
self.player(40)
}
#[must_use]
pub fn window(&self) -> Option<&Window> {
match &self.screen {
ScreenState::Window(w) => Some(w),
_ => None,
}
}
#[must_use]
pub fn gui_open(&self) -> bool {
self.screen != ScreenState::None
}
#[must_use]
pub fn inventory_open(&self) -> bool {
self.screen == ScreenState::Inventory
}
fn layout(&self) -> Layout {
let screen = match &self.screen {
ScreenState::Window(w) => Screen::Container(w.kind),
_ => Screen::Player,
};
Layout::new(self.version, screen)
}
#[must_use]
pub fn slot_count(&self) -> usize {
self.layout().len
}
#[must_use]
pub fn container_len(&self) -> usize {
self.layout().n
}
#[must_use]
pub fn slot(&self, i: usize) -> Option<&Stack> {
let l = self.layout().loc(i)?;
self.m.at(l).as_ref()
}
#[must_use]
pub fn window0_slot(&self, i: usize) -> Option<&Stack> {
let l = Layout::new(self.version, Screen::Player).loc(i)?;
self.m.at(l).as_ref()
}
#[must_use]
pub fn slot_kind(&self, i: usize) -> Option<SlotKind> {
let lay = self.layout();
(i < lay.len).then(|| lay.kind(i))
}
#[must_use]
pub fn slot_of_player(&self, k: usize) -> Option<i16> {
self.layout().slot_of_player(k).map(|i| i as i16)
}
pub fn find(&self, mut pred: impl FnMut(&Stack) -> bool) -> Option<i16> {
let lay = self.layout();
(0..lay.len)
.find(|&i| {
lay.loc(i)
.and_then(|l| self.m.at(l).as_ref())
.is_some_and(&mut pred)
})
.map(|i| i as i16)
}
pub fn count(&self, mut pred: impl FnMut(&Stack) -> bool) -> i32 {
self.m
.player
.iter()
.flatten()
.filter(|s| pred(s))
.map(|s| s.count)
.sum()
}
#[must_use]
pub fn result_known(&self) -> bool {
match &self.screen {
ScreenState::Window(w) if w.kind == WindowKind::Crafting => {
self.m.container_result_known
}
ScreenState::Window(_) => true,
_ => self.m.craft_result_known,
}
}
#[must_use]
pub fn pending(&self) -> bool {
self.pending.is_some()
}
#[must_use]
pub fn resyncing(&self) -> bool {
self.resync.is_some()
}
pub fn can_click(&self) -> Result<(), ClickError> {
if self.pending.is_some() {
return Err(ClickError::Pending);
}
if self.resync.is_some() {
return Err(ClickError::Resyncing);
}
if self.flags.clicked || self.flags.opened {
return Err(ClickError::SameTick);
}
if self.screen == ScreenState::None {
return Err(ClickError::NoScreen);
}
Ok(())
}
pub fn click(&mut self, click: Click) -> Result<Outgoing, ClickError> {
self.can_click()?;
let lay = self.layout();
let (slot, button, mode) = click.wire();
let in_range = usize::try_from(slot).is_ok_and(|i| i < lay.len);
let slot_ok = match click {
Click::Pickup { .. } | Click::Drop { .. } => in_range || slot == OUTSIDE,
Click::QuickMove { .. } | Click::Swap { .. } | Click::PickupAll { .. } => in_range,
};
if !slot_ok {
return Err(ClickError::InvalidSlot);
}
if let Click::Swap { hotbar, .. } = click {
let ok = hotbar < 9 || (hotbar == 40 && self.version == Version::V1_16_5);
if !ok {
return Err(ClickError::InvalidButton);
}
}
let mut m = self.m.clone();
let item = {
let mut sim = Sim {
v: self.version,
lay,
m: &mut m,
book: &self.book,
};
sim.click(slot, button, mode)
.map_err(|_| ClickError::Unpredictable)?
};
self.commit(m, lay);
let (window_id, action) = match &mut self.screen {
ScreenState::Window(w) => {
w.action = w.action.wrapping_add(1);
(w.id, w.action)
}
_ => {
self.player_action = self.player_action.wrapping_add(1);
(0, self.player_action)
}
};
self.pending = Some(PendingClick {
window_id,
action,
age: 0,
});
self.flags.clicked = true;
Ok(Outgoing::Click(ClickPacket {
window_id: window_id as u8,
slot,
button,
mode,
action,
item,
}))
}
fn commit(&mut self, new: Model, lay: Layout) {
let old = std::mem::replace(&mut self.m, new);
if old.player != self.m.player || old.craft != self.m.craft {
self.events.player_changed = true;
}
if old.craft_result != self.m.craft_result {
self.events.player_changed = true;
}
if old.container != self.m.container {
self.events.window_changed = true;
}
if old.cursor != self.m.cursor {
self.events.cursor_changed = true;
}
if self.version == Version::V1_8 {
return;
}
if old.craft != self.m.craft {
self.m.craft_result_known = false;
}
if lay.screen == Screen::Container(WindowKind::Crafting)
&& old.container.get(1..10) != self.m.container.get(1..10)
{
self.m.container_result_known = false;
}
}
pub fn open_inventory(&mut self) -> Result<Option<Outgoing>, ClickError> {
match self.screen {
ScreenState::Inventory => return Ok(None),
ScreenState::Window(_) => return Err(ClickError::WindowOpen),
ScreenState::None => {}
}
if self.flags.clicked || self.flags.closed {
return Err(ClickError::SameTick);
}
self.screen = ScreenState::Inventory;
self.flags.opened = true;
Ok((self.version == Version::V1_8).then_some(Outgoing::OpenInventory))
}
pub fn close(&mut self) -> Result<Outgoing, ClickError> {
if self.screen == ScreenState::None {
return Err(ClickError::NoScreen);
}
if self.flags.opened {
return Err(ClickError::SameTick);
}
let id = match &self.screen {
ScreenState::Window(w) => w.id as u8,
_ => 0,
};
self.close_screen();
self.flags.closed = true;
Ok(Outgoing::CloseWindow(id))
}
fn close_screen(&mut self) {
if self.m.cursor.take().is_some() {
self.events.cursor_changed = true;
}
match std::mem::replace(&mut self.screen, ScreenState::None) {
ScreenState::Inventory => {
if self.version == Version::V1_8 {
if self.m.craft.iter().any(Option::is_some) {
self.events.player_changed = true;
}
self.m.craft = Default::default();
}
self.m.craft_result = None;
self.m.craft_result_known = self.version == Version::V1_8;
}
ScreenState::Window(_) => {
self.m.container.clear();
self.m.container_result_known = true;
}
ScreenState::None => {}
}
if let Some((w, _)) = self.resync
&& w != 0
{
self.resync = None;
}
}
pub fn consume_held(&mut self, n: u8) -> bool {
self.consume(usize::from(self.m.selected.min(8)), n)
}
pub fn consume_offhand(&mut self, n: u8) -> bool {
self.version == Version::V1_16_5 && self.consume(40, n)
}
fn consume(&mut self, k: usize, n: u8) -> bool {
let slot = &mut self.m.player[k];
let Some(s) = slot.as_mut() else {
return false;
};
s.count -= i32::from(n);
if s.count <= 0 {
*slot = None;
}
self.events.player_changed = true;
true
}
pub fn select(&mut self, slot: u8) {
if slot < 9 {
self.m.selected = slot;
}
}
pub fn on_held_slot(&mut self, slot: i8) {
if (0..9).contains(&slot) {
self.m.selected = slot as u8;
}
}
pub fn on_open_window(&mut self, id: i32, kind: Option<WindowKind>, title: ByteString) {
let Some(kind) = kind else { return };
if self.screen != ScreenState::None {
self.close_screen();
}
let n = kind.container_len(self.version);
self.m.container = vec![None; n];
self.m.container_result_known = true;
self.screen = ScreenState::Window(Window {
id,
kind,
title,
action: 0,
properties: Vec::new(),
});
self.events.opened = Some(id);
self.events.window_changed = true;
}
pub fn on_close_window(&mut self) {
if self.screen != ScreenState::None {
let was_window = matches!(self.screen, ScreenState::Window(_));
self.close_screen();
if was_window {
self.events.closed = true;
}
}
}
fn open_id(&self) -> i32 {
match &self.screen {
ScreenState::Window(w) => w.id,
_ => 0,
}
}
pub fn on_window_items(
&mut self,
window_id: i32,
items: impl IntoIterator<Item = Option<Stack>>,
) {
let lay = if window_id == 0 {
Layout::new(self.version, Screen::Player)
} else if window_id == self.open_id() {
self.layout()
} else {
return;
};
let mut player = false;
let mut window = false;
let mut touched_grid = false;
for (i, s) in items.into_iter().enumerate() {
let Some(l) = lay.loc(i) else { break };
touched_grid |= is_grid(lay, l);
let mut s = self.normalize(s);
if lay.clamps(l) {
crate::sim::clamp(&mut s);
}
let slot = self.m.at_mut(l);
if *slot != s {
*slot = s;
match l {
Loc::Container(_) => window = true,
_ => player = true,
}
}
}
if lay.screen == Screen::Player {
self.m.craft_result_known = true;
} else {
self.m.container_result_known = true;
}
if self.version == Version::V1_8 && touched_grid {
crate::sim::recompute_1_8(&mut self.m, lay);
}
self.events.player_changed |= player;
self.events.window_changed |= window;
if self.resync.is_some_and(|(w, _)| w == window_id) {
self.resync = None;
}
}
pub fn on_set_slot(&mut self, window_id: i32, slot: i16, item: Option<Stack>) {
let item = self.normalize(item);
if window_id == -1 {
if self.m.cursor != item {
self.m.cursor = item;
self.events.cursor_changed = true;
}
return;
}
let loc = if window_id == -2 && self.version == Version::V1_16_5 {
usize::try_from(slot)
.ok()
.filter(|&k| k < 41)
.map(Loc::Player)
} else if window_id == 0 && (36..45).contains(&slot) {
Layout::new(self.version, Screen::Player).loc(slot as usize)
} else if window_id == self.open_id() {
usize::try_from(slot)
.ok()
.and_then(|i| self.layout().loc(i))
} else {
None
};
let Some(l) = loc else { return };
let mut item = item;
if self.layout().clamps(l) {
crate::sim::clamp(&mut item);
}
match l {
Loc::CraftResult => self.m.craft_result_known = true,
Loc::Container(0)
if matches!(
self.layout().screen,
Screen::Container(WindowKind::Crafting)
) =>
{
self.m.container_result_known = true;
}
_ => {}
}
let s = self.m.at_mut(l);
if *s != item {
*s = item;
match l {
Loc::Container(_) => self.events.window_changed = true,
_ => self.events.player_changed = true,
}
}
if self.version == Version::V1_8 {
let lay = if window_id == 0 && (36..45).contains(&slot) {
Layout::new(self.version, Screen::Player)
} else {
self.layout()
};
if is_grid(lay, l) {
crate::sim::recompute_1_8(&mut self.m, lay);
}
}
}
#[must_use]
pub fn recipes(&self) -> &RecipeBook {
&self.book
}
pub fn on_declare_recipes(&mut self, book: RecipeBook) {
self.book = book;
}
pub fn on_window_property(&mut self, window_id: i32, property: i16, value: i16) {
if let ScreenState::Window(w) = &mut self.screen
&& w.id == window_id
{
match w.properties.iter_mut().find(|p| p.0 == property) {
Some(p) => p.1 = value,
None => w.properties.push((property, value)),
}
}
}
pub fn on_transaction(
&mut self,
window_id: i8,
action: i16,
accepted: bool,
) -> Option<Outgoing> {
let w = i32::from(window_id);
if let Some(p) = self.pending
&& p.window_id == w
&& p.action == action
{
self.pending = None;
let outcome = if accepted {
ClickOutcome::Accepted
} else {
self.resync = Some((w, 0));
ClickOutcome::Rejected
};
self.events.clicks.push(ClickResult {
window_id: w,
action,
outcome,
});
}
let known = w == 0 || w == self.open_id();
(known && !accepted).then_some(Outgoing::Confirm { window_id, action })
}
fn normalize(&self, s: Option<Stack>) -> Option<Stack> {
match self.version {
Version::V1_8 => s,
Version::V1_16_5 => s
.filter(|s| s.count > 0 && s.id != 0)
.map(|s| Stack { damage: 0, ..s }),
}
}
}