diff --git a/pyproject.toml b/pyproject.toml index 257ecf4..6316b04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,10 +4,11 @@ build-backend = "hatchling.build" [project] name = "endstone-jwinventoryapi" -version = "3.0.0" +version = "3.0.2" dependencies = [ - "bedrock-protocol-packets-ng==0.0.9" + "bedrock-protocol-packets-ng==0.0.11" ] +requires-python = ">=3.10" authors = [ { name = "Shock95", email = "" }, ] diff --git a/src/jwinventoryapi/listener.py b/src/jwinventoryapi/listener.py index 46ed85b..0551fdd 100644 --- a/src/jwinventoryapi/listener.py +++ b/src/jwinventoryapi/listener.py @@ -1,5 +1,4 @@ from bedrock_protocol.packets import MinecraftPacketIds -from bedrock_protocol.packets.enums import ItemStackRequestActionType from bedrock_protocol.packets.packet import ( ContainerClosePacket, ItemRegistryPacket, @@ -7,58 +6,42 @@ ItemStackResponsePacket, NetworkStackLatencyPacket, ) +from bedrock_protocol.packets.types.item_stack_request import DropAction, SwapAction, TakeAction, PlaceAction from bedrock_protocol.packets.types.item_stack_response import ItemStackResponse from endstone.event import event_handler, EventPriority, PlayerQuitEvent, PacketReceiveEvent, PacketSendEvent from endstone.inventory import ItemStack from endstone.plugin import Plugin from .manager import Session -from .manager.container.item_stack_response_builder import ItemStackResponseBuilder +from .manager.container_manager.item_stack_response_builder import ItemStackResponseBuilder from .manager.player_manager import find_session, close_session from .network.container_ui_ids import ContainerUIIds -from .network.network_stack_latency_packet import NetworkStackLatencyPacket from .util.item_utils import all_item_data, add_item_data, is_air -_CONTAINER_ENUM_GUI = 7 +_CONTAINER_ENUM_GUI = ContainerUIIds.LEVEL_ENTITY class EventListener: def __init__(self, plugin: Plugin): self._plugin = plugin - @event_handler(priority=EventPriority.HIGHEST) - def on_packet_receive(self, event: PacketReceiveEvent): - player = event.player - if player is None: + def _send_item_stack_responses(self, player, responses: list[ItemStackResponse]) -> None: + if not responses: return - packet_id = event.packet_id - if packet_id == MinecraftPacketIds.Ping: - self._handle_ping(event) - elif packet_id == MinecraftPacketIds.ContainerClose: - self._handle_container_close(event) - elif packet_id == MinecraftPacketIds.PacketViolationWarning: - self._handle_violation_warning(event) - elif packet_id == MinecraftPacketIds.ItemStackRequest: - self._handle_item_stack_request_packet(event) - - @event_handler - def on_packet_send(self, event: PacketSendEvent): - if event.packet_id == MinecraftPacketIds.ItemRegistryPacket and len(all_item_data()) == 0: - pk = ItemRegistryPacket() - pk.deserialize(event.payload) - for item in pk.item_registry: - add_item_data(item.item_name, item) + pk = ItemStackResponsePacket(responses) + player.send_packet(pk.get_packet_id(), pk.serialize()) - @event_handler - def on_player_quit(self, event: PlayerQuitEvent): - close_session(event.player) + def _reject_item_stack_request(self, player, session, responses: list[ItemStackResponse], client_request_id: int) -> None: + session.container_manager.discard_transaction() + responses.append(ItemStackResponseBuilder.build_error(client_request_id)) + self._send_item_stack_responses(player, responses) - def _handle_ping(self, event: PacketReceiveEvent): - session = find_session(event.player) + def _handle_ping(self, player, payload: bytes) -> None: + session = find_session(player) if session is None: return pk = NetworkStackLatencyPacket() - pk.deserialize(event.payload) + pk.deserialize(payload) if session.ack_timestamp != pk.timestamp: return match session.state: @@ -73,13 +56,12 @@ def _handle_ping(self, event: PacketReceiveEvent): session.open_attempts += 1 session.open() - def _handle_container_close(self, event: PacketReceiveEvent): - player = event.player + def _handle_container_close(self, player, payload: bytes) -> None: session = find_session(player) if session is None: return pk = ContainerClosePacket() - pk.deserialize(event.payload) + pk.deserialize(payload) if pk.container_id != Session.CONTAINER_ID: return if session.menu is not None and session.menu._close_listener is not None: @@ -93,71 +75,32 @@ def _handle_container_close(self, event: PacketReceiveEvent): session.close(sync_inventory=True) close_session(player) - def _handle_violation_warning(self, event: PacketReceiveEvent): - session = find_session(event.player) - if session is None: - return - if session.state == Session.State.OPENING: - session.update_state(Session.State.OPEN) - if session.menu is not None and session.menu._open_listener is not None: - session.menu._open_listener(event.player) - - def _send_item_stack_responses(self, player, responses: list[ItemStackResponse]) -> None: - if not responses: + def _handle_packet_violation_warning(self, player) -> None: + session = find_session(player) + if session is None or session.state != Session.State.OPENING: return - pk = ItemStackResponsePacket(responses) - player.send_packet(pk.get_packet_id(), pk.serialize()) + session.update_state(Session.State.OPEN) + if session.menu is not None and session.menu._open_listener is not None: + session.menu._open_listener(player) - def _reject_item_stack_request(self, player, session, responses: list[ItemStackResponse], client_request_id: int) -> None: - session.container_manager.discard_transaction() - responses.append(ItemStackResponseBuilder.build_error(client_request_id)) - self._send_item_stack_responses(player, responses) - - def _handle_item_stack_request(self, player, session, menu, pk: ItemStackRequestPacket) -> None: - responses: list[ItemStackResponse] = [] - for req_data in pk.request.request_data: - session.container_manager.begin_request(req_data.client_request_id) - try: - for action in req_data.request_actions: - action_type = action.action_type - source = action.action_data.source - destination = action.action_data.distination - if action_type == ItemStackRequestActionType.Drop: - session.container_manager.handle_drop(source, action.action_data.amount) - elif action_type == ItemStackRequestActionType.Swap: - session.container_manager.handle_swap(source, destination) - elif action_type == ItemStackRequestActionType.Take or action_type == ItemStackRequestActionType.Place: - session.container_manager.transfer_items(source, destination, action.action_data.amount) - elif action_type == ItemStackRequestActionType.Destroy: - continue - responses.append(session.container_manager.commit_transaction()) - except Exception as error: - self._plugin.logger.debug(f"Error handling item stack request: {error}") - self._reject_item_stack_request(player, session, responses, req_data.client_request_id) - return - self._send_item_stack_responses(player, responses) - - def _handle_item_stack_request_packet(self, event: PacketReceiveEvent): - player = event.player + def _handle_item_stack_request_packet(self, player, payload: bytes) -> bool: session = find_session(player) if session is None or session.state != Session.State.OPEN: - return + return False pk = ItemStackRequestPacket() - pk.deserialize(event.payload) + pk.deserialize(payload) menu = session.menu if menu.is_locked: - self._handle_locked(event, session) + self._handle_locked(player, session, pk) elif menu.is_editable: - self._handle_editable(event, session) + self._handle_editable(player, session, pk) else: - self._handle_click_only(event, session) + self._handle_click_only(player, session, pk) + return True - def _handle_locked(self, event: PacketReceiveEvent, session: Session): - event.is_cancelled = True - pk = ItemStackRequestPacket() - pk.deserialize(event.payload) + def _handle_locked(self, player, session: Session, pk: ItemStackRequestPacket) -> None: responses = [] - for req_data in pk.request.request_data: + for req_data in pk.requests: session.container_manager.begin_request(req_data.client_request_id) responses.append(ItemStackResponseBuilder.build_error(req_data.client_request_id)) session.container_manager.discard_transaction() @@ -165,30 +108,24 @@ def _handle_locked(self, event: PacketReceiveEvent, session: Session): session.send_contents() session.send_player_inventory() - def _handle_editable(self, event: PacketReceiveEvent, session: Session): - player = event.player + def _handle_editable(self, player, session: Session, pk: ItemStackRequestPacket) -> None: menu = session.menu inv = menu.inventory - pk = ItemStackRequestPacket() - pk.deserialize(event.payload) inv.begin_batch() try: - for req_data in pk.request.request_data: - for action in req_data.request_actions: - action_type = action.action_type - if action_type == ItemStackRequestActionType.Destroy: - continue - if action_type not in (ItemStackRequestActionType.Take, ItemStackRequestActionType.Place, ItemStackRequestActionType.Swap): - continue - if action_type == ItemStackRequestActionType.Swap: - self._process_swap(action, inv, player) - else: - self._process_move(action, inv, player) + for req_data in pk.requests: + for action in req_data.actions: + match action: + case DropAction(): + pass + case SwapAction(): + self._process_swap(action, inv, player) + case TakeAction() | PlaceAction(): + self._process_move(action, inv, player) finally: inv.end_batch() - event.is_cancelled = True responses = [] - for req_data in pk.request.request_data: + for req_data in pk.requests: session.container_manager.begin_request(req_data.client_request_id) responses.append(ItemStackResponseBuilder.build_error(req_data.client_request_id)) session.container_manager.discard_transaction() @@ -196,16 +133,16 @@ def _handle_editable(self, event: PacketReceiveEvent, session: Session): session.send_contents() session.send_player_inventory() - def _handle_click_only(self, event: PacketReceiveEvent, session: Session): - player = event.player + def _handle_click_only(self, player, session: Session, pk: ItemStackRequestPacket) -> None: menu = session.menu - pk = ItemStackRequestPacket() - pk.deserialize(event.payload) - for req_data in pk.request.request_data: - for action in req_data.request_actions: - if action.action_type not in (ItemStackRequestActionType.Take, ItemStackRequestActionType.Place): - continue - src = action.action_data.source + for req_data in pk.requests: + for action in req_data.actions: + match action: + case TakeAction() | PlaceAction(): + pass + case _: + continue + src = action.source source_is_gui = src.container.container_enum == _CONTAINER_ENUM_GUI if source_is_gui: item_clicked = menu.inventory.get_item(src.slot) @@ -214,9 +151,8 @@ def _handle_click_only(self, event: PacketReceiveEvent, session: Session): if menu._place_listener is not None: item_from_player = player.inventory.get_item(src.slot) menu._place_listener(player, src.slot, item_from_player, menu.inventory) - event.is_cancelled = True responses = [] - for rd in pk.request.request_data: + for rd in pk.requests: session.container_manager.begin_request(rd.client_request_id) responses.append(ItemStackResponseBuilder.build_error(rd.client_request_id)) session.container_manager.discard_transaction() @@ -226,9 +162,9 @@ def _handle_click_only(self, event: PacketReceiveEvent, session: Session): return def _process_move(self, action, inv, player): - src = action.action_data.source - dst = action.action_data.distination - count = action.action_data.amount + src = action.source + dst = action.destination + count = action.amount src_is_gui = src.container.container_enum == _CONTAINER_ENUM_GUI dst_is_gui = dst.container.container_enum == _CONTAINER_ENUM_GUI if src_is_gui and not dst_is_gui: @@ -239,8 +175,8 @@ def _process_move(self, action, inv, player): self._move_within_gui(inv, src.slot, dst.slot, count) def _process_swap(self, action, inv, player): - src = action.action_data.source - dst = action.action_data.distination + src = action.source + dst = action.destination src_is_gui = src.container.container_enum == _CONTAINER_ENUM_GUI dst_is_gui = dst.container.container_enum == _CONTAINER_ENUM_GUI if src_is_gui and dst_is_gui: @@ -328,4 +264,32 @@ def _move_within_gui(self, inv, src_slot: int, dst_slot: int, count: int): else: moved = ItemStack(current.type.id, take_count, current.data) moved.set_item_meta(current.item_meta) - inv.set_item(dst_slot, moved) \ No newline at end of file + inv.set_item(dst_slot, moved) + + @event_handler(priority=EventPriority.HIGHEST) + def on_packet_receive(self, event: PacketReceiveEvent): + player = event.player + if player is None: + return + match event.packet_id: + case MinecraftPacketIds.Ping: + self._handle_ping(player, event.payload) + case MinecraftPacketIds.ContainerClose: + self._handle_container_close(player, event.payload) + case MinecraftPacketIds.PacketViolationWarning: + self._handle_packet_violation_warning(player) + case MinecraftPacketIds.ItemStackRequest: + if self._handle_item_stack_request_packet(player, event.payload): + event.cancel() + + @event_handler + def on_packet_send(self, event: PacketSendEvent): + if event.packet_id == MinecraftPacketIds.ItemRegistryPacket and len(all_item_data()) == 0: + pk = ItemRegistryPacket() + pk.deserialize(event.payload) + for item in pk.item_registry: + add_item_data(item.item_name, item) + + @event_handler + def on_player_quit(self, event: PlayerQuitEvent): + close_session(event.player) \ No newline at end of file diff --git a/src/jwinventoryapi/main.py b/src/jwinventoryapi/main.py index 0534f05..d5c1386 100644 --- a/src/jwinventoryapi/main.py +++ b/src/jwinventoryapi/main.py @@ -4,7 +4,7 @@ class JWInventoryAPI(Plugin): prefix = "JWInventoryAPI" - api_version = "0.10" + api_version = "0.11" load = "POSTWORLD" instance: 'JWInventoryAPI' = None diff --git a/src/jwinventoryapi/manager/container_manager/container_manager.py b/src/jwinventoryapi/manager/container_manager/container_manager.py index 2381547..e481a88 100644 --- a/src/jwinventoryapi/manager/container_manager/container_manager.py +++ b/src/jwinventoryapi/manager/container_manager/container_manager.py @@ -14,7 +14,7 @@ from .item_stack_tracker import ItemStackTracker if TYPE_CHECKING: - from jwinventoryapi.menu.menu_inventory import MenuInventory + from jwinventoryapi.menu import Menu class ContainerManager: diff --git a/src/jwinventoryapi/manager/container_manager/item_stack_response_builder.py b/src/jwinventoryapi/manager/container_manager/item_stack_response_builder.py index 029a60c..51aea86 100644 --- a/src/jwinventoryapi/manager/container_manager/item_stack_response_builder.py +++ b/src/jwinventoryapi/manager/container_manager/item_stack_response_builder.py @@ -48,12 +48,13 @@ def build(self) -> ItemStackResponse: for slot in slots.values(): item = self._container_manager.get_item_at(container_enum, slot) stack_id = self._tracker.get_stack_id(container_enum, slot) + is_empty = item is None or is_air(item) custom_name = self._get_custom_name(item) slot_infos.append(ItemStackResponseSlotInfo( slot=slot, hotbar_slot=slot, - count=0 if item is None or is_air(item) else item.amount, - item_stack_id=stack_id, + count=0 if is_empty else item.amount, + item_stack_id=None if is_empty else stack_id, custom_name=custom_name, filtered_custom_name=custom_name, durability_correction=self._get_durability_correction(item), diff --git a/src/jwinventoryapi/manager/container_manager/transaction_container.py b/src/jwinventoryapi/manager/container_manager/transaction_container.py index b887212..4529794 100644 --- a/src/jwinventoryapi/manager/container_manager/transaction_container.py +++ b/src/jwinventoryapi/manager/container_manager/transaction_container.py @@ -1,6 +1,6 @@ from endstone.inventory import ItemStack -from jwinventoryapi.manager.container.container_adapter import ContainerAdapter +from jwinventoryapi.manager.container_manager.container_adapter import ContainerAdapter from jwinventoryapi.util.item_utils import is_air, clone_item diff --git a/src/jwinventoryapi/manager/session.py b/src/jwinventoryapi/manager/session.py index 1bb0c79..7b1bf3f 100644 --- a/src/jwinventoryapi/manager/session.py +++ b/src/jwinventoryapi/manager/session.py @@ -6,7 +6,7 @@ from endstone import Player from endstone.inventory import ItemStack -from jwinventoryapi.manager.container.container_manager import ContainerManager +from jwinventoryapi.manager.container_manager.container_manager import ContainerManager from jwinventoryapi.menu.graphic.block_graphic import BlockGraphic from jwinventoryapi.menu.graphic.block_pair_graphic import BlockPairGraphic from jwinventoryapi.menu.graphic.graphic import Graphic @@ -22,6 +22,7 @@ _PLAYER_CONTAINER_ID = 28 + class Session: CONTAINER_ID: int = 2 MAX_OPEN_ATTEMPTS: int = 10 @@ -98,10 +99,8 @@ def send_contents(self): pk = InventoryContentPacket(self.CONTAINER_ID) for i in range(inventory.size): item_stack = inventory.get_item(i) - if is_air(item_stack): - pk.items.append(ItemStackWrapper(0, item_stack, 0)) - else: - pk.items.append(ItemStackWrapper(self._alloc_stack_id(), item_stack, 0)) + stack_id = self.container_manager.assign_virtual_slot(i, item_stack) + pk.items.append(ItemStackWrapper(stack_id, item_stack)) self.player.send_packet(pk.get_packet_id(), pk.serialize()) def send_player_inventory(self): @@ -110,17 +109,15 @@ def send_player_inventory(self): for i in range(player_inv.size): item = player_inv.get_item(i) if item is None or is_air(item): - pk.items.append(ItemStackWrapper(0, ItemStack("minecraft:air"), 0)) + pk.items.append(ItemStackWrapper(0, ItemStack("minecraft:air"))) else: - pk.items.append(ItemStackWrapper(self._alloc_stack_id(), item, 0)) + pk.items.append(ItemStackWrapper(self._alloc_stack_id(), item)) self.player.send_packet(pk.get_packet_id(), pk.serialize()) def update_slot(self, slot: int): item = self.menu.inventory.get_item(slot) - if is_air(item): - pk = InventorySlotPacket(self.CONTAINER_ID, slot, item=ItemStackWrapper(0, item, 0)) - else: - pk = InventorySlotPacket(self.CONTAINER_ID, slot, item=ItemStackWrapper(self._alloc_stack_id(), item, 0)) + stack_id = self.container_manager.assign_virtual_slot(slot, item) + pk = InventorySlotPacket(self.CONTAINER_ID, slot=slot, item=ItemStackWrapper(stack_id, item)) self.player.send_packet(pk.get_packet_id(), pk.serialize()) def close(self, sync_inventory: bool = False): diff --git a/src/jwinventoryapi/network/inventory_content_packet.py b/src/jwinventoryapi/network/inventory_content_packet.py index 20a33ef..5e1d69a 100644 --- a/src/jwinventoryapi/network/inventory_content_packet.py +++ b/src/jwinventoryapi/network/inventory_content_packet.py @@ -30,9 +30,9 @@ def write(self, stream: BinaryStream) -> None: stream.write_unsigned_varint(self.container_id) stream.write_unsigned_varint(len(self.items)) for item in self.items: - item.write_descriptor(stream) + item.write(stream) self.container_name.write(stream) - self.storage.write_descriptor(stream) + self.storage.write(stream) def read(self, stream: ReadOnlyBinaryStream) -> None: self.container_id = stream.get_unsigned_varint() \ No newline at end of file diff --git a/src/jwinventoryapi/network/inventory_slot_packet.py b/src/jwinventoryapi/network/inventory_slot_packet.py index d232669..bba3a9b 100644 --- a/src/jwinventoryapi/network/inventory_slot_packet.py +++ b/src/jwinventoryapi/network/inventory_slot_packet.py @@ -37,7 +37,7 @@ def write(self, stream: BinaryStream) -> None: stream.write_bool(self.storage is not None) if self.storage is not None: self.storage.write(stream) - self.item.write_descriptor(stream) + self.item.write(stream) def read(self, stream: ReadOnlyBinaryStream) -> None: pass \ No newline at end of file diff --git a/src/jwinventoryapi/network/item_stack_wrapper.py b/src/jwinventoryapi/network/item_stack_wrapper.py index 85d8ab2..8b6eec5 100644 --- a/src/jwinventoryapi/network/item_stack_wrapper.py +++ b/src/jwinventoryapi/network/item_stack_wrapper.py @@ -15,15 +15,6 @@ def __init__(self, stack_id: int = 0, item_stack: ItemStack | None = None, stack data = get_item_data("minecraft:air") self.data: ItemData = data - def write_header(self, stream: BinaryStream) -> bool: - if is_air(self.item_stack): - stream.write_varint(0) - return False - stream.write_varint(self.data.item_id) - stream.write_unsigned_short(self.item_stack.amount) - stream.write_unsigned_varint(self.item_stack.data) - return True - def write_footer(self, stream: BinaryStream): item_meta = self.item_stack.item_meta tag = build_tag(item_meta) @@ -37,30 +28,22 @@ def write_footer(self, stream: BinaryStream): stream.write_unsigned_int(0) def write(self, stream: BinaryStream): - if self.write_header(stream): - has_net_id = self.stack_id != 0 - stream.write_bool(has_net_id) - if has_net_id: - stream.write_varint(self.stack_id) - stream.write_varint(0) - user_data = BinaryStream() - self.write_footer(user_data) - stream.write_bytes(user_data.copy_buffer()) - - def write_descriptor(self, stream: BinaryStream): is_air_item = is_air(self.item_stack) has_net_id = self.stack_id != 0 + stream.write_signed_short(0 if is_air_item else self.data.item_id) stream.write_unsigned_short(self.item_stack.amount) stream.write_unsigned_varint(self.item_stack.data) + stream.write_bool(has_net_id) if has_net_id: - stream.write_unsigned_varint(0) stream.write_varint(self.stack_id) + stream.write_unsigned_varint(0) if is_air_item: stream.write_unsigned_varint(0) return + user_data = BinaryStream() self.write_footer(user_data) stream.write_bytes(user_data.copy_buffer()) \ No newline at end of file diff --git a/src/jwinventoryapi/network/network_stack_latency_packet.py b/src/jwinventoryapi/network/network_stack_latency_packet.py deleted file mode 100644 index 5b9dead..0000000 --- a/src/jwinventoryapi/network/network_stack_latency_packet.py +++ /dev/null @@ -1,27 +0,0 @@ -from bedrock_protocol.packets.minecraft_packet_ids import MinecraftPacketIds -from bedrock_protocol.packets.packet.packet_base import Packet -from bstream import BinaryStream, ReadOnlyBinaryStream - - -class NetworkStackLatencyPacket(Packet): - timestamp: int - from_server: bool - - def __init__(self, timestamp: int = 0, from_server: bool = False): - super().__init__() - self.timestamp = timestamp - self.from_server = from_server - - def get_packet_id(self) -> MinecraftPacketIds: - return MinecraftPacketIds.Ping - - def get_packet_name(self) -> str: - return "NetworkStackLatencyPacket" - - def write(self, stream: BinaryStream) -> None: - stream.write_unsigned_int64(self.timestamp) - stream.write_bool(self.from_server) - - def read(self, stream: ReadOnlyBinaryStream) -> None: - self.timestamp = stream.get_unsigned_int64() - self.from_server = stream.get_bool() diff --git a/src/jwinventoryapi/util/utils.py b/src/jwinventoryapi/util/utils.py index afbacac..c384b7d 100644 --- a/src/jwinventoryapi/util/utils.py +++ b/src/jwinventoryapi/util/utils.py @@ -1,11 +1,11 @@ import math import random -from bedrock_protocol.packets.packet import UpdateBlockPacket +from bedrock_protocol.packets.packet import UpdateBlockPacket, NetworkStackLatencyPacket from bedrock_protocol.packets.types import BlockPos -from endstone import Player +from endstone import Player, Server -from jwinventoryapi.network.network_stack_latency_packet import NetworkStackLatencyPacket +server: Server | None = None def send_ack_packet(player: Player) -> int: timestamp = random.randint(1, 32767) @@ -22,10 +22,16 @@ def send_block(player: Player, name: str, pos: BlockPos): def get_block_behind(player: Player, distance: int = 1) -> BlockPos: location = player.location - yaw_rad = math.radians(location.yaw + 180) - x = location.x - math.sin(yaw_rad) * distance - z = location.z + math.cos(yaw_rad) * distance - return BlockPos(math.floor(x), math.floor(location.y + 1), math.floor(z)) + yaw = location.yaw + behind_yaw = yaw + 180 + yaw_rad = math.radians(behind_yaw) + x = -math.sin(yaw_rad) * distance + z = math.cos(yaw_rad) * distance + behind_x = location.x + x + behind_y = location.y + 1 + behind_z = location.z + z + + return BlockPos(math.floor(behind_x), math.floor(behind_y), math.floor(behind_z)) def west(pos: BlockPos) -> BlockPos: @@ -33,4 +39,4 @@ def west(pos: BlockPos) -> BlockPos: def east(pos: BlockPos) -> BlockPos: - return BlockPos(pos.x + 1, pos.y, pos.z) + return BlockPos(pos.x + 1, pos.y, pos.z) \ No newline at end of file