From d7814145b675ec8e77022f86c173132492540e54 Mon Sep 17 00:00:00 2001 From: Alice Date: Fri, 1 May 2026 15:00:39 -0400 Subject: [PATCH 1/6] Add Dialog Sender via Puller from frame object --- .../Helpers/Dialogs/Multibox Dialog.py | 278 ++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 Widgets/Automation/Helpers/Dialogs/Multibox Dialog.py diff --git a/Widgets/Automation/Helpers/Dialogs/Multibox Dialog.py b/Widgets/Automation/Helpers/Dialogs/Multibox Dialog.py new file mode 100644 index 000000000..cd229bb9d --- /dev/null +++ b/Widgets/Automation/Helpers/Dialogs/Multibox Dialog.py @@ -0,0 +1,278 @@ +import ctypes +import os +import traceback +from enum import IntEnum + +import Py4GW # type: ignore +from HeroAI.cache_data import CacheData +from Py4GWCoreLib import GLOBAL_CACHE, PyUIManager, UIManager, IconsFontAwesome5 +from Py4GWCoreLib import IniHandler +from Py4GWCoreLib import PyImGui, Color, ImGui +from Py4GWCoreLib import Routines +from Py4GWCoreLib import Timer, Player +from Py4GWCoreLib.Overlay import Overlay +from Py4GWCoreLib.enums_src.Multiboxing_enums import SharedCommandType + +script_directory = os.path.dirname(os.path.abspath(__file__)) +project_root = Py4GW.Console.get_projects_path() + +first_run = True + +BASE_DIR = os.path.join(project_root, "Widgets/Config") +INI_WIDGET_WINDOW_PATH = os.path.join(BASE_DIR, "nightfall_dialog_sender.ini") +os.makedirs(BASE_DIR, exist_ok=True) + +cached_data = CacheData() + +# ——— Window Persistence Setup ——— +ini_window = IniHandler(INI_WIDGET_WINDOW_PATH) +save_window_timer = Timer() +save_window_timer.Start() + +# String consts +MODULE_NAME = "Dialog Sender (Multibox)" # Change this Module name +MODULE_ICON = "Textures\\Module_Icons\\Dialogs - Nightfall.png" +COLLAPSED = "collapsed" +X_POS = "x" +Y_POS = "y" + +# load last‐saved window state (fallback to 100,100 / un-collapsed) +window_x = ini_window.read_int(MODULE_NAME, X_POS, 100) +window_y = ini_window.read_int(MODULE_NAME, Y_POS, 100) +window_collapsed = ini_window.read_bool(MODULE_NAME, COLLAPSED, False) + +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 + +# Virtual-key code for left mouse button +VK_LBUTTON = 0x01 + +def is_left_pressed() -> bool: + return bool(user32.GetAsyncKeyState(VK_LBUTTON) & 0x8000) + +_left_was_pressed = False + +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 + + +class WhichKey(IntEnum): + CTRL = 1 + SHIFT = 2 + + +which_key: int = 2 +to_hex : str = "?" +dialog_int : int = 0 +gray_color = Color(150, 150, 150, 255) + + +def draw_dialog_overlay(): + 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: + # print("no cache data") + return + + dialog_open = UIManager.IsNPCDialogVisible() + frame_coords = UIManager.GetDialogButtonFrames() if dialog_open else [] + + if not frame_coords or not dialog_open: + # print("no dialog data") + 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 + + # print("dialog open") + 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() + + else: + # print("no dialog") + pass + + pass + + +# todo expose setting to not clash with hero ai and gw1 call target ideal of using ctrl or remove feature from hero ai to be more modular +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 + + +# Allow the user to override the dialog id manually if they so choose as well as display current dialog id and button to send +def draw_widget(): + global window_x, window_y, window_collapsed, first_run + global default_dialog_string + + if not PyImGui.begin(MODULE_NAME, PyImGui.WindowFlags.AlwaysAutoResize): + PyImGui.end() + return + + default_dialog_string = ImGui.input_text("Dialog Id", default_dialog_string, 0) + + if PyImGui.button(f"{IconsFontAwesome5.ICON_CROSSHAIRS} Send Dialog"): + send_dialog() + + PyImGui.end() + + +# Module tooltip, not the dialog tooltip +def tooltip(): + PyImGui.begin_tooltip() + + # Title + title_color = Color(255, 200, 100, 255) + ImGui.push_font("Regular", 20) + PyImGui.text_colored(MODULE_NAME, title_color.to_tuple_normalized()) + ImGui.pop_font() + PyImGui.spacing() + PyImGui.separator() + + # Description + PyImGui.text("An automation utility for sending dialogs when multiboxing based on the open dialogs.") + PyImGui.spacing() + + PyImGui.end_tooltip() + + +# @staticmethod +def set_dialog_id(dialog_string: str): + global default_dialog_string + default_dialog_string = dialog_string + + +def send_dialog(): + 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"Well sending {default_dialog_string} failed {e}") + default_dialog_string = "0x84" + + +# @staticmethod +def send_dialog_for_all(dialog_string: str, dialog_id: int, include_sender: bool = True): + 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 + + # TODO distance check first to avoid the wobble of death + + print(f"Ordering {account.AccountEmail} to send dialog {dialog_id} ({dialog_string}) to target: {target}") + GLOBAL_CACHE.ShMem.SendMessage( + sender_email, account.AccountEmail, SharedCommandType.SendDialogToTarget, + (target, dialog_id, 0, 0) + ) + + +def main(): + global cached_data + try: + if not Routines.Checks.Map.MapValid(): + return + + draw_widget() + + draw_dialog_overlay() + + except ImportError as e: + Py4GW.Console.Log(MODULE_NAME, f"ImportError encountered: {str(e)}", Py4GW.Console.MessageType.Error) + Py4GW.Console.Log(MODULE_NAME, f"Stack trace: {traceback.format_exc()}", Py4GW.Console.MessageType.Error) + except ValueError as e: + Py4GW.Console.Log(MODULE_NAME, f"ValueError encountered: {str(e)}", Py4GW.Console.MessageType.Error) + Py4GW.Console.Log(MODULE_NAME, f"Stack trace: {traceback.format_exc()}", Py4GW.Console.MessageType.Error) + except TypeError as e: + Py4GW.Console.Log(MODULE_NAME, f"TypeError encountered: {str(e)}", Py4GW.Console.MessageType.Error) + Py4GW.Console.Log(MODULE_NAME, f"Stack trace: {traceback.format_exc()}", Py4GW.Console.MessageType.Error) + except Exception as e: + # Catch-all for any other unexpected exceptions + Py4GW.Console.Log(MODULE_NAME, f"Unexpected error encountered: {str(e)}", Py4GW.Console.MessageType.Error) + Py4GW.Console.Log(MODULE_NAME, f"Stack trace: {traceback.format_exc()}", Py4GW.Console.MessageType.Error) + finally: + pass + + +if __name__ == "__main__": + main() From 6cee8a03714a416a4a5e9da4f30ea6a9b5e577fc Mon Sep 17 00:00:00 2001 From: Alice Date: Sun, 3 May 2026 13:00:19 -0400 Subject: [PATCH 2/6] Add Ignores for config files --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) 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 From b4a26b50474e7dce703bf011c4379c3411098bc9 Mon Sep 17 00:00:00 2001 From: Alice Date: Sun, 3 May 2026 12:57:43 -0400 Subject: [PATCH 3/6] merge --- .../Helpers/Dialogs/Multibox Dialog.py | 278 ------------------ Widgets/Coding/Tools/Active Dialog Viewer.py | 221 +++++++++++++- 2 files changed, 218 insertions(+), 281 deletions(-) delete mode 100644 Widgets/Automation/Helpers/Dialogs/Multibox Dialog.py diff --git a/Widgets/Automation/Helpers/Dialogs/Multibox Dialog.py b/Widgets/Automation/Helpers/Dialogs/Multibox Dialog.py deleted file mode 100644 index cd229bb9d..000000000 --- a/Widgets/Automation/Helpers/Dialogs/Multibox Dialog.py +++ /dev/null @@ -1,278 +0,0 @@ -import ctypes -import os -import traceback -from enum import IntEnum - -import Py4GW # type: ignore -from HeroAI.cache_data import CacheData -from Py4GWCoreLib import GLOBAL_CACHE, PyUIManager, UIManager, IconsFontAwesome5 -from Py4GWCoreLib import IniHandler -from Py4GWCoreLib import PyImGui, Color, ImGui -from Py4GWCoreLib import Routines -from Py4GWCoreLib import Timer, Player -from Py4GWCoreLib.Overlay import Overlay -from Py4GWCoreLib.enums_src.Multiboxing_enums import SharedCommandType - -script_directory = os.path.dirname(os.path.abspath(__file__)) -project_root = Py4GW.Console.get_projects_path() - -first_run = True - -BASE_DIR = os.path.join(project_root, "Widgets/Config") -INI_WIDGET_WINDOW_PATH = os.path.join(BASE_DIR, "nightfall_dialog_sender.ini") -os.makedirs(BASE_DIR, exist_ok=True) - -cached_data = CacheData() - -# ——— Window Persistence Setup ——— -ini_window = IniHandler(INI_WIDGET_WINDOW_PATH) -save_window_timer = Timer() -save_window_timer.Start() - -# String consts -MODULE_NAME = "Dialog Sender (Multibox)" # Change this Module name -MODULE_ICON = "Textures\\Module_Icons\\Dialogs - Nightfall.png" -COLLAPSED = "collapsed" -X_POS = "x" -Y_POS = "y" - -# load last‐saved window state (fallback to 100,100 / un-collapsed) -window_x = ini_window.read_int(MODULE_NAME, X_POS, 100) -window_y = ini_window.read_int(MODULE_NAME, Y_POS, 100) -window_collapsed = ini_window.read_bool(MODULE_NAME, COLLAPSED, False) - -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 - -# Virtual-key code for left mouse button -VK_LBUTTON = 0x01 - -def is_left_pressed() -> bool: - return bool(user32.GetAsyncKeyState(VK_LBUTTON) & 0x8000) - -_left_was_pressed = False - -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 - - -class WhichKey(IntEnum): - CTRL = 1 - SHIFT = 2 - - -which_key: int = 2 -to_hex : str = "?" -dialog_int : int = 0 -gray_color = Color(150, 150, 150, 255) - - -def draw_dialog_overlay(): - 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: - # print("no cache data") - return - - dialog_open = UIManager.IsNPCDialogVisible() - frame_coords = UIManager.GetDialogButtonFrames() if dialog_open else [] - - if not frame_coords or not dialog_open: - # print("no dialog data") - 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 - - # print("dialog open") - 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() - - else: - # print("no dialog") - pass - - pass - - -# todo expose setting to not clash with hero ai and gw1 call target ideal of using ctrl or remove feature from hero ai to be more modular -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 - - -# Allow the user to override the dialog id manually if they so choose as well as display current dialog id and button to send -def draw_widget(): - global window_x, window_y, window_collapsed, first_run - global default_dialog_string - - if not PyImGui.begin(MODULE_NAME, PyImGui.WindowFlags.AlwaysAutoResize): - PyImGui.end() - return - - default_dialog_string = ImGui.input_text("Dialog Id", default_dialog_string, 0) - - if PyImGui.button(f"{IconsFontAwesome5.ICON_CROSSHAIRS} Send Dialog"): - send_dialog() - - PyImGui.end() - - -# Module tooltip, not the dialog tooltip -def tooltip(): - PyImGui.begin_tooltip() - - # Title - title_color = Color(255, 200, 100, 255) - ImGui.push_font("Regular", 20) - PyImGui.text_colored(MODULE_NAME, title_color.to_tuple_normalized()) - ImGui.pop_font() - PyImGui.spacing() - PyImGui.separator() - - # Description - PyImGui.text("An automation utility for sending dialogs when multiboxing based on the open dialogs.") - PyImGui.spacing() - - PyImGui.end_tooltip() - - -# @staticmethod -def set_dialog_id(dialog_string: str): - global default_dialog_string - default_dialog_string = dialog_string - - -def send_dialog(): - 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"Well sending {default_dialog_string} failed {e}") - default_dialog_string = "0x84" - - -# @staticmethod -def send_dialog_for_all(dialog_string: str, dialog_id: int, include_sender: bool = True): - 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 - - # TODO distance check first to avoid the wobble of death - - print(f"Ordering {account.AccountEmail} to send dialog {dialog_id} ({dialog_string}) to target: {target}") - GLOBAL_CACHE.ShMem.SendMessage( - sender_email, account.AccountEmail, SharedCommandType.SendDialogToTarget, - (target, dialog_id, 0, 0) - ) - - -def main(): - global cached_data - try: - if not Routines.Checks.Map.MapValid(): - return - - draw_widget() - - draw_dialog_overlay() - - except ImportError as e: - Py4GW.Console.Log(MODULE_NAME, f"ImportError encountered: {str(e)}", Py4GW.Console.MessageType.Error) - Py4GW.Console.Log(MODULE_NAME, f"Stack trace: {traceback.format_exc()}", Py4GW.Console.MessageType.Error) - except ValueError as e: - Py4GW.Console.Log(MODULE_NAME, f"ValueError encountered: {str(e)}", Py4GW.Console.MessageType.Error) - Py4GW.Console.Log(MODULE_NAME, f"Stack trace: {traceback.format_exc()}", Py4GW.Console.MessageType.Error) - except TypeError as e: - Py4GW.Console.Log(MODULE_NAME, f"TypeError encountered: {str(e)}", Py4GW.Console.MessageType.Error) - Py4GW.Console.Log(MODULE_NAME, f"Stack trace: {traceback.format_exc()}", Py4GW.Console.MessageType.Error) - except Exception as e: - # Catch-all for any other unexpected exceptions - Py4GW.Console.Log(MODULE_NAME, f"Unexpected error encountered: {str(e)}", Py4GW.Console.MessageType.Error) - Py4GW.Console.Log(MODULE_NAME, f"Stack trace: {traceback.format_exc()}", Py4GW.Console.MessageType.Error) - finally: - pass - - -if __name__ == "__main__": - main() diff --git a/Widgets/Coding/Tools/Active Dialog Viewer.py b/Widgets/Coding/Tools/Active Dialog Viewer.py index a0f9bdd38..f8ab69922 100644 --- a/Widgets/Coding/Tools/Active Dialog Viewer.py +++ b/Widgets/Coding/Tools/Active Dialog Viewer.py @@ -1,15 +1,66 @@ 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 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 +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 +71,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 +113,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 +127,152 @@ 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") + + +# 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""" + 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 + + print(f"Ordering {account.AccountEmail} to send dialog {dialog_id} ({dialog_string}) to target: {target}") + GLOBAL_CACHE.ShMem.SendMessage( + sender_email, account.AccountEmail, SharedCommandType.SendDialogToTarget, + (target, dialog_id, 0, 0) + ) + + def main(): global _dialog_was_active try: @@ -78,9 +282,20 @@ 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() + 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) From ee5a91fcb15d641d4282e7a1faff81eb3d3de61f Mon Sep 17 00:00:00 2001 From: Alice Date: Fri, 1 May 2026 21:51:45 -0400 Subject: [PATCH 4/6] add movetoxy (cherry picked from commit 4c119de49be2c8d1be5b16ab9bd7dafd3a7abc86) --- Py4GWCoreLib/enums_src/Multiboxing_enums.py | 2 +- Widgets/System/Messaging.py | 55 +++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) 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/System/Messaging.py b/Widgets/System/Messaging.py index d21996cee..003772741 100644 --- a/Widgets/System/Messaging.py +++ b/Widgets/System/Messaging.py @@ -410,6 +410,59 @@ 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: + if Utils.Distance((map_x, map_y), Player.GetXY()) > 150: + + 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=150, + log=False, + timeout=15_000, + progress_callback=lambda progress: ConsoleLog("MoveToXY", f"FollowPath: progress: {progress}", Console.MessageType.Info), + custom_pause_fn=lambda: False) + + + ConsoleLog(MODULE_NAME, f"MoveToXY at ({map_x}, {map_y}).", Console.MessageType.Info, False) + + if dialog_id > 0: + 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) + 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) + + # endregion # region TravelToMap @@ -2576,6 +2629,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 From 3770f6bd45d8f8d125435173f9fd0cb99becd077 Mon Sep 17 00:00:00 2001 From: Alice Date: Sun, 3 May 2026 13:42:23 -0400 Subject: [PATCH 5/6] settings --- Widgets/Coding/Tools/Active Dialog Viewer.py | 55 +++++++++++++++-- Widgets/System/Messaging.py | 62 ++++++++++++-------- 2 files changed, 85 insertions(+), 32 deletions(-) diff --git a/Widgets/Coding/Tools/Active Dialog Viewer.py b/Widgets/Coding/Tools/Active Dialog Viewer.py index f8ab69922..faa3db21c 100644 --- a/Widgets/Coding/Tools/Active Dialog Viewer.py +++ b/Widgets/Coding/Tools/Active Dialog Viewer.py @@ -9,7 +9,7 @@ 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 @@ -52,6 +52,8 @@ class WhichKey(IntEnum): 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) @@ -143,6 +145,34 @@ def _draw_multibox_controls() -> None: 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}", which_key, WhichKey.CTRL.value) + radio_value = ImGui.radio_button(f"{WhichKey.SHIFT.name}", which_key, WhichKey.SHIFT.value) + + if radio_value: + which_key = radio_value + + # Multibox helper functions def is_left_pressed() -> bool: return bool(user32.GetAsyncKeyState(VK_LBUTTON) & 0x8000) @@ -255,6 +285,7 @@ def _send_dialog(): 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: @@ -266,11 +297,20 @@ def _send_dialog_for_all(dialog_string: str, dialog_id: int, include_sender: boo if not include_sender and sender_email == account.AccountEmail: continue - print(f"Ordering {account.AccountEmail} to send dialog {dialog_id} ({dialog_string}) to target: {target}") - GLOBAL_CACHE.ShMem.SendMessage( - sender_email, account.AccountEmail, SharedCommandType.SendDialogToTarget, - (target, dialog_id, 0, 0) - ) + 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(): @@ -290,6 +330,9 @@ def main(): 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() diff --git a/Widgets/System/Messaging.py b/Widgets/System/Messaging.py index 003772741..dada8f584 100644 --- a/Widgets/System/Messaging.py +++ b/Widgets/System/Messaging.py @@ -429,38 +429,48 @@ def MoveToXY(index: int, message: SharedMessageStruct): dialog_id = int(message.Params[3]) if Map.GetMapID() == map_id: - if Utils.Distance((map_x, map_y), Player.GetXY()) > 150: + yield from MoveToXY_correct_map(dialog_id, map_x, map_y) + else: + ConsoleLog(MODULE_NAME, f"Wrong map id.", Console.MessageType.Info, False) - 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] + GLOBAL_CACHE.ShMem.MarkMessageAsFinished(message.ReceiverEmail, index) + ConsoleLog(MODULE_NAME, "MoveToXY message processed", Console.MessageType.Info, False) - yield from Routines.Yield.Movement.FollowPath( - path_points= path2d, - custom_exit_condition=lambda: Agent.IsDead(Player.GetAgentID()), - tolerance=150, - log=False, - timeout=15_000, - progress_callback=lambda progress: ConsoleLog("MoveToXY", f"FollowPath: progress: {progress}", Console.MessageType.Info), - custom_pause_fn=lambda: 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) + ConsoleLog(MODULE_NAME, f"MoveToXY at ({map_x}, {map_y}).", Console.MessageType.Info, False) - if dialog_id > 0: - 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) - else: - ConsoleLog(MODULE_NAME, f"Wrong map id.", 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] - GLOBAL_CACHE.ShMem.MarkMessageAsFinished(message.ReceiverEmail, index) - ConsoleLog(MODULE_NAME, "MoveToXY message processed", Console.MessageType.Info, False) + 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 From 250d8ad202b03fee8b93364f5f5d3cf5b2df14c4 Mon Sep 17 00:00:00 2001 From: Alice Date: Sun, 3 May 2026 14:03:43 -0400 Subject: [PATCH 6/6] radio button silliness --- Widgets/Coding/Tools/Active Dialog Viewer.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Widgets/Coding/Tools/Active Dialog Viewer.py b/Widgets/Coding/Tools/Active Dialog Viewer.py index faa3db21c..f74eb7c52 100644 --- a/Widgets/Coding/Tools/Active Dialog Viewer.py +++ b/Widgets/Coding/Tools/Active Dialog Viewer.py @@ -166,11 +166,10 @@ def _draw_settings() -> None: radio_value = which_key - radio_value = ImGui.radio_button(f"{WhichKey.CTRL.name}", which_key, WhichKey.CTRL.value) - radio_value = ImGui.radio_button(f"{WhichKey.SHIFT.name}", which_key, WhichKey.SHIFT.value) + 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) - if radio_value: - which_key = radio_value + which_key = radio_value # Multibox helper functions