feat(client): add inventory manipulation wrappers

This commit is contained in:
Ryan 2025-02-17 13:38:20 -05:00
parent 2373d6500e
commit 21cc1a5488
Signed by: ErrorNoInternet
GPG Key ID: 2486BFB7B1E6A4A3
7 changed files with 331 additions and 11 deletions

View File

@ -13,3 +13,22 @@ RADIUS_GOAL = 1
REACH_BLOCK_POS_GOAL = 2
XZ_GOAL = 3
Y_GOAL = 4
PICKUP_LEFT = 0
PICKUP_RIGHT = 1
PICKUP_LEFT_OUTSIDE = 2
PICKUP_RIGHT_OUTSIDE = 3
QUICK_MOVE_LEFT = 4
QUICK_MOVE_RIGHT = 5
SWAP = 6
CLONE = 7
THROW_SINGLE = 8
THROW_ALL = 9
QUICK_CRAFT = 10
QUICK_CRAFT_LEFT = 0
QUICK_CRAFT_RIGHT = 1
QUICK_CRAFT_MIDDLE = 2
QUICK_CRAFT_START = 0
QUICK_CRAFT_ADD = 1
QUICK_CRAFT_END = 2
PICKUP_ALL = 11

View File

@ -0,0 +1,88 @@
use super::{Client, Container, ContainerRef, ItemStack, Vec3};
use azalea::{
BlockPos, inventory::Inventory, prelude::ContainerClientExt,
protocol::packets::game::ServerboundSetCarriedItem,
};
use log::error;
use mlua::{Lua, Result, UserDataRef};
pub fn held_item(_lua: &Lua, client: &Client) -> Result<ItemStack> {
Ok(ItemStack {
inner: client
.inner
.as_ref()
.unwrap()
.component::<Inventory>()
.held_item(),
})
}
pub fn held_slot(_lua: &Lua, client: &Client) -> Result<u8> {
Ok(client
.inner
.as_ref()
.unwrap()
.component::<Inventory>()
.selected_hotbar_slot)
}
pub fn open_container(_lua: &Lua, client: &Client) -> Result<Option<ContainerRef>> {
Ok(client
.inner
.as_ref()
.unwrap()
.get_open_container()
.map(|c| ContainerRef { inner: c }))
}
pub async fn open_container_at(
_lua: Lua,
client: UserDataRef<Client>,
position: Vec3,
) -> Result<Option<Container>> {
#[allow(clippy::cast_possible_truncation)]
Ok(client
.inner
.clone()
.unwrap()
.open_container_at(BlockPos::new(
position.x as i32,
position.y as i32,
position.z as i32,
))
.await
.map(|c| Container { inner: c }))
}
pub fn open_inventory(_lua: &Lua, client: &mut Client, _: ()) -> Result<Option<Container>> {
Ok(client
.inner
.as_mut()
.unwrap()
.open_inventory()
.map(|c| Container { inner: c }))
}
pub fn set_held_slot(_lua: &Lua, client: &Client, slot: u8) -> Result<()> {
if slot > 8 {
return Ok(());
}
let client = client.inner.as_ref().unwrap();
{
let mut ecs = client.ecs.lock();
let mut inventory = client.query::<&mut Inventory>(&mut ecs);
if inventory.selected_hotbar_slot == slot {
return Ok(());
}
inventory.selected_hotbar_slot = slot;
};
if let Err(error) = client.write_packet(ServerboundSetCarriedItem {
slot: u16::from(slot),
}) {
error!("failed to send SetCarriedItem packet: {error:?}");
}
Ok(())
}

View File

@ -1,10 +1,17 @@
mod container;
mod interaction;
mod movement;
mod state;
mod world;
use super::{
block::Block, direction::Direction, entity::Entity, fluid_state::FluidState, hunger::Hunger,
block::Block,
container::item_stack::ItemStack,
container::{Container, ContainerRef},
direction::Direction,
entity::Entity,
fluid_state::FluidState,
hunger::Hunger,
vec3::Vec3,
};
use azalea::Client as AzaleaClient;
@ -20,8 +27,11 @@ impl UserData for Client {
f.add_field_method_get("direction", movement::direction);
f.add_field_method_get("eye_position", movement::eye_position);
f.add_field_method_get("health", state::health);
f.add_field_method_get("held_item", container::held_item);
f.add_field_method_get("held_slot", container::held_slot);
f.add_field_method_get("hunger", state::hunger);
f.add_field_method_get("looking_at", movement::looking_at);
f.add_field_method_get("open_container", container::open_container);
f.add_field_method_get("pathfinder", movement::pathfinder);
f.add_field_method_get("position", movement::position);
f.add_field_method_get("score", state::score);
@ -31,13 +41,17 @@ impl UserData for Client {
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_async_method("mine", interaction::mine);
m.add_async_method("open_container_at", container::open_container_at);
m.add_async_method("set_client_information", state::set_client_information);
m.add_method("best_tool_for_block", world::best_tool_for_block);
m.add_method("block_names_to_states", world::block_names_to_states);
m.add_method("chat", chat);
m.add_method("find_blocks", world::find_blocks);
m.add_method("find_entities", world::find_entities);
m.add_method("get_block_from_state", world::get_block_from_state);
m.add_method("get_block_state", world::get_block_state);
m.add_method("get_fluid_state", world::get_fluid_state);
m.add_method("set_held_slot", container::set_held_slot);
m.add_method("set_mining", interaction::set_mining);
m.add_method("stop_pathfinding", movement::stop_pathfinding);
m.add_method_mut("attack", interaction::attack);
@ -45,6 +59,7 @@ impl UserData for Client {
m.add_method_mut("goto", movement::goto);
m.add_method_mut("jump", movement::jump);
m.add_method_mut("look_at", movement::look_at);
m.add_method_mut("open_inventory", container::open_inventory);
m.add_method_mut("set_direction", movement::set_direction);
m.add_method_mut("set_jumping", movement::set_jumping);
m.add_method_mut("sprint", movement::sprint);

View File

@ -1,17 +1,47 @@
use super::{Block, Client, Entity, FluidState, Vec3};
use azalea::{
BlockPos,
auto_tool::AutoToolClientExt,
blocks::{Block as AzaleaBlock, BlockState, BlockStates},
ecs::query::Without,
entity::{Dead, EntityKind, EntityUuid, Position as AzaleaPosition, metadata::CustomName},
world::MinecraftEntityId,
};
use mlua::{Function, Lua, Result};
use mlua::{Function, Lua, Result, Table};
pub fn best_tool_for_block(lua: &Lua, client: &Client, block_state: u16) -> Result<Table> {
let tr = client
.inner
.as_ref()
.unwrap()
.best_tool_in_hotbar_for_block(BlockState { id: block_state });
let tool_result = lua.create_table()?;
tool_result.set("index", tr.index)?;
tool_result.set("percentage_per_tick", tr.percentage_per_tick)?;
Ok(tool_result)
}
pub fn block_names_to_states(
_lua: &Lua,
_client: &Client,
block_names: Vec<String>,
) -> Result<Vec<u16>> {
Ok(block_names
.iter()
.flat_map(|n| {
(u32::MIN..u32::MAX)
.map_while(|i| BlockState::try_from(i).ok())
.filter(move |&b| n == Into::<Box<dyn AzaleaBlock>>::into(b).id())
.map(|b| b.id)
})
.collect())
}
pub fn find_blocks(
_lua: &Lua,
client: &Client,
(nearest_to, block_names): (Vec3, Vec<String>),
(nearest_to, block_states): (Vec3, Vec<u16>),
) -> Result<Vec<Vec3>> {
#[allow(clippy::cast_possible_truncation)]
Ok(client
@ -27,14 +57,7 @@ pub fn find_blocks(
nearest_to.z as i32,
),
&BlockStates {
set: block_names
.iter()
.flat_map(|n| {
(u32::MIN..u32::MAX)
.map_while(|i| BlockState::try_from(i).ok())
.filter(move |&b| n == Into::<Box<dyn AzaleaBlock>>::into(b).id())
})
.collect(),
set: block_states.iter().map(|&id| BlockState { id }).collect(),
},
)
.map(|p| Vec3 {

View File

@ -0,0 +1,50 @@
use azalea::inventory::components::{CustomName, Damage, MaxDamage};
use mlua::{UserData, UserDataFields, UserDataMethods};
pub struct ItemStack {
pub inner: azalea::inventory::ItemStack,
}
impl UserData for ItemStack {
fn add_fields<F: UserDataFields<Self>>(f: &mut F) {
f.add_field_method_get("is_empty", |_, this| Ok(this.inner.is_empty()));
f.add_field_method_get("is_present", |_, this| Ok(this.inner.is_present()));
f.add_field_method_get("count", |_, this| Ok(this.inner.count()));
f.add_field_method_get("kind", |_, this| Ok(this.inner.kind().to_string()));
f.add_field_method_get("custom_name", |_, this| {
Ok(if let Some(data) = this.inner.as_present() {
data.components
.get::<CustomName>()
.map(|n| n.name.to_string())
} else {
None
})
});
f.add_field_method_get("damage", |_, this| {
Ok(if let Some(data) = this.inner.as_present() {
data.components.get::<Damage>().map(|d| d.amount)
} else {
None
})
});
f.add_field_method_get("max_damage", |_, this| {
Ok(if let Some(data) = this.inner.as_present() {
data.components.get::<MaxDamage>().map(|d| d.amount)
} else {
None
})
});
}
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_method_mut("split", |_, this, count: u32| {
Ok(ItemStack {
inner: this.inner.split(count),
})
});
m.add_method_mut("update_empty", |_, this, (): ()| {
this.inner.update_empty();
Ok(())
});
}
}

124
src/lua/container/mod.rs Normal file
View File

@ -0,0 +1,124 @@
pub mod item_stack;
use azalea::{
container::{ContainerHandle, ContainerHandleRef},
inventory::operations::{
ClickOperation, CloneClick, PickupAllClick, PickupClick, QuickCraftClick, QuickCraftKind,
QuickCraftStatus, QuickMoveClick, SwapClick, ThrowClick,
},
};
use item_stack::ItemStack;
use mlua::{Result, Table, UserData, UserDataFields, UserDataMethods};
pub struct Container {
pub inner: ContainerHandle,
}
impl UserData for Container {
fn add_fields<F: UserDataFields<Self>>(f: &mut F) {
f.add_field_method_get("id", |_, this| Ok(this.inner.id()));
f.add_field_method_get("menu", |_, this| {
Ok(this.inner.menu().map(|m| format!("{m:?}")))
});
f.add_field_method_get("contents", |_, this| {
Ok(this.inner.contents().map(|v| {
v.iter()
.map(|i| ItemStack { inner: i.clone() })
.collect::<Vec<_>>()
}))
});
}
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_method("click", |_, this, operation: Table| {
this.inner.click(click_operation_from_table(operation)?);
Ok(())
});
}
}
pub struct ContainerRef {
pub inner: ContainerHandleRef,
}
impl UserData for ContainerRef {
fn add_fields<F: UserDataFields<Self>>(f: &mut F) {
f.add_field_method_get("id", |_, this| Ok(this.inner.id()));
f.add_field_method_get("menu", |_, this| {
Ok(this.inner.menu().map(|m| format!("{m:?}")))
});
f.add_field_method_get("contents", |_, this| {
Ok(this.inner.contents().map(|v| {
v.iter()
.map(|i| ItemStack { inner: i.clone() })
.collect::<Vec<_>>()
}))
});
}
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_method("close", |_, this, (): ()| {
this.inner.close();
Ok(())
});
m.add_method("click", |_, this, operation: Table| {
this.inner.click(click_operation_from_table(operation)?);
Ok(())
});
}
}
fn click_operation_from_table(o: Table) -> Result<ClickOperation> {
Ok(match o.get("type")? {
0 => ClickOperation::Pickup(PickupClick::Left {
slot: o.get("slot")?,
}),
1 => ClickOperation::Pickup(PickupClick::Right {
slot: o.get("slot")?,
}),
2 => ClickOperation::Pickup(PickupClick::LeftOutside),
3 => ClickOperation::Pickup(PickupClick::RightOutside),
5 => ClickOperation::QuickMove(QuickMoveClick::Right {
slot: o.get("slot")?,
}),
6 => ClickOperation::Swap(SwapClick {
source_slot: o.get("source_slot")?,
target_slot: o.get("target_slot")?,
}),
7 => ClickOperation::Clone(CloneClick {
slot: o.get("slot")?,
}),
8 => ClickOperation::Throw(ThrowClick::Single {
slot: o.get("slot")?,
}),
9 => ClickOperation::Throw(ThrowClick::All {
slot: o.get("slot")?,
}),
10 => ClickOperation::QuickCraft(QuickCraftClick {
kind: match o.get("kind").unwrap_or_default() {
1 => QuickCraftKind::Right,
2 => QuickCraftKind::Middle,
_ => QuickCraftKind::Left,
},
status: match o.get("status").unwrap_or_default() {
1 => QuickCraftStatus::Add {
slot: o.get("slot")?,
},
2 => QuickCraftStatus::End,
_ => QuickCraftStatus::Start,
},
}),
11 => ClickOperation::PickupAll(PickupAllClick {
slot: o.get("slot")?,
reversed: o.get("reversed").unwrap_or_default(),
}),
_ => ClickOperation::QuickMove(QuickMoveClick::Left {
slot: o.get("slot")?,
}),
})
}

View File

@ -1,5 +1,6 @@
pub mod block;
pub mod client;
pub mod container;
pub mod direction;
pub mod entity;
pub mod fluid_state;