diff --git a/.gitignore b/.gitignore index b477b6af5..0620e9a6e 100644 --- a/.gitignore +++ b/.gitignore @@ -218,3 +218,9 @@ cython_debug/ # macOS system files .DS_Store /Sources/frenkeyLib/Scripts/output + +# Add ignores +/Widgets/Automation/Bots/Farmers/Materials/Bones/DoA Gemstone Farm Heroes.json +/Widgets/Automation/Bots/Farmers/Titles/Lightbringer - MirrorOfLyss Heroes.json +/Bots/common_ac/loot_settings.ini +/Widgets/Automation/Bots/Missions/Dungeons/Tunnels of the Forsaken Heroes.json diff --git a/Py4GWCoreLib/enums_src/Multiboxing_enums.py b/Py4GWCoreLib/enums_src/Multiboxing_enums.py index f7631fb67..1592519e4 100644 --- a/Py4GWCoreLib/enums_src/Multiboxing_enums.py +++ b/Py4GWCoreLib/enums_src/Multiboxing_enums.py @@ -68,7 +68,7 @@ class SharedCommandType(IntEnum): BroadcastChatCommand = auto() #endregion - + MoveToXY = auto() class CombatPrepSkillsType(IntEnum): diff --git a/Widgets/Coding/Tools/Active Dialog Viewer.py b/Widgets/Coding/Tools/Active Dialog Viewer.py index a0f9bdd38..f74eb7c52 100644 --- a/Widgets/Coding/Tools/Active Dialog Viewer.py +++ b/Widgets/Coding/Tools/Active Dialog Viewer.py @@ -1,15 +1,68 @@ MODULE_NAME = "Active Dialog Viewer" MODULE_ICON = "Textures/Module_Icons/Script Runner.png" +import ctypes +import os import traceback +from enum import IntEnum import Py4GW import PyDialog import PyImGui -from Py4GWCoreLib import Player +from Py4GWCoreLib import Player, Agent, Map +from HeroAI.cache_data import CacheData +from Py4GWCoreLib import GLOBAL_CACHE, PyUIManager, UIManager, IconsFontAwesome5 +from Py4GWCoreLib import IniHandler +from Py4GWCoreLib import Color, ImGui +from Py4GWCoreLib import Routines +from Py4GWCoreLib import Timer +from Py4GWCoreLib.Overlay import Overlay +from Py4GWCoreLib.enums_src.Multiboxing_enums import SharedCommandType _dialog_was_active = False +# Multibox Dialog variables +script_directory = os.path.dirname(os.path.abspath(__file__)) +project_root = Py4GW.Console.get_projects_path() + +BASE_DIR = os.path.join(project_root, "Widgets/Config") +INI_WIDGET_WINDOW_PATH = os.path.join(BASE_DIR, "active_dialog_viewer_multibox.ini") +os.makedirs(BASE_DIR, exist_ok=True) + +# Window persistence setup +ini_window = IniHandler(INI_WIDGET_WINDOW_PATH) +save_window_timer = Timer() +save_window_timer.Start() + +# Multibox Dialog constants +default_dialog_string: str = "0x84" +dialog_open : bool = False +frame_coords : list[tuple[int, tuple[int, int, int, int]]] = [] +dialog_coords : tuple[int, int, int, int] = (0, 0, 0, 0) +overlay = Overlay() + +# Load user32.dll +user32 = ctypes.windll.user32 +VK_LBUTTON = 0x01 + +_left_was_pressed = False + +class WhichKey(IntEnum): + CTRL = 1 + SHIFT = 2 + +which_key: int = 2 +# Should use new MoveToXY or old Agent Id + Dialog id that can fail. +legacy_mode: bool = False +to_hex : str = "?" +dialog_int : int = 0 +gray_color = Color(150, 150, 150, 255) + +# Window state +window_x = ini_window.read_int(MODULE_NAME, "x", 100) +window_y = ini_window.read_int(MODULE_NAME, "y", 100) +window_collapsed = ini_window.read_bool(MODULE_NAME, "collapsed", False) + def configure(): pass @@ -20,6 +73,8 @@ def tooltip(): PyImGui.text(MODULE_NAME) PyImGui.separator() PyImGui.text_wrapped("Displays the current active Guild Wars dialog, the tracked context dialog, and the currently visible dialog buttons.") + PyImGui.separator() + PyImGui.text_wrapped("Multibox: Shift+Click on dialog buttons to send to all accounts.") PyImGui.end_tooltip() @@ -60,8 +115,13 @@ def _draw_buttons() -> None: PyImGui.text(f"dialog_id: 0x{button.dialog_id:X} ({button.dialog_id})") PyImGui.text(f"button_icon: {button.button_icon}") PyImGui.text(f"decode_pending: {button.message_decode_pending}") - if PyImGui.button(f"Send##dialog_button_{index}"): + if PyImGui.button(f"{IconsFontAwesome5.ICON_RSS} Send##dialog_button_{index}"): Player.SendAutomaticDialog(index) + PyImGui.same_line(0, -1) + if PyImGui.button(f"{IconsFontAwesome5.ICON_CROSSHAIRS} Send Dialog to All##dialog_button_{index}"): + active = PyDialog.PyDialog.get_active_dialog() + Player.ChangeTarget(active.agent_id) + _send_dialog_for_all(f"0x{button.dialog_id:X}", button.dialog_id, True) label = button.message_decoded or button.message if label: PyImGui.text_wrapped(label) @@ -69,6 +129,189 @@ def _draw_buttons() -> None: PyImGui.text("") +def _draw_multibox_controls() -> None: + """Draw multibox dialog controls""" + global default_dialog_string + + PyImGui.separator() + PyImGui.text("Multibox Controls") + PyImGui.separator() + + default_dialog_string = ImGui.input_text("Dialog Id", default_dialog_string, 0) + + if PyImGui.button(f"{IconsFontAwesome5.ICON_CROSSHAIRS} Send Dialog to All"): + _send_dialog() + + PyImGui.text_wrapped("Ctrl+Shift+Click on dialog buttons to send to all accounts") + + +def _draw_settings() -> None: + """Draw multibox dialog settings""" + global legacy_mode, which_key + + radio_legacy_mode = 0 if legacy_mode else 1 + + PyImGui.text(f"How to find the target on other boxes") + + radio_legacy_mode = ImGui.radio_button("Prefer Agent Id", radio_legacy_mode, 0) + radio_legacy_mode = ImGui.radio_button("Prefer Map + Location", radio_legacy_mode, 1) + + if radio_legacy_mode == 0: + legacy_mode = True + elif radio_legacy_mode == 1: + legacy_mode = False + + PyImGui.separator() + PyImGui.text(f"Which key?") + + radio_value = which_key + + radio_value = ImGui.radio_button(f"{WhichKey.CTRL.name}", radio_value, WhichKey.CTRL.value) + radio_value = ImGui.radio_button(f"{WhichKey.SHIFT.name}", radio_value, WhichKey.SHIFT.value) + + which_key = radio_value + + +# Multibox helper functions +def is_left_pressed() -> bool: + return bool(user32.GetAsyncKeyState(VK_LBUTTON) & 0x8000) + + +def is_left_mouse_clicked() -> bool: + """ + Returns True exactly once per full click (press → release). + False at all other times. + """ + global _left_was_pressed + + # Is button physically down now? + pressed = bool(user32.GetAsyncKeyState(VK_LBUTTON) & 0x8000) + + # Detect release event (was pressed, now not pressed) + clicked = _left_was_pressed and not pressed + + # Update state for next call + _left_was_pressed = pressed + + return clicked + + +def _draw_dialog_overlay(): + """Draw overlay for multibox dialog interaction""" + global frame_coords, dialog_open, dialog_coords, gray_color, to_hex, dialog_int, which_key + + account_email = Player.GetAccountEmail() + own_data = GLOBAL_CACHE.ShMem.GetAccountDataFromEmail(account_email) + if own_data is None: + return + + dialog_open = UIManager.IsNPCDialogVisible() + frame_coords = UIManager.GetDialogButtonFrames() if dialog_open else [] + + if not frame_coords or not dialog_open: + return + + pyimgui_io = PyImGui.get_io() + mouse_pos = (pyimgui_io.mouse_pos_x, pyimgui_io.mouse_pos_y) + + sorted_frames = sorted(frame_coords, key=lambda x: (x[1][1], x[1][0])) # Sort by Y, then X + + for i, (frame_id, frame) in enumerate(sorted_frames): + if ImGui.is_mouse_in_rect((frame[0], frame[1], frame[2] - frame[0], frame[3] - frame[1]), mouse_pos): + frame_obj = PyUIManager.UIFrame(frame_id) + if frame_obj is not None: + dialog_int = frame_obj.field105_0x1c4 + to_hex = f"0x{dialog_int:X}" + _set_dialog_id(to_hex) + + ctrl, str_ctrl = _get_modifier_state(pyimgui_io, which_key) + if is_left_mouse_clicked(): + if ctrl: + + # hero ai does this... + # accounts = [acc for acc in GLOBAL_CACHE.ShMem.GetAllAccountData() if acc.AccountEmail != account_email] + # print(f"sending dialog {i + 1}") + # commands.send_dialog(accounts, i + 1) + + # this is the field in toolbox: + # PyUIManager.UIFrame(frame_id).field105_0x1c4 + _send_dialog_for_all(to_hex, dialog_int, include_sender = False) + + return + else: + #todo if debug + print(f"clicked without {str_ctrl}") + else: + + if ctrl: + # to show that you have the right key sleted + ImGui.begin_tooltip() + ImGui.text_colored(f"({str_ctrl}) + Click to send dialog {to_hex} ({dialog_int}) on all accounts.", gray_color.color_tuple, 12) + ImGui.end_tooltip() + else: + ImGui.begin_tooltip() + ImGui.text_colored(f"{str_ctrl} + Click to send dialog {to_hex} ({dialog_int}) on all accounts.", gray_color.color_tuple, 12) + ImGui.end_tooltip() + + +def _get_modifier_state(pyimgui_io, which_key): + ctrl = False + str_ctrl = "Shift" + if WhichKey.CTRL.value == which_key: + ctrl = pyimgui_io.key_ctrl + str_ctrl = "Ctrl" + else: + ctrl = pyimgui_io.key_shift + return ctrl, str_ctrl + + +def _set_dialog_id(dialog_string: str): + """Set the dialog ID for multibox operations""" + global default_dialog_string + default_dialog_string = dialog_string + + +def _send_dialog(): + """Send dialog to all accounts""" + global default_dialog_string + try: + dialog_id: int = int(default_dialog_string, 0) + _send_dialog_for_all(default_dialog_string, dialog_id) + except Exception as e: + print(f"Sending {default_dialog_string} failed: {e}") + default_dialog_string = "0x84" + + +def _send_dialog_for_all(dialog_string: str, dialog_id: int, include_sender: bool = True): + """Send dialog command to all accounts""" + global legacy_mode + print(f"Starting sending {dialog_string} as {dialog_id}") + target = Player.GetTargetID() + if target == 0: + print("No target to interact with.") + else: + sender_email = Player.GetAccountEmail() + accounts = GLOBAL_CACHE.ShMem.GetAllAccountData() + for account in accounts: + if not include_sender and sender_email == account.AccountEmail: + continue + + agent_x , agent_y = Agent.GetXY(target) + print(f"Ordering {account.AccountEmail} to send dialog {dialog_id} ({dialog_string}) to target: {target} @ ({agent_x},{agent_y})") + + if legacy_mode: + GLOBAL_CACHE.ShMem.SendMessage( + sender_email, account.AccountEmail, SharedCommandType.SendDialogToTarget, + (target, dialog_id, 0, 0) + ) + else: + map_id: int = Map.GetMapID() + GLOBAL_CACHE.ShMem.SendMessage( + sender_email, account.AccountEmail, SharedCommandType.MoveToXY, + (map_id, agent_x, agent_y, dialog_id) + ) + + def main(): global _dialog_was_active try: @@ -78,9 +321,23 @@ def main(): _dialog_was_active = dialog_active if PyImGui.begin(MODULE_NAME, PyImGui.WindowFlags.AlwaysAutoResize): - _draw_active_dialog() - _draw_buttons() + if PyImGui.begin_tab_bar("top_level_tabs"): + if ImGui.begin_tab_item("Default"): + _draw_active_dialog() + _draw_buttons() + ImGui.end_tab_item() + if ImGui.begin_tab_item("Actions"): + _draw_multibox_controls() + ImGui.end_tab_item() + if ImGui.begin_tab_item("Settings"): + _draw_settings() + ImGui.end_tab_item() + PyImGui.end_tab_bar() PyImGui.end() + + # Draw multibox overlay + _draw_dialog_overlay() + except Exception as e: Py4GW.Console.Log(MODULE_NAME, f"Error: {e}", Py4GW.Console.MessageType.Error) Py4GW.Console.Log(MODULE_NAME, traceback.format_exc(), Py4GW.Console.MessageType.Error) diff --git a/Widgets/System/Messaging.py b/Widgets/System/Messaging.py index d21996cee..dada8f584 100644 --- a/Widgets/System/Messaging.py +++ b/Widgets/System/Messaging.py @@ -410,6 +410,69 @@ def LeaveParty(index: int, message: SharedMessageStruct): ConsoleLog(MODULE_NAME, "LeaveParty message processed and finished.", Console.MessageType.Info, False) +# endregion + +# region MoveToXY + + +def MoveToXY(index: int, message: SharedMessageStruct): + # ConsoleLog(MODULE_NAME, f"Processing TravelToMap message: {message}", Console.MessageType.Info) + GLOBAL_CACHE.ShMem.MarkMessageAsRunning(message.ReceiverEmail, index) + sender_data = GLOBAL_CACHE.ShMem.GetAccountDataFromEmail(message.SenderEmail) + if sender_data is None: + GLOBAL_CACHE.ShMem.MarkMessageAsFinished(message.ReceiverEmail, index) + return + + map_id = int(message.Params[0]) + map_x = int(message.Params[1]) + map_y = int(message.Params[2]) + dialog_id = int(message.Params[3]) + + if Map.GetMapID() == map_id: + yield from MoveToXY_correct_map(dialog_id, map_x, map_y) + else: + ConsoleLog(MODULE_NAME, f"Wrong map id.", Console.MessageType.Info, False) + + GLOBAL_CACHE.ShMem.MarkMessageAsFinished(message.ReceiverEmail, index) + ConsoleLog(MODULE_NAME, "MoveToXY message processed", Console.MessageType.Info, False) + + +def MoveToXY_correct_map(dialog_id, map_x, map_y): + + ConsoleLog(MODULE_NAME, f"MoveToXY at ({map_x}, {map_y}).", Console.MessageType.Info, False) + + range_adjacent_value = Range.Adjacent.value + if Utils.Distance((map_x, map_y), Player.GetXY()) > range_adjacent_value: + path3d = yield from AutoPathing().get_path_to(map_x, map_y, smooth_by_los=True, margin=100.0, step_dist=242.0) + path2d: list[tuple[float, float]] = [(x, y) for (x, y, *_) in path3d] + + yield from Routines.Yield.Movement.FollowPath( + path_points=path2d, + custom_exit_condition=lambda: Agent.IsDead(Player.GetAgentID()), + tolerance=range_adjacent_value, + log=False, + timeout=15_000, + progress_callback=lambda progress: ConsoleLog("MoveToXY", f"FollowPath: progress: {progress}", + Console.MessageType.Info), + custom_pause_fn=lambda: False) + + if dialog_id > 0: + yield from MoveToXY_send_dialog_id(dialog_id, map_x, map_y) + + +def MoveToXY_send_dialog_id(dialog_id, map_x, map_y): + result = yield from Routines.Yield.Agents.InteractWithAgentXY(x=map_x, y=map_y) + yield from Routines.Yield.wait(500) + # ConsoleLog(MODULE_NAME, f"Interaction result: {result}", Py4GW.Console.MessageType.Info) + if result: + ConsoleLog(MODULE_NAME, f"Dialog {dialog_id} at ({map_x}, {map_y}).", Console.MessageType.Info, False) + Player.SendDialog(dialog_id) + yield from Routines.Yield.wait(500) + else: + ConsoleLog(MODULE_NAME, f"Dialog at ({map_x}, {map_y}) failed, could not find an npc there.", + Console.MessageType.Info, False) + + # endregion # region TravelToMap @@ -2576,6 +2639,8 @@ def ProcessMessages(): case SharedCommandType.ReservedLegacyCommand: GLOBAL_CACHE.ShMem.MarkMessageAsFinished(account_email, index) pass + case SharedCommandType.MoveToXY: + GLOBAL_CACHE.Coroutines.append(MoveToXY(index, message)) case _: GLOBAL_CACHE.ShMem.MarkMessageAsFinished(account_email, index) pass