diff --git a/Py4GWCoreLib/Inventory.py b/Py4GWCoreLib/Inventory.py index 8cf29765d..ae00d9da6 100644 --- a/Py4GWCoreLib/Inventory.py +++ b/Py4GWCoreLib/Inventory.py @@ -2,7 +2,9 @@ import PyInventory from typing import TypedDict, cast -from Py4GWCoreLib.enums_src.Item_enums import Bags +from Py4GWCoreLib.enums_src.Item_enums import ( + Bags, +) from .Item import Item from .ItemArray import ItemArray @@ -317,6 +319,106 @@ def IdentifyItem (item_id, id_kit_id): inventory = PyInventory.PyInventory() inventory.IdentifyItem(id_kit_id, item_id) + @staticmethod + def GetInventoryIDFromAgent(agent_id: int) -> int: + """ + Purpose: Resolve the native inventory ID used by the equipment panel for a player or hero agent. + Args: + agent_id (int): Agent ID of the player or hero. + Returns: int: Native inventory ID, or 0 if unavailable. + """ + return int(Inventory.inventory_instance().GetInventoryIDFromAgent(agent_id)) + + @staticmethod + def IsInventoryIDValid(inventory_id: int) -> bool: + """ + Purpose: Check whether a native inventory ID exists in the client's inventory table. + Args: + inventory_id (int): Native inventory ID. + Returns: bool: True if the inventory ID can be used by native inventory calls. + """ + return bool(Inventory.inventory_instance().IsInventoryIDValid(inventory_id)) + + @staticmethod + def GetEquippedItemID(inventory_id: int, equip_slot: int) -> int: + """ + Purpose: Return the item ID equipped in a native equipment slot. + Args: + inventory_id (int): Native inventory ID. + equip_slot (int): Native equipment slot. + Returns: int: Equipped item ID, or 0 if unavailable. + """ + return int(Inventory.inventory_instance().GetEquippedItemID(inventory_id, equip_slot)) + + @staticmethod + def GetUpgradeSlot(upgrade_item_id: int) -> int: + """ + Purpose: Return the native upgrade slot encoded by an upgrade item's interaction flags. + Args: + upgrade_item_id (int): Rune/upgrade item ID. + Returns: int: Native upgrade slot, or 0 if the item is not a supported upgrade. + """ + return int(Inventory.inventory_instance().GetUpgradeSlot(upgrade_item_id)) + + @staticmethod + def ValidateUpgrade(target_item_id: int, upgrade_item_id: int) -> bool: + """ + Purpose: Ask the native client whether an upgrade item can be applied to a target item. + Args: + target_item_id (int): Target armor/weapon item ID. + upgrade_item_id (int): Rune/upgrade item ID. + Returns: bool: True if the native validation accepts the pair. + """ + return bool(Inventory.inventory_instance().ValidateUpgrade(target_item_id, upgrade_item_id)) + + @staticmethod + def ApplyUpgrade( + inventory_id: int, + target_item_id: int, + upgrade_item_id: int, + upgrade_slot: int | None = None, + target_agent_id: int = 0, + ) -> bool: + """ + Purpose: Request the native UI upgrade flow for a target item. + Args: + inventory_id (int): Native inventory ID containing the target item. + target_item_id (int): Target armor/weapon item ID. + upgrade_item_id (int): Rune/upgrade item ID. + upgrade_slot (int | None, optional): Native upgrade slot. None lets this helper derive it; + explicit 0 is forwarded for native insignia upgrade orders. + target_agent_id (int, optional): Agent ID that owns the target inventory. + Returns: bool: True if the native UI order request was sent. + """ + try: + inventory_id = int(inventory_id or 0) + target_item_id = int(target_item_id or 0) + upgrade_item_id = int(upgrade_item_id or 0) + derive_upgrade_slot = upgrade_slot is None + upgrade_slot = 0 if derive_upgrade_slot else int(upgrade_slot or 0) + target_agent_id = int(target_agent_id or 0) + except Exception: + return False + + if not (inventory_id and target_item_id and upgrade_item_id and target_agent_id): + return False + if target_item_id == upgrade_item_id: + return False + if not Inventory.IsInventoryIDValid(inventory_id): + return False + if derive_upgrade_slot: + upgrade_slot = Inventory.GetUpgradeSlot(upgrade_item_id) + if not upgrade_slot: + return False + if not Inventory.ValidateUpgrade(target_item_id, upgrade_item_id): + return False + return bool(Inventory.inventory_instance().ApplyUpgrade( + inventory_id, + target_item_id, + upgrade_item_id, + upgrade_slot, + )) + @staticmethod def IdentifyFirst(): """ @@ -1590,9 +1692,3 @@ def WithdrawItemFromStorage(item_id, quantity=250): return True return moved_any - - - - - - diff --git a/Py4GWCoreLib/Party.py b/Py4GWCoreLib/Party.py index b3942b23e..7d6fa9dcc 100644 --- a/Py4GWCoreLib/Party.py +++ b/Py4GWCoreLib/Party.py @@ -487,6 +487,24 @@ def GetHeroAgentIDByPartyPosition(hero_position): """ return Party.party_instance().GetHeroAgentID(hero_position) + @staticmethod + def GetInventorySelectedAgentID(): + """ + Retrieve the agent ID currently selected in the inventory equipment panel. + Args: None + Returns: int + """ + return Party.party_instance().GetInventorySelectedAgentID() + + @staticmethod + def GetInventoryEquipmentFrameID(): + """ + Retrieve the native frame ID for the inventory equipment panel. + Args: None + Returns: int + """ + return Party.party_instance().GetInventoryEquipmentFrameID() + @staticmethod @frame_cache(category="Party.Heroes", source_lib="GetHeroAgentIDByHeroID") def GetHeroIDByAgentID(agent_id): diff --git a/Py4GWCoreLib/enums_src/Item_enums.py b/Py4GWCoreLib/enums_src/Item_enums.py index 023562330..1f4a2d4c1 100644 --- a/Py4GWCoreLib/enums_src/Item_enums.py +++ b/Py4GWCoreLib/enums_src/Item_enums.py @@ -169,6 +169,55 @@ def is_weapon_type_literal(item_type: ItemType) -> TypeIs[WeaponType]: ArmorType: TypeAlias = Literal[ItemType.Headpiece, ItemType.Chestpiece, ItemType.Gloves, ItemType.Leggings, ItemType.Boots, ItemType.Salvage] ARMOR_TYPES = frozenset(cast(tuple[ItemType, ...], get_args(ArmorType))) +RUNE_MOD_ITEM_TYPE = ItemType.Rune_Mod.value + +ARMOR_RUNE_UPGRADE_SLOT = 1 +ARMOR_INSIGNIA_UPGRADE_SLOT = 7 +ARMOR_UPGRADE_SLOTS = frozenset((ARMOR_RUNE_UPGRADE_SLOT, ARMOR_INSIGNIA_UPGRADE_SLOT)) +ARMOR_EQUIPMENT_SLOTS = ( + ("Chest", 2), + ("Leggings", 3), + ("Head", 4), + ("Boots", 5), + ("Gloves", 6), +) + +EQUIPPED_WEAPON_SLOTS = ( + ("main hand", 0), + ("offhand", 1), +) + +WEAPON_UPGRADE_CLASSIFICATION_SLOT = 6 +WEAPON_PREFIX_REQUEST_SLOT = 0 +WEAPON_SUFFIX_REQUEST_SLOT = 1 +WEAPON_INSCRIPTION_REQUEST_SLOT = 2 +WEAPON_UPGRADE_REQUEST_SLOTS = { + "prefix": WEAPON_PREFIX_REQUEST_SLOT, + "suffix": WEAPON_SUFFIX_REQUEST_SLOT, + "inscription": WEAPON_INSCRIPTION_REQUEST_SLOT, +} + +WEAPON_PREFIX_NAME_MARKERS = ( + "axe haft", + "bowstring", + "bow string", + "dagger tang", + "hammer haft", + "scythe snathe", + "spearhead", + "spear head", + "staff head", + "sword hilt", +) + +WEAPON_SUFFIX_NAME_MARKERS = ( + "focus core", + "grip", + "handle", + "pommel", + "wrapping", +) + ITEM_TYPE_META_TYPES: dict[ItemType, list[ItemType]] = { ItemType.Weapon: [ ItemType.Axe, diff --git a/Py4GWCoreLib/enums_src/UI_enums.py b/Py4GWCoreLib/enums_src/UI_enums.py index 855f61612..981e40b57 100644 --- a/Py4GWCoreLib/enums_src/UI_enums.py +++ b/Py4GWCoreLib/enums_src/UI_enums.py @@ -1,6 +1,9 @@ from enum import Enum from enum import IntEnum +INVENTORY_EQUIPMENT_FRAME_LABEL = "Inventory-Equipment" +INVENTORY_SET_AGENT_FRAME_MESSAGE = 0x56 + #region ImguiFonts class ImguiFonts(IntEnum): diff --git a/Widgets/Automation/Enhancements/Hero Inventory Selector.py b/Widgets/Automation/Enhancements/Hero Inventory Selector.py new file mode 100644 index 000000000..d95d2bd64 --- /dev/null +++ b/Widgets/Automation/Enhancements/Hero Inventory Selector.py @@ -0,0 +1,298 @@ +from Py4GWCoreLib import * +from Py4GWCoreLib.enums_src.UI_enums import ( + INVENTORY_EQUIPMENT_FRAME_LABEL, + INVENTORY_SET_AGENT_FRAME_MESSAGE, + WindowID, +) +import Py4GW +import time + +MODULE_NAME = "Inventory Hero Selector" +MODULE_ICON = "Textures/Module_Icons/Hero Helper.png" + +NATIVE_RETRY_DELAY_SECONDS = 0.08 +NATIVE_RETRY_LIMIT = 8 +NATIVE_TIMEOUT_SECONDS = 2.5 + +_last_status = "Ready." +_selection_state = "idle" +_selection_hero_agent_id = 0 +_selection_deadline = 0.0 +_selection_next_action = 0.0 +_selection_native_attempts = 0 +_selection_direct_pending = False +_selection_direct_completed = False +_selection_direct_sent = False +_selection_direct_frame_id = 0 +_selection_direct_error = "" + + +def _log_status(message, message_type): + global _last_status + _last_status = message + Py4GW.Console.Log(MODULE_NAME, message, message_type) + + +def _ensure_inventory_window_open(): + if not UIManager.IsWindowVisible(WindowID.WindowID_Inventory): + UIManager.SetWindowVisible(WindowID.WindowID_Inventory, True) + + +def _hero_name(): + return Party.Heroes.GetNameByAgentID(_selection_hero_agent_id) or f"agent {_selection_hero_agent_id}" + + +def _current_hero_one_agent_id(): + try: + return int(Party.Heroes.GetHeroAgentIDByPartyPosition(1) or 0) + except Exception: + return 0 + + +def _is_selection_target_current(): + current_agent_id = _current_hero_one_agent_id() + if current_agent_id == _selection_hero_agent_id: + return True + + if current_agent_id: + _log_status( + f"Selection canceled: hero 1 changed (old agent {_selection_hero_agent_id}, new agent {current_agent_id}).", + Py4GW.Console.MessageType.Warning, + ) + else: + _log_status("Selection canceled: hero 1 is no longer in the party.", Py4GW.Console.MessageType.Warning) + return False + + +def _get_valid_inventory_id_for_agent(agent_id): + global _selection_direct_error + + try: + inventory_id = int(Inventory.GetInventoryIDFromAgent(agent_id) or 0) + except Exception as exception: + _selection_direct_error = f"inventory unavailable: {exception}" + return 0 + + if not inventory_id: + _selection_direct_error = "inventory not ready" + return 0 + + if inventory_id == int(agent_id): + _selection_direct_error = f"inventory {inventory_id} matches the agent" + return 0 + + validator = getattr(Inventory, "IsInventoryIDValid", None) + if not callable(validator): + _selection_direct_error = "inventory validator unavailable" + return 0 + + try: + if not bool(validator(inventory_id)): + _selection_direct_error = f"inventory {inventory_id} is invalid" + return 0 + except Exception as exception: + _selection_direct_error = f"inventory validation failed: {exception}" + return 0 + + return inventory_id + + +def _selection_debug_details(): + try: + selected_agent_id = Party.Heroes.GetInventorySelectedAgentID() + visible = UIManager.IsWindowVisible(WindowID.WindowID_Inventory) + direct_error = _selection_direct_error or "none" + return ( + f"selected_agent_id={selected_agent_id}, " + f"direct_frame={_selection_direct_frame_id}, " + f"direct_sent={1 if _selection_direct_sent else 0}, " + f"direct_done={1 if _selection_direct_completed else 0}, " + f"direct_error={direct_error}, " + f"inventory_visible={visible}" + ) + except Exception as exception: + return f"diagnostics unavailable: {exception}" + + +def _get_inventory_equipment_frame_id(): + native_resolver = getattr(Party.Heroes, "GetInventoryEquipmentFrameID", None) + if native_resolver: + return int(native_resolver() or 0) + + return int(UIManager.GetFrameIDByLabel(INVENTORY_EQUIPMENT_FRAME_LABEL) or 0) + + +def _queue_direct_inventory_label_select(): + global _selection_direct_pending, _selection_direct_completed, _selection_direct_sent, _selection_direct_frame_id, _selection_direct_error + agent_id = _selection_hero_agent_id + _selection_direct_pending = True + _selection_direct_completed = False + _selection_direct_sent = False + _selection_direct_frame_id = 0 + _selection_direct_error = "" + + def _send_on_game_thread(): + global _selection_direct_pending, _selection_direct_completed, _selection_direct_sent, _selection_direct_frame_id, _selection_direct_error + try: + current_agent_id = _current_hero_one_agent_id() + if current_agent_id != agent_id: + _selection_direct_error = f"hero 1 changed before sending (old agent {agent_id}, new agent {current_agent_id})" + _selection_direct_sent = False + return + + inventory_id = _get_valid_inventory_id_for_agent(agent_id) + if not inventory_id: + _selection_direct_sent = False + return + + frame_id = _get_inventory_equipment_frame_id() + _selection_direct_frame_id = frame_id + _selection_direct_sent = bool( + frame_id and UIManager.SendFrameUIMessage( + frame_id, + INVENTORY_SET_AGENT_FRAME_MESSAGE, + agent_id, + 0, + ) + ) + except Exception as exception: + _selection_direct_error = str(exception) + _selection_direct_sent = False + finally: + _selection_direct_completed = True + _selection_direct_pending = False + + Py4GW.Game.enqueue(_send_on_game_thread) + + +def _try_native_select_hero_one(): + global _selection_native_attempts + + selected_agent_id = Party.Heroes.GetInventorySelectedAgentID() + if selected_agent_id == _selection_hero_agent_id: + _log_status(f"{_hero_name()} selected in inventory.", Py4GW.Console.MessageType.Info) + return True + + if _selection_direct_pending and not _selection_direct_completed: + _log_status( + f"Direct native message pending for {_hero_name()}; label={INVENTORY_EQUIPMENT_FRAME_LABEL}.", + Py4GW.Console.MessageType.Info, + ) + return False + + if _selection_native_attempts >= NATIVE_RETRY_LIMIT: + return False + + _selection_native_attempts += 1 + _queue_direct_inventory_label_select() + if _selection_direct_completed and not _selection_direct_sent and _selection_direct_error: + _log_status( + f"Native selection deferred for {_hero_name()}: {_selection_direct_error} " + f"(attempt {_selection_native_attempts}/{NATIVE_RETRY_LIMIT}; current inventory agent: {selected_agent_id}).", + Py4GW.Console.MessageType.Warning, + ) + else: + _log_status( + f"Sending native Python 0x{INVENTORY_SET_AGENT_FRAME_MESSAGE:X} for {_hero_name()} via {INVENTORY_EQUIPMENT_FRAME_LABEL} " + f"(attempt {_selection_native_attempts}/{NATIVE_RETRY_LIMIT}; current inventory agent: {selected_agent_id}).", + Py4GW.Console.MessageType.Info, + ) + return False + + +def _start_native_selection(hero_agent_id): + global _selection_state, _selection_hero_agent_id, _selection_deadline, _selection_next_action, _selection_native_attempts, _selection_direct_pending, _selection_direct_completed, _selection_direct_sent, _selection_direct_frame_id, _selection_direct_error + _selection_hero_agent_id = hero_agent_id + _selection_native_attempts = 0 + _selection_direct_pending = False + _selection_direct_completed = False + _selection_direct_sent = False + _selection_direct_frame_id = 0 + _selection_direct_error = "" + now = time.perf_counter() + _selection_deadline = now + NATIVE_TIMEOUT_SECONDS + + _ensure_inventory_window_open() + _selection_state = "retry_native" + _selection_next_action = now + _log_status(f"Hero 1 selection request received ({_hero_name()}); native call pending.", Py4GW.Console.MessageType.Info) + + +def _tick_native_selection(): + global _selection_state, _selection_next_action, _selection_native_attempts + if _selection_state == "idle": + return + + now = time.perf_counter() + if now > _selection_deadline: + _selection_state = "idle" + _log_status( + f"Native selection failed for {_hero_name()} ({_selection_debug_details()}).", + Py4GW.Console.MessageType.Warning, + ) + return + + if now < _selection_next_action: + return + + if not _is_selection_target_current(): + _selection_state = "idle" + return + + if _selection_state == "retry_native": + _ensure_inventory_window_open() + if _try_native_select_hero_one(): + _selection_state = "idle" + return + + if _selection_direct_pending or _selection_native_attempts < NATIVE_RETRY_LIMIT: + _selection_next_action = now + NATIVE_RETRY_DELAY_SECONDS + return + + _selection_state = "idle" + _log_status( + f"Native selection failed for {_hero_name()} ({_selection_debug_details()}).", + Py4GW.Console.MessageType.Warning, + ) + return + + +def _select_hero_one(): + hero_agent_id = Party.Heroes.GetHeroAgentIDByPartyPosition(1) + if not hero_agent_id: + _log_status("Hero 1 not found in the party.", Py4GW.Console.MessageType.Warning) + return + + _start_native_selection(hero_agent_id) + + +def _log_runtime_error(context, exception): + global _selection_state + _selection_state = "idle" + _log_status(f"Error {context}: {exception}", Py4GW.Console.MessageType.Error) + + +def _safe_select_hero_one(): + try: + _select_hero_one() + except Exception as exception: + _log_runtime_error("hero selection", exception) + + +def _safe_tick_native_selection(): + try: + _tick_native_selection() + except Exception as exception: + _log_runtime_error("hero selection tracking", exception) + + +def main(): + _safe_tick_native_selection() + + if PyImGui.begin(MODULE_NAME, PyImGui.WindowFlags.AlwaysAutoResize): + if PyImGui.button("Select hero 1", width=180, height=28): + _safe_select_hero_one() + + PyImGui.text(_last_status) + + PyImGui.end() diff --git a/Widgets/Automation/Enhancements/Inventory Rune Applier.py b/Widgets/Automation/Enhancements/Inventory Rune Applier.py new file mode 100644 index 000000000..2473ff162 --- /dev/null +++ b/Widgets/Automation/Enhancements/Inventory Rune Applier.py @@ -0,0 +1,248 @@ +import Py4GW +from Py4GWCoreLib import Inventory, Item, ItemArray, Party, Player, PyImGui +from Py4GWCoreLib.enums_src.Item_enums import ( + ARMOR_EQUIPMENT_SLOTS, + ARMOR_INSIGNIA_UPGRADE_SLOT, + ARMOR_UPGRADE_SLOTS, + INVENTORY_BAGS, +) + + +MODULE_NAME = "Inventory Armor Upgrader" +MODULE_ICON = "Textures/Module_Icons/Inventory.png" + +_selected_rune_index = 0 +_selected_armor_slot_index = 0 +_selected_target_agent_index = 0 +_target_agent_options = [] +_last_status = "Ready." +_rune_name_cache = {} +_rune_name_requested = set() + + +def _log_status(message, message_type): + global _last_status + _last_status = message + Py4GW.Console.Log(MODULE_NAME, message, message_type) + + +def _inventory_bag_ids(): + return [int(getattr(bag, "value", bag)) for bag in INVENTORY_BAGS] + + +def _safe_item_name(item_id): + if item_id in _rune_name_cache: + return _rune_name_cache[item_id] + + try: + if Item.IsNameReady(item_id): + name = Item.GetName(item_id) + if name and name not in ("Unknown", "Timeout"): + _rune_name_cache[item_id] = name + _rune_name_requested.discard(item_id) + return name + except Exception: + pass + + if item_id not in _rune_name_requested: + try: + Item.RequestName(item_id) + _rune_name_requested.add(item_id) + except Exception: + pass + return f"item {item_id}" + + +def _get_inventory_upgrade_items(): + upgrades = [] + bags = ItemArray.CreateBagList(*_inventory_bag_ids()) + for item_id in ItemArray.GetItemArray(bags): + try: + if int(Inventory.GetUpgradeSlot(item_id)) not in ARMOR_UPGRADE_SLOTS: + continue + except Exception: + continue + upgrades.append((int(item_id), _safe_item_name(int(item_id)))) + return upgrades + + +def _get_target_agent_id(options=None): + if options is None: + options = _target_agent_options or _get_target_agent_options() + if not options: + return 0 + + index = max(0, min(_selected_target_agent_index, len(options) - 1)) + return int(options[index][0] or 0) + + +def _get_target_agent_options(): + options = [] + seen_agent_ids = set() + + try: + player_agent_id = int(Player.GetAgentID() or 0) + except Exception: + player_agent_id = 0 + if player_agent_id: + options.append((player_agent_id, f"Player ({player_agent_id})")) + seen_agent_ids.add(player_agent_id) + + try: + heroes = list(Party.GetHeroes() or []) + except Exception: + heroes = [] + + for hero_index, hero in enumerate(heroes, start=1): + try: + agent_id = int(getattr(hero, "agent_id", 0) or 0) + except Exception: + agent_id = 0 + if not agent_id or agent_id in seen_agent_ids: + continue + + try: + name = Party.Heroes.GetNameByAgentID(agent_id) or "" + except Exception: + name = "" + if not name: + name = f"Hero {hero_index}" + + options.append((agent_id, f"{name} ({agent_id})")) + seen_agent_ids.add(agent_id) + + return options + + +def _clamp_selected_indexes(runes, target_options=None): + global _selected_rune_index, _selected_armor_slot_index, _selected_target_agent_index + if runes: + _selected_rune_index = max(0, min(_selected_rune_index, len(runes) - 1)) + else: + _selected_rune_index = 0 + _selected_armor_slot_index = max(0, min(_selected_armor_slot_index, len(ARMOR_EQUIPMENT_SLOTS) - 1)) + if target_options: + _selected_target_agent_index = max(0, min(_selected_target_agent_index, len(target_options) - 1)) + else: + _selected_target_agent_index = 0 + + +def _get_upgrade_request_slot(item_upgrade_slot): + if item_upgrade_slot == ARMOR_INSIGNIA_UPGRADE_SLOT: + return 0 + return item_upgrade_slot + + +def _resolve_upgrade_application(agent_id, equip_slot, upgrade_item_id): + inventory_id = int(Inventory.GetInventoryIDFromAgent(agent_id) or 0) + if not inventory_id: + return False, "inventory_id=0", None + + target_item_id = int(Inventory.GetEquippedItemID(inventory_id, equip_slot) or 0) + if not target_item_id: + return False, f"inventory_id={inventory_id}, target_item=0", None + if target_item_id == int(upgrade_item_id): + return False, f"inventory_id={inventory_id}, target_item={target_item_id}, upgrade_item={int(upgrade_item_id)}", None + + upgrade_slot = int(Inventory.GetUpgradeSlot(upgrade_item_id) or 0) + if upgrade_slot not in ARMOR_UPGRADE_SLOTS: + return False, f"inventory_id={inventory_id}, target_item={target_item_id}, upgrade_item={int(upgrade_item_id)}, upgrade_slot={upgrade_slot}", None + request_upgrade_slot = _get_upgrade_request_slot(upgrade_slot) + + validate_ok = bool(Inventory.ValidateUpgrade(target_item_id, upgrade_item_id)) + if not validate_ok: + return False, f"inventory_id={inventory_id}, target_item={target_item_id}, upgrade_item={int(upgrade_item_id)}, validate=0", None + + request = { + "inventory_id": inventory_id, + "target_item_id": target_item_id, + "upgrade_slot": request_upgrade_slot, + } + diagnostic = f"inventory_id={inventory_id}, target_item={target_item_id}, upgrade_item={int(upgrade_item_id)}, upgrade_slot={request_upgrade_slot}" + if request_upgrade_slot != upgrade_slot: + diagnostic = f"{diagnostic}, item_upgrade_slot={upgrade_slot}" + return True, diagnostic, request + + +def _apply_selected_upgrade(): + upgrades = _get_inventory_upgrade_items() + target_options = _get_target_agent_options() + _clamp_selected_indexes(upgrades, target_options) + if not upgrades: + _log_status("No applicable armor upgrade found in inventory.", Py4GW.Console.MessageType.Warning) + return False + + agent_id = _get_target_agent_id(target_options) + if not agent_id: + _log_status("No target agent available.", Py4GW.Console.MessageType.Warning) + return False + + upgrade_item_id, upgrade_name = upgrades[_selected_rune_index] + armor_label, equip_slot = ARMOR_EQUIPMENT_SLOTS[_selected_armor_slot_index] + try: + request_ok, diagnostic, request = _resolve_upgrade_application(agent_id, equip_slot, upgrade_item_id) + applied = bool(request_ok and request and Inventory.ApplyUpgrade( + request["inventory_id"], + request["target_item_id"], + upgrade_item_id, + request["upgrade_slot"], + agent_id, + )) + except Exception as exception: + _log_status(f"Upgrade application error: {exception}", Py4GW.Console.MessageType.Error) + return False + + if applied: + _log_status( + f"Application request sent for {upgrade_name} on {armor_label} (agent {agent_id}; {diagnostic}).", + Py4GW.Console.MessageType.Info, + ) + return True + + _log_status( + f"Native application refused for {upgrade_name} on {armor_label} (agent {agent_id}; {diagnostic}).", + Py4GW.Console.MessageType.Warning, + ) + return False + + +def _safe_apply_selected_upgrade(): + try: + return _apply_selected_upgrade() + except Exception as exception: + _log_status(f"Upgrade widget error: {exception}", Py4GW.Console.MessageType.Error) + return False + + +def main(): + global _selected_rune_index, _selected_armor_slot_index, _selected_target_agent_index, _target_agent_options + + if PyImGui.begin(MODULE_NAME, PyImGui.WindowFlags.AlwaysAutoResize): + upgrades = _get_inventory_upgrade_items() + _target_agent_options = _get_target_agent_options() + _clamp_selected_indexes(upgrades, _target_agent_options) + + if _target_agent_options: + target_labels = [label for _, label in _target_agent_options] + _selected_target_agent_index = PyImGui.combo("Target", _selected_target_agent_index, target_labels) + else: + PyImGui.text("No player or hero available.") + + if upgrades: + upgrade_labels = [f"{name} ({item_id})" for item_id, name in upgrades] + _selected_rune_index = PyImGui.combo("Upgrade", _selected_rune_index, upgrade_labels) + else: + PyImGui.text("No applicable armor upgrade in bags.") + + armor_labels = [label for label, _ in ARMOR_EQUIPMENT_SLOTS] + _selected_armor_slot_index = PyImGui.combo("Armor piece", _selected_armor_slot_index, armor_labels) + + target_agent_id = _get_target_agent_id() + PyImGui.text(f"Target agent: {target_agent_id or 'none'}") + + if PyImGui.button("Apply upgrade", width=180, height=28): + _safe_apply_selected_upgrade() + + PyImGui.text(_last_status) + + PyImGui.end() diff --git a/Widgets/Automation/Enhancements/Inventory Weapon Upgrader.py b/Widgets/Automation/Enhancements/Inventory Weapon Upgrader.py new file mode 100644 index 000000000..58af11cb8 --- /dev/null +++ b/Widgets/Automation/Enhancements/Inventory Weapon Upgrader.py @@ -0,0 +1,456 @@ +import Py4GW +from Py4GWCoreLib import Inventory, Item, ItemArray, Party, Player, PyImGui +from Py4GWCoreLib.enums_src.Item_enums import ( + ARMOR_INSIGNIA_UPGRADE_SLOT, + ARMOR_RUNE_UPGRADE_SLOT, + EQUIPPED_WEAPON_SLOTS, + INVENTORY_BAGS, + RUNE_MOD_ITEM_TYPE, + WEAPON_PREFIX_NAME_MARKERS, + WEAPON_SUFFIX_NAME_MARKERS, + WEAPON_UPGRADE_CLASSIFICATION_SLOT, + WEAPON_UPGRADE_REQUEST_SLOTS, +) + + +MODULE_NAME = "Inventory Weapon Upgrader" +MODULE_ICON = "Textures/Module_Icons/Inventory.png" + +_selected_weapon_index = 0 +_selected_upgrade_index = 0 +_last_status = "Ready." +_item_name_cache = {} +_item_name_requested = set() + + +def _log_status(message, message_type): + global _last_status + _last_status = message + Py4GW.Console.Log(MODULE_NAME, message, message_type) + + +def _inventory_bag_ids(): + return [int(getattr(bag, "value", bag)) for bag in INVENTORY_BAGS] + + +def _safe_item_name(item_id): + item_id = int(item_id or 0) + if item_id in _item_name_cache: + return _item_name_cache[item_id] + + try: + if Item.IsNameReady(item_id): + name = Item.GetName(item_id) + if name and name not in ("Unknown", "Timeout"): + _item_name_cache[item_id] = name + _item_name_requested.discard(item_id) + return name + except Exception: + pass + + if item_id not in _item_name_requested: + try: + Item.RequestName(item_id) + _item_name_requested.add(item_id) + except Exception: + pass + return f"item {item_id}" + + +def _get_inventory_items(): + try: + bags = ItemArray.CreateBagList(*_inventory_bag_ids()) + return [int(item_id) for item_id in ItemArray.GetItemArray(bags)] + except Exception: + return [] + + +def _is_weapon_item(item_id): + try: + return bool(Item.IsWeapon(item_id)) + except Exception: + return False + + +def _is_inscription_item(item_id): + try: + if bool(Item.Customization.IsInscription(item_id)): + return True + except Exception: + pass + return _is_inscription_display_name(item_id) + + +def _is_inscription_display_name(item_id): + name = _safe_item_name(item_id).strip().lower() + return name.startswith("inscription:") + + +def _normalized_item_name(item_id): + return _safe_item_name(item_id).strip().lower() + + +def _is_armor_upgrade_display_name(item_id): + name = _normalized_item_name(item_id) + return "insignia" in name or "rune" in name + + +def _classify_weapon_upgrade_from_display_name(item_id): + if _is_inscription_display_name(item_id): + return "inscription" + if _is_armor_upgrade_display_name(item_id): + return "armor" + + name = _normalized_item_name(item_id) + if any(marker in name for marker in WEAPON_PREFIX_NAME_MARKERS): + return "prefix" + if any(marker in name for marker in WEAPON_SUFFIX_NAME_MARKERS): + return "suffix" + if "@itemenhance" in name: + return "weapon" + return "unknown" + + +def _is_rune_mod_item(item_id): + try: + item_type_value, item_type_name = Item.GetItemType(item_id) + except Exception: + return False + try: + if int(item_type_value) == RUNE_MOD_ITEM_TYPE: + return True + except Exception: + pass + return str(item_type_name or "") == "Rune_Mod" + + +def _is_weapon_upgrade_item(item_id): + display_kind = _classify_weapon_upgrade_from_display_name(item_id) + if display_kind in ("prefix", "suffix", "inscription"): + return True + if display_kind == "armor": + return False + + try: + upgrade_slot = int(Inventory.GetUpgradeSlot(item_id) or 0) + if upgrade_slot in (ARMOR_RUNE_UPGRADE_SLOT, ARMOR_INSIGNIA_UPGRADE_SLOT): + return False + if upgrade_slot == WEAPON_UPGRADE_CLASSIFICATION_SLOT: + return True + except Exception: + pass + if _is_inscription_item(item_id): + return True + return _is_rune_mod_item(item_id) and not _is_armor_upgrade_display_name(item_id) + + +def _get_inventory_weapon_items(): + return [(entry["item_id"], entry["name"]) for entry in _get_inventory_weapon_target_entries()] + + +def _make_weapon_target_entry(item_id, name, label=None, inventory_id=0, target_agent_id=0, source="inventory"): + return { + "item_id": int(item_id or 0), + "name": str(name or ""), + "label": str(label or name or f"item {int(item_id or 0)}"), + "inventory_id": int(inventory_id or 0), + "target_agent_id": int(target_agent_id or 0), + "source": str(source or ""), + } + + +def _get_inventory_weapon_target_entries(): + weapons = [] + target_agent_id, inventory_id = _get_player_inventory_context() + for item_id in _get_inventory_items(): + if not _is_weapon_item(item_id): + continue + name = _safe_item_name(item_id) + weapons.append(_make_weapon_target_entry( + item_id, + name, + inventory_id=inventory_id, + target_agent_id=target_agent_id, + source="inventory", + )) + return weapons + + +def _get_party_weapon_target_agents(): + agents = [] + seen_agent_ids = set() + + try: + player_agent_id = int(Player.GetAgentID() or 0) + except Exception: + player_agent_id = 0 + if player_agent_id: + agents.append((player_agent_id, "Player")) + seen_agent_ids.add(player_agent_id) + + try: + heroes = list(Party.GetHeroes() or []) + except Exception: + heroes = [] + + for hero_index, hero in enumerate(heroes, start=1): + try: + agent_id = int(getattr(hero, "agent_id", 0) or 0) + except Exception: + agent_id = 0 + if not agent_id or agent_id in seen_agent_ids: + continue + + try: + name = Party.Heroes.GetNameByAgentID(agent_id) or "" + except Exception: + name = "" + if not name: + name = f"Hero {hero_index}" + + agents.append((agent_id, name)) + seen_agent_ids.add(agent_id) + + return agents + + +def _get_equipped_weapon_target_entries(): + weapons = [] + for agent_id, agent_label in _get_party_weapon_target_agents(): + try: + inventory_id = int(Inventory.GetInventoryIDFromAgent(agent_id) or 0) + except Exception: + inventory_id = 0 + if not inventory_id: + continue + + for slot_label, equip_slot in EQUIPPED_WEAPON_SLOTS: + try: + item_id = int(Inventory.GetEquippedItemID(inventory_id, equip_slot) or 0) + except Exception: + item_id = 0 + if not item_id or not _is_weapon_item(item_id): + continue + + name = _safe_item_name(item_id) + weapons.append(_make_weapon_target_entry( + item_id, + name, + label=f"{agent_label} {slot_label}: {name}", + inventory_id=inventory_id, + target_agent_id=agent_id, + source=f"equipped:{slot_label}", + )) + + return weapons + + +def _get_weapon_target_entries(): + return _get_inventory_weapon_target_entries() + _get_equipped_weapon_target_entries() + + +def _get_inventory_weapon_upgrade_items(): + upgrades = [] + for item_id in _get_inventory_items(): + if not _is_weapon_upgrade_item(item_id): + continue + upgrades.append((item_id, _safe_item_name(item_id))) + return upgrades + + +def _clamp_selected_indexes(weapons, upgrades): + global _selected_weapon_index, _selected_upgrade_index + if weapons: + _selected_weapon_index = max(0, min(_selected_weapon_index, len(weapons) - 1)) + else: + _selected_weapon_index = 0 + if upgrades: + _selected_upgrade_index = max(0, min(_selected_upgrade_index, len(upgrades) - 1)) + else: + _selected_upgrade_index = 0 + + +def _classify_weapon_upgrade(item_id): + display_kind = _classify_weapon_upgrade_from_display_name(item_id) + if display_kind in ("prefix", "suffix", "inscription"): + return display_kind + if _is_inscription_item(item_id): + return "inscription" + try: + if int(Inventory.GetUpgradeSlot(item_id) or 0) == WEAPON_UPGRADE_CLASSIFICATION_SLOT: + return "weapon" + except Exception: + pass + return "unknown" + + +def _get_confirmed_weapon_request_slot(upgrade_item_id): + upgrade_kind = _classify_weapon_upgrade(upgrade_item_id) + return WEAPON_UPGRADE_REQUEST_SLOTS.get(upgrade_kind) + + +def _get_player_inventory_context(): + try: + target_agent_id = int(Player.GetAgentID() or 0) + except Exception: + target_agent_id = 0 + try: + inventory_id = int(Inventory.GetInventoryIDFromAgent(target_agent_id) or 0) if target_agent_id else 0 + except Exception: + inventory_id = 0 + return target_agent_id, inventory_id + + +def _get_weapon_target_context(target_context=None): + if isinstance(target_context, dict): + try: + target_agent_id = int(target_context.get("target_agent_id", 0) or 0) + except Exception: + target_agent_id = 0 + try: + inventory_id = int(target_context.get("inventory_id", 0) or 0) + except Exception: + inventory_id = 0 + return target_agent_id, inventory_id + return _get_player_inventory_context() + + +def _resolve_weapon_upgrade_application(target_weapon_id, upgrade_item_id, target_context=None): + try: + target_weapon_id = int(target_weapon_id or 0) + upgrade_item_id = int(upgrade_item_id or 0) + except Exception: + return False, "target_item=0, upgrade_item=0", None + + if not target_weapon_id: + return False, f"target_item={target_weapon_id}, upgrade_item={upgrade_item_id}", None + if not upgrade_item_id: + return False, f"target_item={target_weapon_id}, upgrade_item=0", None + if target_weapon_id == upgrade_item_id: + return False, f"target_item={target_weapon_id}, upgrade_item={upgrade_item_id}", None + if not _is_weapon_item(target_weapon_id): + return False, f"target_item={target_weapon_id}, upgrade_item={upgrade_item_id}, target_weapon=0", None + if not _is_weapon_upgrade_item(upgrade_item_id): + return False, f"target_item={target_weapon_id}, upgrade_item={upgrade_item_id}, weapon_upgrade=0", None + + validate_ok = bool(Inventory.ValidateUpgrade(target_weapon_id, upgrade_item_id)) + if not validate_ok: + return False, f"target_item={target_weapon_id}, upgrade_item={upgrade_item_id}, validate=0", None + + request_slot = _get_confirmed_weapon_request_slot(upgrade_item_id) + if request_slot is None: + return ( + False, + f"target_item={target_weapon_id}, upgrade_item={upgrade_item_id}, validate=1, request_slot=unconfirmed", + None, + ) + + target_agent_id, inventory_id = _get_weapon_target_context(target_context) + if not target_agent_id or not inventory_id: + return ( + False, + ( + f"target_item={target_weapon_id}, upgrade_item={upgrade_item_id}, validate=1, " + f"request_slot={int(request_slot)}, target_agent_id={target_agent_id}, inventory_id={inventory_id}" + ), + None, + ) + + request = { + "inventory_id": inventory_id, + "target_item_id": target_weapon_id, + "upgrade_item_id": upgrade_item_id, + "upgrade_slot": int(request_slot), + "target_agent_id": target_agent_id, + } + return ( + True, + ( + f"target_item={target_weapon_id}, upgrade_item={upgrade_item_id}, validate=1, " + f"request_slot={int(request_slot)}, target_agent_id={target_agent_id}, inventory_id={inventory_id}" + ), + request, + ) + + +def _apply_selected_weapon_upgrade(): + weapons = _get_weapon_target_entries() + upgrades = _get_inventory_weapon_upgrade_items() + _clamp_selected_indexes(weapons, upgrades) + + if not weapons: + _log_status("No weapon found in inventory bags or equipped by party.", Py4GW.Console.MessageType.Warning) + return False + if not upgrades: + _log_status("No weapon upgrade component found in inventory bags.", Py4GW.Console.MessageType.Warning) + return False + + target_weapon = weapons[_selected_weapon_index] + target_weapon_id = int(target_weapon["item_id"]) + target_weapon_name = str(target_weapon["label"]) + upgrade_item_id, upgrade_name = upgrades[_selected_upgrade_index] + ok, diagnostic, request = _resolve_weapon_upgrade_application(target_weapon_id, upgrade_item_id, target_weapon) + if not ok or request is None: + if "request_slot=unconfirmed" in diagnostic: + _log_status( + f"Unsupported weapon upgrade type for {upgrade_name} on {target_weapon_name} ({diagnostic}).", + Py4GW.Console.MessageType.Warning, + ) + else: + _log_status( + f"Native validation refused {upgrade_name} on {target_weapon_name} ({diagnostic}).", + Py4GW.Console.MessageType.Warning, + ) + return False + + sent = bool(Inventory.ApplyUpgrade( + request["inventory_id"], + request["target_item_id"], + request["upgrade_item_id"], + request["upgrade_slot"], + request["target_agent_id"], + )) + if not sent: + _log_status( + f"Native upgrade order refused for {upgrade_name} on {target_weapon_name} ({diagnostic}).", + Py4GW.Console.MessageType.Error, + ) + return False + + _log_status( + f"Native upgrade order sent for {upgrade_name} on {target_weapon_name} ({diagnostic}).", + Py4GW.Console.MessageType.Info, + ) + return True + + +def main(): + global _selected_weapon_index, _selected_upgrade_index + + if PyImGui.begin(MODULE_NAME, PyImGui.WindowFlags.AlwaysAutoResize): + weapons = _get_weapon_target_entries() + upgrades = _get_inventory_weapon_upgrade_items() + _clamp_selected_indexes(weapons, upgrades) + + if weapons: + weapon_labels = [f"{entry['label']} ({entry['item_id']})" for entry in weapons] + _selected_weapon_index = PyImGui.combo("Weapon", _selected_weapon_index, weapon_labels) + else: + PyImGui.text("No weapon found in inventory bags or equipped by party.") + + if upgrades: + upgrade_labels = [f"{name} ({item_id})" for item_id, name in upgrades] + _selected_upgrade_index = PyImGui.combo("Upgrade", _selected_upgrade_index, upgrade_labels) + else: + PyImGui.text("No weapon upgrade component found in inventory bags.") + + if PyImGui.button("Apply upgrade", width=180, height=26): + _apply_selected_weapon_upgrade() + + PyImGui.text_wrapped(_last_status) + + PyImGui.end() + + +if __name__ == "__main__": + main() diff --git a/stubs/PyInventory.pyi b/stubs/PyInventory.pyi index 01afbca90..40632f30b 100644 --- a/stubs/PyInventory.pyi +++ b/stubs/PyInventory.pyi @@ -33,6 +33,12 @@ class PyInventory: def UseItem(self, item_id: int) -> bool: ... def DestroyItem(self, item_id: int) -> bool: ... def IdentifyItem(self, id_kit_id: int, item_id: int) -> bool: ... + def GetInventoryIDFromAgent(self, agent_id: int) -> int: ... + def IsInventoryIDValid(self, inventory_id: int) -> bool: ... + def GetEquippedItemID(self, inventory_id: int, equip_slot: int) -> int: ... + def GetUpgradeSlot(self, upgrade_item_id: int) -> int: ... + def ValidateUpgrade(self, target_item_id: int, upgrade_item_id: int) -> bool: ... + def ApplyUpgrade(self, inventory_id: int, target_item_id: int, upgrade_item_id: int, upgrade_slot: int = 0) -> bool: ... def GetHoveredItemID(self) -> int: ... def GetGoldAmount(self) -> int: ... def GetGoldAmountInStorage(self) -> int: ... diff --git a/stubs/PyParty.pyi b/stubs/PyParty.pyi index 9c5042b7d..17f3d305f 100644 --- a/stubs/PyParty.pyi +++ b/stubs/PyParty.pyi @@ -150,6 +150,8 @@ class PyParty: def GetAllFlagX(self) -> float: ... def GetAllFlagY(self) -> float: ... def GetHeroAgentID(self, hero_index: int) -> int: ... + def GetInventorySelectedAgentID(self) -> int: ... + def GetInventoryEquipmentFrameID(self) -> int: ... def GetAgentHeroID(self, agent_id: int) -> int: ... def GetAgentIDByLoginNumber(self, login_number: int) -> int: ... def GetPlayerNameByLoginNumber(self, login_number: int) -> str: ...