From 4ef859e26f82ff163f07f615cfe7acbb5c2ed14e Mon Sep 17 00:00:00 2001 From: server Date: Thu, 16 Apr 2026 18:33:07 +0200 Subject: [PATCH 1/5] issue-5: add teleport hud --- assets/root/interfacemodule.py | 14 +++ assets/root/uiTeleport.py | 173 +++++++++++++++++++++++++++++++++ assets/root/uiminimap.py | 30 ++++++ 3 files changed, 217 insertions(+) create mode 100644 assets/root/uiTeleport.py diff --git a/assets/root/interfacemodule.py b/assets/root/interfacemodule.py index a2752c77..02976c0a 100644 --- a/assets/root/interfacemodule.py +++ b/assets/root/interfacemodule.py @@ -26,6 +26,7 @@ import uiRestart import uiToolTip import uiMiniMap import uiBiolog +import uiTeleport import uiParty import uiSafebox import uiGuild @@ -77,6 +78,7 @@ class Interface(object): self.wndMessenger = None self.wndMiniMap = None self.wndBiolog = None + self.wndTeleport = None self.wndGuild = None self.wndGuildBuilding = None @@ -183,6 +185,7 @@ class Interface(object): wndMiniMap = uiMiniMap.MiniMap() wndBiolog = uiBiolog.BiologWindow() + wndTeleport = uiTeleport.TeleportWindow() wndSafebox = uiSafebox.SafeboxWindow() # ITEM_MALL @@ -199,9 +202,11 @@ class Interface(object): self.wndDragonSoulRefine = wndDragonSoulRefine self.wndMiniMap = wndMiniMap self.wndBiolog = wndBiolog + self.wndTeleport = wndTeleport self.wndSafebox = wndSafebox self.wndChatLog = wndChatLog self.wndMiniMap.SetBiologButtonEvent(ui.__mem_func__(self.ToggleBiologWindow)) + self.wndMiniMap.SetTeleportButtonEvent(ui.__mem_func__(self.ToggleTeleportWindow)) if app.ENABLE_DRAGON_SOUL_SYSTEM: self.wndDragonSoul.SetDragonSoulRefineWindow(self.wndDragonSoulRefine) @@ -963,6 +968,15 @@ class Interface(object): else: self.wndBiolog.Hide() + def ToggleTeleportWindow(self): + if False == self.wndTeleport.IsShow(): + (miniMapX, miniMapY) = self.wndMiniMap.GetGlobalPosition() + self.wndTeleport.SetPosition(max(10, miniMapX - self.wndTeleport.GetWidth() - 10), miniMapY + 24) + self.wndTeleport.Show() + self.wndTeleport.SetTop() + else: + self.wndTeleport.Hide() + def ToggleCharacterWindow(self, state): if False == player.IsObserverMode(): if False == self.wndCharacter.IsShow(): diff --git a/assets/root/uiTeleport.py b/assets/root/uiTeleport.py new file mode 100644 index 00000000..d3caebed --- /dev/null +++ b/assets/root/uiTeleport.py @@ -0,0 +1,173 @@ +import time + +import background +import net +import player +import quest +import ui + + +class TeleportWindow(ui.BoardWithTitleBar): + TITLE = "Teleport System" + PRESET_BUTTONS = ( + (1, "Village 1"), + (2, "Village 2"), + (3, "Valley"), + (4, "Desert"), + (5, "Sohan"), + (6, "Fireland"), + (7, "Devil"), + (8, "Cave"), + ) + + def __init__(self): + ui.BoardWithTitleBar.__init__(self) + + self.lastRefreshTime = 0.0 + self.slotRows = [] + + self.AddFlag("float") + self.AddFlag("movable") + self.SetSize(310, 255) + self.SetTitleName("Teleport") + self.SetCloseEvent(self.Hide) + + self.__CreateChildren() + self.Hide() + + def __del__(self): + ui.BoardWithTitleBar.__del__(self) + + def __CreateChildren(self): + self.statusLine = self.__CreateValueLine(15, 36) + self.mapLine = self.__CreateValueLine(15, 56) + self.coordLine = self.__CreateValueLine(15, 76) + self.slotSummaryLine = self.__CreateValueLine(15, 96) + self.cooldownLine = self.__CreateValueLine(15, 116) + + for index, presetData in enumerate(self.PRESET_BUTTONS): + (presetId, label) = presetData + row = index // 4 + column = index % 4 + + button = self.__CreateButton(15 + column * 71, 142 + row * 24, 64, label) + button.SetEvent(self.__SendPresetWarp, presetId) + + for slot in range(1, 6): + label = self.__CreateValueLine(15, 196 + (slot - 1) * 11) + saveButton = self.__CreateButton(100, 192 + (slot - 1) * 11, 42, "Save") + saveButton.SetEvent(self.__SendSaveSlot, slot) + useButton = self.__CreateButton(148, 192 + (slot - 1) * 11, 42, "Use") + useButton.SetEvent(self.__SendUseSlot, slot) + self.slotRows.append((label, saveButton, useButton)) + + def __CreateValueLine(self, x, y): + textLine = ui.TextLine() + textLine.SetParent(self) + textLine.SetPosition(x, y) + textLine.SetOutline() + textLine.Show() + return textLine + + def __CreateButton(self, x, y, width, text): + button = ui.Button() + button.SetParent(self) + button.SetPosition(x, y) + button.SetUpVisual("d:/ymir work/ui/public/small_thin_button_01.sub") + button.SetOverVisual("d:/ymir work/ui/public/small_thin_button_02.sub") + button.SetDownVisual("d:/ymir work/ui/public/small_thin_button_03.sub") + button.SetDisableVisual("d:/ymir work/ui/public/small_thin_button_01.sub") + button.SetSize(width, 17) + button.SetText(text) + button.Show() + return button + + def __GetTeleportQuestData(self): + questCount = min(quest.GetQuestCount(), quest.QUEST_MAX_NUM) + for questIndex in range(questCount): + (questName, questIcon, questCounterName, questCounterValue) = quest.GetQuestData(questIndex) + if questName != self.TITLE: + continue + + maxSlots = 3 + if "/" in questCounterName: + parts = questCounterName.split("/") + try: + maxSlots = int(parts[-1].strip()) + except: + maxSlots = 3 + + (clockName, clockValue) = quest.GetQuestLastTime(questIndex) + return { + "savedCount" : max(questCounterValue, 0), + "maxSlots" : max(maxSlots, 3), + "clockName" : clockName, + "clockValue" : max(clockValue, 0), + } + + return None + + def __FormatCooldown(self, seconds): + return "%02d:%02d" % (seconds // 60, seconds % 60) + + def __RefreshSlotRows(self, maxSlots): + for slot, row in enumerate(self.slotRows, 1): + (label, saveButton, useButton) = row + if slot <= maxSlots: + label.SetText("Slot %d" % slot) + saveButton.Enable() + useButton.Enable() + else: + label.SetText("Slot %d (VIP)" % slot) + saveButton.Disable() + useButton.Disable() + + def Refresh(self): + teleportData = self.__GetTeleportQuestData() + (x, y, z) = player.GetMainCharacterPosition() + currentMap = background.GetCurrentMapName() + + self.mapLine.SetText("Map: %s" % currentMap) + self.coordLine.SetText("Coords: %d, %d" % (x // 100, y // 100)) + + if not teleportData: + self.statusLine.SetText("Status: Waiting for teleport quest") + self.slotSummaryLine.SetText("Saved: -") + self.cooldownLine.SetText("Cooldown: -") + self.__RefreshSlotRows(3) + return + + self.statusLine.SetText("Status: %s" % ("Cooldown" if teleportData["clockValue"] > 0 else "Ready")) + self.slotSummaryLine.SetText("Saved: %d / %d" % (teleportData["savedCount"], teleportData["maxSlots"])) + if teleportData["clockValue"] > 0 and len(teleportData["clockName"]) > 0: + self.cooldownLine.SetText("Cooldown: %s" % self.__FormatCooldown(teleportData["clockValue"])) + else: + self.cooldownLine.SetText("Cooldown: Ready") + + self.__RefreshSlotRows(teleportData["maxSlots"]) + + def Show(self): + self.Refresh() + ui.BoardWithTitleBar.Show(self) + + def OnUpdate(self): + currentTime = time.time() + if currentTime - self.lastRefreshTime < 0.2: + return + + self.lastRefreshTime = currentTime + self.Refresh() + + def __SendTeleportCommand(self, action, arg): + net.SendChatPacket("/teleport_system %s %d" % (action, arg), 0) + self.lastRefreshTime = 0.0 + self.Refresh() + + def __SendSaveSlot(self, slot): + self.__SendTeleportCommand("save", slot) + + def __SendUseSlot(self, slot): + self.__SendTeleportCommand("saved", slot) + + def __SendPresetWarp(self, presetId): + self.__SendTeleportCommand("preset", presetId) diff --git a/assets/root/uiminimap.py b/assets/root/uiminimap.py index ae0143a8..cc2e1d97 100644 --- a/assets/root/uiminimap.py +++ b/assets/root/uiminimap.py @@ -225,6 +225,9 @@ class MiniMap(ui.ScriptWindow): self.tooltipBiolog = MapTextToolTip() self.tooltipBiolog.SetText("Biolog") self.tooltipBiolog.Show() + self.tooltipTeleport = MapTextToolTip() + self.tooltipTeleport.SetText("Teleport") + self.tooltipTeleport.Show() self.tooltipInfo = MapTextToolTip() self.tooltipInfo.Show() @@ -263,7 +266,9 @@ class MiniMap(ui.ScriptWindow): self.MiniMapShowButton = 0 self.AtlasShowButton = 0 self.BiologButton = 0 + self.TeleportButton = 0 self.biologButtonEvent = None + self.teleportButtonEvent = None self.tooltipMiniMapOpen = 0 self.tooltipMiniMapClose = 0 @@ -271,6 +276,7 @@ class MiniMap(ui.ScriptWindow): self.tooltipScaleDown = 0 self.tooltipAtlasOpen = 0 self.tooltipBiolog = 0 + self.tooltipTeleport = 0 self.tooltipInfo = None self.serverInfo = None @@ -363,6 +369,17 @@ class MiniMap(ui.ScriptWindow): self.BiologButton.SetEvent(self.biologButtonEvent) self.BiologButton.Show() + self.TeleportButton = ui.Button() + self.TeleportButton.SetParent(self.OpenWindow) + self.TeleportButton.SetPosition(9, 132) + self.TeleportButton.SetUpVisual("d:/ymir work/ui/public/small_thin_button_01.sub") + self.TeleportButton.SetOverVisual("d:/ymir work/ui/public/small_thin_button_02.sub") + self.TeleportButton.SetDownVisual("d:/ymir work/ui/public/small_thin_button_03.sub") + self.TeleportButton.SetText("TP") + if self.teleportButtonEvent: + self.TeleportButton.SetEvent(self.teleportButtonEvent) + self.TeleportButton.Show() + (ButtonPosX, ButtonPosY) = self.MiniMapShowButton.GetGlobalPosition() self.tooltipMiniMapOpen.SetTooltipPosition(ButtonPosX, ButtonPosY) @@ -381,6 +398,9 @@ class MiniMap(ui.ScriptWindow): (ButtonPosX, ButtonPosY) = self.BiologButton.GetGlobalPosition() self.tooltipBiolog.SetTooltipPosition(ButtonPosX, ButtonPosY) + (ButtonPosX, ButtonPosY) = self.TeleportButton.GetGlobalPosition() + self.tooltipTeleport.SetTooltipPosition(ButtonPosX, ButtonPosY) + self.ShowMiniMap() def Destroy(self): @@ -466,6 +486,11 @@ class MiniMap(ui.ScriptWindow): else: self.tooltipBiolog.Hide() + if True == self.TeleportButton.IsIn(): + self.tooltipTeleport.Show() + else: + self.tooltipTeleport.Hide() + def OnRender(self): (x, y) = self.GetGlobalPosition() fx = float(x) @@ -515,3 +540,8 @@ class MiniMap(ui.ScriptWindow): self.biologButtonEvent = event if self.BiologButton: self.BiologButton.SetEvent(event) + + def SetTeleportButtonEvent(self, event): + self.teleportButtonEvent = event + if self.TeleportButton: + self.TeleportButton.SetEvent(event) From 7871288b6d43d763bac46f4c81cc1613425b8190 Mon Sep 17 00:00:00 2001 From: server Date: Thu, 16 Apr 2026 19:28:16 +0200 Subject: [PATCH 2/5] issue-6: add autopickup settings ui --- assets/root/game.py | 5 + assets/root/uiAutopickup.py | 145 +++++++++++++++++++ assets/root/uigameoption.py | 18 +++ assets/uiscript/uiscript/gameoptiondialog.py | 28 +++- 4 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 assets/root/uiAutopickup.py diff --git a/assets/root/game.py b/assets/root/game.py index 84f0745d..8d4747fa 100644 --- a/assets/root/game.py +++ b/assets/root/game.py @@ -33,6 +33,7 @@ import uiAffectShower import uiPlayerGauge import uiCharacter import uiTarget +import uiAutopickup # PRIVATE_SHOP_PRICE_LIST import uiPrivateShopBuilder @@ -2177,6 +2178,7 @@ class GameWindow(ui.ScriptWindow): "PartyRequestDenied" : self.__PartyRequestDenied, "horse_state" : self.__Horse_UpdateState, "hide_horse_state" : self.__Horse_HideState, + "AutoPickupState" : self.__AutoPickupState, "WarUC" : self.__GuildWar_UpdateMemberCount, "test_server" : self.__EnableTestServerFlag, "mall" : self.__InGameShop_Show, @@ -2224,6 +2226,9 @@ class GameWindow(ui.ScriptWindow): def PartyHealReady(self): self.interface.PartyHealReady() + def __AutoPickupState(self, enabled, mode, mask, vip): + uiAutopickup.SetAutoPickupState(int(enabled), int(mode), int(mask), int(vip)) + def AskSafeboxPassword(self): self.interface.AskSafeboxPassword() diff --git a/assets/root/uiAutopickup.py b/assets/root/uiAutopickup.py new file mode 100644 index 00000000..0a290696 --- /dev/null +++ b/assets/root/uiAutopickup.py @@ -0,0 +1,145 @@ +import net +import ui + + +STATE_ENABLED = 0 +STATE_MODE = 0 +STATE_MASK = 31 +STATE_VIP = 0 +OPEN_WINDOWS = [] + +FILTERS = ( + (1, "Weapons"), + (2, "Armor"), + (4, "Yang"), + (8, "Stones"), + (16, "Materials"), +) + + +def SetAutoPickupState(enabled, mode, mask, vip): + global STATE_ENABLED + global STATE_MODE + global STATE_MASK + global STATE_VIP + + STATE_ENABLED = 1 if int(enabled) else 0 + STATE_MODE = 1 if int(mode) else 0 + STATE_MASK = int(mask) & 31 + STATE_VIP = 1 if int(vip) else 0 + + for window in OPEN_WINDOWS: + try: + window.ApplyState() + except: + pass + + +class AutoPickupWindow(ui.BoardWithTitleBar): + def __init__(self): + ui.BoardWithTitleBar.__init__(self) + + OPEN_WINDOWS.append(self) + + self.filterButtons = {} + + self.AddFlag("float") + self.AddFlag("movable") + self.SetSize(280, 248) + self.SetTitleName("Auto Pickup") + self.SetCloseEvent(self.Hide) + + self.__CreateChildren() + self.Hide() + + def __del__(self): + if self in OPEN_WINDOWS: + OPEN_WINDOWS.remove(self) + ui.BoardWithTitleBar.__del__(self) + + def Destroy(self): + if self in OPEN_WINDOWS: + OPEN_WINDOWS.remove(self) + self.ClearDictionary() + self.filterButtons = {} + + def __CreateChildren(self): + self.statusLine = self.__CreateLine(15, 36) + self.rangeLine = self.__CreateLine(15, 56) + self.modeLine = self.__CreateLine(15, 76) + self.noteLine = self.__CreateLine(15, 96) + + self.enableButton = self.__CreateButton(170, 34, 90, "Enable") + self.enableButton.SetEvent(self.__ToggleEnabled) + + self.whitelistButton = self.__CreateButton(15, 118, 118, "Whitelist") + self.whitelistButton.SetEvent(self.__SetMode, 0) + + self.blacklistButton = self.__CreateButton(142, 118, 118, "Blacklist") + self.blacklistButton.SetEvent(self.__SetMode, 1) + + for index, filterData in enumerate(FILTERS): + (bit, label) = filterData + button = self.__CreateButton(15, 150 + index * 18, 245, label) + button.SetEvent(self.__ToggleFilter, bit) + self.filterButtons[bit] = button + + def __CreateLine(self, x, y): + textLine = ui.TextLine() + textLine.SetParent(self) + textLine.SetPosition(x, y) + textLine.SetOutline() + textLine.Show() + return textLine + + def __CreateButton(self, x, y, width, text): + button = ui.Button() + button.SetParent(self) + button.SetPosition(x, y) + button.SetUpVisual("d:/ymir work/ui/public/small_thin_button_01.sub") + button.SetOverVisual("d:/ymir work/ui/public/small_thin_button_02.sub") + button.SetDownVisual("d:/ymir work/ui/public/small_thin_button_03.sub") + button.SetDisableVisual("d:/ymir work/ui/public/small_thin_button_01.sub") + button.SetSize(width, 17) + button.SetText(text) + button.Show() + return button + + def __Send(self, command): + net.SendChatPacket(command, 0) + + def __ToggleEnabled(self): + SetAutoPickupState(0 if STATE_ENABLED else 1, STATE_MODE, STATE_MASK, STATE_VIP) + self.__Send("/autopickup enable %d" % STATE_ENABLED) + + def __SetMode(self, mode): + SetAutoPickupState(STATE_ENABLED, mode, STATE_MASK, STATE_VIP) + self.__Send("/autopickup mode %s" % ("blacklist" if mode else "whitelist")) + + def __ToggleFilter(self, bit): + mask = STATE_MASK ^ bit + SetAutoPickupState(STATE_ENABLED, STATE_MODE, mask, STATE_VIP) + self.__Send("/autopickup mask %d" % STATE_MASK) + + def ApplyState(self): + self.statusLine.SetText("Status: %s" % ("Enabled" if STATE_ENABLED else "Disabled")) + self.rangeLine.SetText("Range: %s" % ("VIP 300 / Free 220" if STATE_VIP else "Free 220")) + self.modeLine.SetText("Mode: %s" % ("Blacklist" if STATE_MODE else "Whitelist")) + self.noteLine.SetText("Only nearby owned drops are valid") + self.enableButton.SetText("Disable" if STATE_ENABLED else "Enable") + + if STATE_MODE == 0: + self.whitelistButton.Down() + self.blacklistButton.SetUp() + else: + self.whitelistButton.SetUp() + self.blacklistButton.Down() + + for (bit, label) in FILTERS: + prefix = "[x]" if (STATE_MASK & bit) else "[ ]" + self.filterButtons[bit].SetText("%s %s" % (prefix, label)) + + def Show(self): + self.__Send("/autopickup sync") + self.ApplyState() + ui.BoardWithTitleBar.Show(self) diff --git a/assets/root/uigameoption.py b/assets/root/uigameoption.py index 56919877..601ababa 100644 --- a/assets/root/uigameoption.py +++ b/assets/root/uigameoption.py @@ -8,6 +8,7 @@ import localeInfo import constInfo import chrmgr import player +import uiAutopickup import uiPrivateShopBuilder # 占쏙옙占쏙옙호 import interfaceModule # 占쏙옙占쏙옙호 @@ -39,8 +40,14 @@ class OptionDialog(ui.ScriptWindow): self.alwaysShowNameButtonList = [] self.showDamageButtonList = [] self.showsalesTextButtonList = [] + self.autoPickupButton = None + self.autoPickupDialog = None def Destroy(self): + if self.autoPickupDialog: + self.autoPickupDialog.Destroy() + self.autoPickupDialog = None + self.ClearDictionary() self.__Initialize() @@ -80,6 +87,7 @@ class OptionDialog(ui.ScriptWindow): self.showDamageButtonList.append(GetObject("show_damage_off_button")) self.showsalesTextButtonList.append(GetObject("salestext_on_button")) self.showsalesTextButtonList.append(GetObject("salestext_off_button")) + self.autoPickupButton = GetObject("autopickup_button") except: import exception @@ -130,6 +138,7 @@ class OptionDialog(ui.ScriptWindow): self.showsalesTextButtonList[0].SAFE_SetEvent(self.__OnClickSalesTextOnButton) self.showsalesTextButtonList[1].SAFE_SetEvent(self.__OnClickSalesTextOffButton) + self.autoPickupButton.SAFE_SetEvent(self.__OnClickAutoPickupButton) self.__ClickRadioButton(self.nameColorModeButtonList, constInfo.GET_CHRNAME_COLOR_INDEX()) self.__ClickRadioButton(self.viewTargetBoardButtonList, constInfo.GET_VIEW_OTHER_EMPIRE_PLAYER_TARGET_BOARD()) @@ -241,6 +250,15 @@ class OptionDialog(ui.ScriptWindow): def __OnClickSalesTextOffButton(self): systemSetting.SetShowSalesTextFlag(False) self.RefreshShowSalesText() + + def __OnClickAutoPickupButton(self): + if not self.autoPickupDialog: + self.autoPickupDialog = uiAutopickup.AutoPickupWindow() + + if self.autoPickupDialog.IsShow(): + self.autoPickupDialog.Hide() + else: + self.autoPickupDialog.Show() def __CheckPvPProtectedLevelPlayer(self): if player.GetStatus(player.LEVEL) Date: Thu, 16 Apr 2026 20:02:14 +0200 Subject: [PATCH 3/5] issue-7: add stone queue ui --- assets/root/game.py | 5 + assets/root/uiStoneQueue.py | 233 ++++++++++++++++++++++++++++++++++++ assets/root/uiinventory.py | 29 +++-- 3 files changed, 252 insertions(+), 15 deletions(-) create mode 100644 assets/root/uiStoneQueue.py diff --git a/assets/root/game.py b/assets/root/game.py index 8d4747fa..b81463c2 100644 --- a/assets/root/game.py +++ b/assets/root/game.py @@ -34,6 +34,7 @@ import uiPlayerGauge import uiCharacter import uiTarget import uiAutopickup +import uiStoneQueue # PRIVATE_SHOP_PRICE_LIST import uiPrivateShopBuilder @@ -2179,6 +2180,7 @@ class GameWindow(ui.ScriptWindow): "horse_state" : self.__Horse_UpdateState, "hide_horse_state" : self.__Horse_HideState, "AutoPickupState" : self.__AutoPickupState, + "StoneQueueState" : self.__StoneQueueState, "WarUC" : self.__GuildWar_UpdateMemberCount, "test_server" : self.__EnableTestServerFlag, "mall" : self.__InGameShop_Show, @@ -2229,6 +2231,9 @@ class GameWindow(ui.ScriptWindow): def __AutoPickupState(self, enabled, mode, mask, vip): uiAutopickup.SetAutoPickupState(int(enabled), int(mode), int(mask), int(vip)) + def __StoneQueueState(self, maxCount, active, current, success, fail): + uiStoneQueue.SetStoneQueueState(int(maxCount), int(active), int(current), int(success), int(fail)) + def AskSafeboxPassword(self): self.interface.AskSafeboxPassword() diff --git a/assets/root/uiStoneQueue.py b/assets/root/uiStoneQueue.py new file mode 100644 index 00000000..349782b8 --- /dev/null +++ b/assets/root/uiStoneQueue.py @@ -0,0 +1,233 @@ +import item +import net +import player +import ui + + +STATE_MAX = 3 +STATE_ACTIVE = 0 +STATE_CURRENT = 0 +STATE_SUCCESS = 0 +STATE_FAIL = 0 +OPEN_WINDOWS = [] + + +def SetStoneQueueState(maxCount, active, current, success, fail): + global STATE_MAX + global STATE_ACTIVE + global STATE_CURRENT + global STATE_SUCCESS + global STATE_FAIL + + STATE_MAX = max(1, int(maxCount)) + STATE_ACTIVE = 1 if int(active) else 0 + STATE_CURRENT = max(0, int(current)) + STATE_SUCCESS = max(0, int(success)) + STATE_FAIL = max(0, int(fail)) + + for window in OPEN_WINDOWS: + try: + window.Refresh() + except: + pass + + +class StoneQueueWindow(ui.BoardWithTitleBar): + PAGE_SIZE = 20 + + def __init__(self): + ui.BoardWithTitleBar.__init__(self) + + OPEN_WINDOWS.append(self) + + self.scrollSlotPos = -1 + self.selectedSlots = [] + self.eligibleSlots = [] + self.pageIndex = 0 + self.candidateButtons = [] + self.queueLines = [] + + self.AddFlag("float") + self.AddFlag("movable") + self.SetSize(340, 355) + self.SetTitleName("Stone Queue") + self.SetCloseEvent(self.Hide) + + self.__CreateChildren() + self.Hide() + + def __del__(self): + if self in OPEN_WINDOWS: + OPEN_WINDOWS.remove(self) + ui.BoardWithTitleBar.__del__(self) + + def Destroy(self): + if self in OPEN_WINDOWS: + OPEN_WINDOWS.remove(self) + self.ClearDictionary() + self.selectedSlots = [] + self.eligibleSlots = [] + self.candidateButtons = [] + self.queueLines = [] + + def __CreateChildren(self): + self.scrollLine = self.__CreateLine(15, 36) + self.statusLine = self.__CreateLine(15, 56) + self.progressLine = self.__CreateLine(15, 76) + self.resultLine = self.__CreateLine(15, 96) + + self.prevButton = self.__CreateButton(15, 118, 45, "Prev") + self.prevButton.SetEvent(self.__ChangePage, -1) + self.pageLine = self.__CreateLine(127, 122) + self.nextButton = self.__CreateButton(280, 118, 45, "Next") + self.nextButton.SetEvent(self.__ChangePage, 1) + + for row in range(5): + for column in range(4): + button = self.__CreateButton(15 + column * 78, 145 + row * 24, 72, "-") + self.candidateButtons.append(button) + + for index in range(8): + line = self.__CreateLine(15, 272 + index * 10) + self.queueLines.append(line) + + self.startButton = self.__CreateButton(185, 328, 65, "Start") + self.startButton.SetEvent(self.__StartQueue) + self.cancelButton = self.__CreateButton(260, 328, 65, "Cancel") + self.cancelButton.SetEvent(self.__CancelQueue) + + def __CreateLine(self, x, y): + textLine = ui.TextLine() + textLine.SetParent(self) + textLine.SetPosition(x, y) + textLine.SetOutline() + textLine.Show() + return textLine + + def __CreateButton(self, x, y, width, text): + button = ui.Button() + button.SetParent(self) + button.SetPosition(x, y) + button.SetUpVisual("d:/ymir work/ui/public/small_thin_button_01.sub") + button.SetOverVisual("d:/ymir work/ui/public/small_thin_button_02.sub") + button.SetDownVisual("d:/ymir work/ui/public/small_thin_button_03.sub") + button.SetDisableVisual("d:/ymir work/ui/public/small_thin_button_01.sub") + button.SetSize(width, 17) + button.SetText(text) + button.Show() + return button + + def __BuildEligibleSlots(self): + self.eligibleSlots = [] + if self.scrollSlotPos < 0: + return + + scrollIndex = player.GetItemIndex(self.scrollSlotPos) + for slot in range(player.INVENTORY_PAGE_SIZE * player.INVENTORY_PAGE_COUNT): + if player.REFINE_OK != player.CanRefine(scrollIndex, slot): + continue + + targetIndex = player.GetItemIndex(slot) + if targetIndex == 0: + continue + + item.SelectItem(targetIndex) + if item.GetItemType() != item.ITEM_TYPE_METIN: + continue + + self.eligibleSlots.append(slot) + + def __GetSlotLabel(self, slot): + itemVnum = player.GetItemIndex(slot) + if itemVnum == 0: + return "Slot %d" % slot + + item.SelectItem(itemVnum) + return "%d:+%d" % (slot, player.GetItemGrade(slot)) + + def __RefreshCandidates(self): + start = self.pageIndex * self.PAGE_SIZE + end = start + self.PAGE_SIZE + pageSlots = self.eligibleSlots[start:end] + + totalPages = max(1, (len(self.eligibleSlots) + self.PAGE_SIZE - 1) // self.PAGE_SIZE) + self.pageLine.SetText("Page %d / %d" % (self.pageIndex + 1, totalPages)) + + for index, button in enumerate(self.candidateButtons): + if index < len(pageSlots): + slot = pageSlots[index] + prefix = "[x]" if slot in self.selectedSlots else "[ ]" + button.SetText("%s %s" % (prefix, self.__GetSlotLabel(slot))) + button.Enable() + button.SetEvent(self.__ToggleSlot, slot) + else: + button.SetText("-") + button.Disable() + + self.prevButton.Enable() if self.pageIndex > 0 else self.prevButton.Disable() + self.nextButton.Enable() if end < len(self.eligibleSlots) else self.nextButton.Disable() + + def __RefreshQueueLines(self): + for index, line in enumerate(self.queueLines): + if index < len(self.selectedSlots): + line.SetText("Queue %d: %s" % (index + 1, self.__GetSlotLabel(self.selectedSlots[index]))) + else: + line.SetText("Queue %d: -" % (index + 1)) + + def __ToggleSlot(self, slot): + if STATE_ACTIVE: + return + + if slot in self.selectedSlots: + self.selectedSlots.remove(slot) + elif len(self.selectedSlots) < STATE_MAX: + self.selectedSlots.append(slot) + + self.Refresh() + + def __ChangePage(self, delta): + totalPages = max(1, (len(self.eligibleSlots) + self.PAGE_SIZE - 1) // self.PAGE_SIZE) + self.pageIndex = max(0, min(totalPages - 1, self.pageIndex + delta)) + self.__RefreshCandidates() + + def __StartQueue(self): + if STATE_ACTIVE or self.scrollSlotPos < 0 or not self.selectedSlots: + return + + command = "/stone_queue start %d %s" % ( + self.scrollSlotPos, + " ".join([str(slot) for slot in self.selectedSlots[:STATE_MAX]]), + ) + net.SendChatPacket(command, 0) + + def __CancelQueue(self): + net.SendChatPacket("/stone_queue cancel", 0) + + def Open(self, scrollSlotPos, targetSlotPos): + self.scrollSlotPos = scrollSlotPos + self.pageIndex = 0 + self.selectedSlots = [] + + self.__BuildEligibleSlots() + if targetSlotPos in self.eligibleSlots: + self.selectedSlots.append(targetSlotPos) + + net.SendChatPacket("/stone_queue sync", 0) + self.Refresh() + self.SetTop() + self.Show() + + def Refresh(self): + self.scrollLine.SetText("Scroll slot: %d" % self.scrollSlotPos) + self.statusLine.SetText("Status: %s" % ("Running" if STATE_ACTIVE else "Ready")) + self.progressLine.SetText("Progress: %d / %d" % (STATE_CURRENT, len(self.selectedSlots))) + self.resultLine.SetText("Results: %d success / %d fail / max %d" % (STATE_SUCCESS, STATE_FAIL, STATE_MAX)) + self.__RefreshCandidates() + self.__RefreshQueueLines() + + if STATE_ACTIVE: + self.startButton.Disable() + self.cancelButton.Enable() + else: + self.startButton.Enable() + self.cancelButton.Disable() diff --git a/assets/root/uiinventory.py b/assets/root/uiinventory.py index deb20a39..6686922d 100644 --- a/assets/root/uiinventory.py +++ b/assets/root/uiinventory.py @@ -11,6 +11,7 @@ import grp import uiScriptLocale import uiRefine import uiAttachMetin +import uiStoneQueue import uiPickMoney import uiCommon import uiPrivateShopBuilder # Prevent ItemMove while private shop is open @@ -357,6 +358,10 @@ class InventoryWindow(ui.ScriptWindow): self.attachMetinDialog = uiAttachMetin.AttachMetinDialog() self.attachMetinDialog.Hide() + ## StoneQueueDialog + self.stoneQueueDialog = uiStoneQueue.StoneQueueWindow() + self.stoneQueueDialog.Hide() + ## MoneySlot self.wndMoneySlot.SetEvent(ui.__mem_func__(self.OpenPickMoneyDialog)) @@ -407,6 +412,9 @@ class InventoryWindow(ui.ScriptWindow): self.attachMetinDialog.Destroy() self.attachMetinDialog = 0 + self.stoneQueueDialog.Destroy() + self.stoneQueueDialog = 0 + self.tooltipItem = None self.wndItem = 0 self.wndEquip = 0 @@ -841,20 +849,6 @@ class InventoryWindow(ui.ScriptWindow): scrollIndex = player.GetItemIndex(scrollSlotPos) targetIndex = player.GetItemIndex(targetSlotPos) - if player.REFINE_OK != player.CanRefine(scrollIndex, targetSlotPos): - return - - ########################################################### - self.__SendUseItemToItemPacket(scrollSlotPos, targetSlotPos) - #net.SendItemUseToItemPacket(scrollSlotPos, targetSlotPos) - return - ########################################################### - - ########################################################### - #net.SendRequestRefineInfoPacket(targetSlotPos) - #return - ########################################################### - result = player.CanRefine(scrollIndex, targetSlotPos) if player.REFINE_ALREADY_MAX_SOCKET_COUNT == result: @@ -879,7 +873,12 @@ class InventoryWindow(ui.ScriptWindow): if player.REFINE_OK != result: return - self.refineDialog.Open(scrollSlotPos, targetSlotPos) + item.SelectItem(targetIndex) + if item.GetItemType() == item.ITEM_TYPE_METIN: + self.stoneQueueDialog.Open(scrollSlotPos, targetSlotPos) + return + + self.__SendUseItemToItemPacket(scrollSlotPos, targetSlotPos) def DetachMetinFromItem(self, scrollSlotPos, targetSlotPos): scrollIndex = player.GetItemIndex(scrollSlotPos) From 09efcadbdd8441016e5d48663245a50e32bb231f Mon Sep 17 00:00:00 2001 From: server Date: Thu, 16 Apr 2026 20:38:12 +0200 Subject: [PATCH 4/5] issue-8: add switchbot ui --- assets/root/game.py | 9 + assets/root/interfacemodule.py | 17 ++ assets/root/uiSwitchbot.py | 455 +++++++++++++++++++++++++++++++++ assets/root/uiminimap.py | 30 +++ 4 files changed, 511 insertions(+) create mode 100644 assets/root/uiSwitchbot.py diff --git a/assets/root/game.py b/assets/root/game.py index b81463c2..13bfa0f1 100644 --- a/assets/root/game.py +++ b/assets/root/game.py @@ -35,6 +35,7 @@ import uiCharacter import uiTarget import uiAutopickup import uiStoneQueue +import uiSwitchbot # PRIVATE_SHOP_PRICE_LIST import uiPrivateShopBuilder @@ -2181,6 +2182,8 @@ class GameWindow(ui.ScriptWindow): "hide_horse_state" : self.__Horse_HideState, "AutoPickupState" : self.__AutoPickupState, "StoneQueueState" : self.__StoneQueueState, + "SwitchbotState" : self.__SwitchbotState, + "SwitchbotSlot" : self.__SwitchbotSlot, "WarUC" : self.__GuildWar_UpdateMemberCount, "test_server" : self.__EnableTestServerFlag, "mall" : self.__InGameShop_Show, @@ -2234,6 +2237,12 @@ class GameWindow(ui.ScriptWindow): def __StoneQueueState(self, maxCount, active, current, success, fail): uiStoneQueue.SetStoneQueueState(int(maxCount), int(active), int(current), int(success), int(fail)) + def __SwitchbotState(self, speedIndex, activeCount, scrollCount, etaSeconds): + uiSwitchbot.SetSwitchbotState(int(speedIndex), int(activeCount), int(scrollCount), int(etaSeconds)) + + def __SwitchbotSlot(self, slotIndex, active, itemCell, attrType, minValue, attempts): + uiSwitchbot.SetSwitchbotSlotState(int(slotIndex), int(active), int(itemCell), int(attrType), int(minValue), int(attempts)) + def AskSafeboxPassword(self): self.interface.AskSafeboxPassword() diff --git a/assets/root/interfacemodule.py b/assets/root/interfacemodule.py index 02976c0a..6b88f4fe 100644 --- a/assets/root/interfacemodule.py +++ b/assets/root/interfacemodule.py @@ -27,6 +27,7 @@ import uiToolTip import uiMiniMap import uiBiolog import uiTeleport +import uiSwitchbot import uiParty import uiSafebox import uiGuild @@ -79,6 +80,7 @@ class Interface(object): self.wndMiniMap = None self.wndBiolog = None self.wndTeleport = None + self.wndSwitchbot = None self.wndGuild = None self.wndGuildBuilding = None @@ -186,6 +188,7 @@ class Interface(object): wndMiniMap = uiMiniMap.MiniMap() wndBiolog = uiBiolog.BiologWindow() wndTeleport = uiTeleport.TeleportWindow() + wndSwitchbot = uiSwitchbot.SwitchbotWindow() wndSafebox = uiSafebox.SafeboxWindow() # ITEM_MALL @@ -203,10 +206,12 @@ class Interface(object): self.wndMiniMap = wndMiniMap self.wndBiolog = wndBiolog self.wndTeleport = wndTeleport + self.wndSwitchbot = wndSwitchbot self.wndSafebox = wndSafebox self.wndChatLog = wndChatLog self.wndMiniMap.SetBiologButtonEvent(ui.__mem_func__(self.ToggleBiologWindow)) self.wndMiniMap.SetTeleportButtonEvent(ui.__mem_func__(self.ToggleTeleportWindow)) + self.wndMiniMap.SetSwitchbotButtonEvent(ui.__mem_func__(self.ToggleSwitchbotWindow)) if app.ENABLE_DRAGON_SOUL_SYSTEM: self.wndDragonSoul.SetDragonSoulRefineWindow(self.wndDragonSoulRefine) @@ -421,6 +426,8 @@ class Interface(object): if self.wndBiolog: self.wndBiolog.Hide() + if self.wndSwitchbot: + self.wndSwitchbot.Hide() if self.wndSafebox: self.wndSafebox.Destroy() @@ -513,6 +520,7 @@ class Interface(object): del self.tooltipSkill del self.wndMiniMap del self.wndBiolog + del self.wndSwitchbot del self.wndSafebox del self.wndMall del self.wndParty @@ -977,6 +985,15 @@ class Interface(object): else: self.wndTeleport.Hide() + def ToggleSwitchbotWindow(self): + if False == self.wndSwitchbot.IsShow(): + (miniMapX, miniMapY) = self.wndMiniMap.GetGlobalPosition() + self.wndSwitchbot.SetPosition(max(10, miniMapX - self.wndSwitchbot.GetWidth() - 10), miniMapY + 40) + self.wndSwitchbot.Show() + self.wndSwitchbot.SetTop() + else: + self.wndSwitchbot.Hide() + def ToggleCharacterWindow(self, state): if False == player.IsObserverMode(): if False == self.wndCharacter.IsShow(): diff --git a/assets/root/uiSwitchbot.py b/assets/root/uiSwitchbot.py new file mode 100644 index 00000000..b59e0dc2 --- /dev/null +++ b/assets/root/uiSwitchbot.py @@ -0,0 +1,455 @@ +import item +import net +import player +import ui +import uitooltip + + +STATE_SPEED = 1 +STATE_ACTIVE_COUNT = 0 +STATE_SCROLL_COUNT = 0 +STATE_ETA_SECONDS = 0 +STATE_SLOTS = [] +OPEN_WINDOWS = [] +_AFFECT_HELPER = None + +for _slotIndex in range(5): + STATE_SLOTS.append({ + "active": 0, + "itemCell": 0, + "attrType": 0, + "minValue": 0, + "attempts": 0, + }) + + +def _GetAffectHelper(): + global _AFFECT_HELPER + + if _AFFECT_HELPER is None: + _AFFECT_HELPER = uitooltip.ItemToolTip() + + return _AFFECT_HELPER + + +def _GetAffectLabel(attrType): + if attrType <= 0: + return "-" + + try: + label = _GetAffectHelper()._ItemToolTip__GetAffectString(attrType, 1) + except: + label = None + + if not label: + return "Attr %d" % attrType + + return label + + +ATTR_OPTIONS = ( + (item.APPLY_MAX_HP, _GetAffectLabel(item.APPLY_MAX_HP)), + (item.APPLY_MAX_SP, _GetAffectLabel(item.APPLY_MAX_SP)), + (item.APPLY_STR, _GetAffectLabel(item.APPLY_STR)), + (item.APPLY_DEX, _GetAffectLabel(item.APPLY_DEX)), + (item.APPLY_CON, _GetAffectLabel(item.APPLY_CON)), + (item.APPLY_INT, _GetAffectLabel(item.APPLY_INT)), + (item.APPLY_ATT_SPEED, _GetAffectLabel(item.APPLY_ATT_SPEED)), + (item.APPLY_MOV_SPEED, _GetAffectLabel(item.APPLY_MOV_SPEED)), + (item.APPLY_CAST_SPEED, _GetAffectLabel(item.APPLY_CAST_SPEED)), + (item.APPLY_CRITICAL_PCT, _GetAffectLabel(item.APPLY_CRITICAL_PCT)), + (item.APPLY_PENETRATE_PCT, _GetAffectLabel(item.APPLY_PENETRATE_PCT)), + (item.APPLY_ATTBONUS_MONSTER, _GetAffectLabel(item.APPLY_ATTBONUS_MONSTER)), + (item.APPLY_ATTBONUS_DEVIL, _GetAffectLabel(item.APPLY_ATTBONUS_DEVIL)), + (item.APPLY_ATTBONUS_HUMAN, _GetAffectLabel(item.APPLY_ATTBONUS_HUMAN)), + (item.APPLY_STEAL_HP, _GetAffectLabel(item.APPLY_STEAL_HP)), + (item.APPLY_STEAL_SP, _GetAffectLabel(item.APPLY_STEAL_SP)), + (item.APPLY_BLOCK, _GetAffectLabel(item.APPLY_BLOCK)), + (item.APPLY_DODGE, _GetAffectLabel(item.APPLY_DODGE)), + (item.APPLY_ATT_GRADE_BONUS, _GetAffectLabel(item.APPLY_ATT_GRADE_BONUS)), + (item.APPLY_DEF_GRADE_BONUS, _GetAffectLabel(item.APPLY_DEF_GRADE_BONUS)), + (item.APPLY_RESIST_SWORD, _GetAffectLabel(item.APPLY_RESIST_SWORD)), + (item.APPLY_RESIST_TWOHAND, _GetAffectLabel(item.APPLY_RESIST_TWOHAND)), + (item.APPLY_RESIST_DAGGER, _GetAffectLabel(item.APPLY_RESIST_DAGGER)), + (item.APPLY_RESIST_BOW, _GetAffectLabel(item.APPLY_RESIST_BOW)), + (item.APPLY_RESIST_FIRE, _GetAffectLabel(item.APPLY_RESIST_FIRE)), + (item.APPLY_RESIST_ELEC, _GetAffectLabel(item.APPLY_RESIST_ELEC)), + (item.APPLY_RESIST_MAGIC, _GetAffectLabel(item.APPLY_RESIST_MAGIC)), + (item.APPLY_RESIST_WIND, _GetAffectLabel(item.APPLY_RESIST_WIND)), + (item.APPLY_RESIST_ICE, _GetAffectLabel(item.APPLY_RESIST_ICE)), + (item.APPLY_RESIST_EARTH, _GetAffectLabel(item.APPLY_RESIST_EARTH)), + (item.APPLY_RESIST_DARK, _GetAffectLabel(item.APPLY_RESIST_DARK)), +) + +SPEED_OPTIONS = ( + (0, "Slow / 3s"), + (1, "Normal / 2s"), + (2, "Fast / 1s"), +) + + +def SetSwitchbotState(speedIndex, activeCount, scrollCount, etaSeconds): + global STATE_SPEED + global STATE_ACTIVE_COUNT + global STATE_SCROLL_COUNT + global STATE_ETA_SECONDS + + STATE_SPEED = int(speedIndex) + STATE_ACTIVE_COUNT = max(0, int(activeCount)) + STATE_SCROLL_COUNT = max(0, int(scrollCount)) + STATE_ETA_SECONDS = max(0, int(etaSeconds)) + + for window in OPEN_WINDOWS: + try: + window.Refresh() + except: + pass + + +def SetSwitchbotSlotState(slotIndex, active, itemCell, attrType, minValue, attempts): + slotIndex = int(slotIndex) + if slotIndex < 0 or slotIndex >= len(STATE_SLOTS): + return + + STATE_SLOTS[slotIndex]["active"] = 1 if int(active) else 0 + STATE_SLOTS[slotIndex]["itemCell"] = int(itemCell) + STATE_SLOTS[slotIndex]["attrType"] = int(attrType) + STATE_SLOTS[slotIndex]["minValue"] = int(minValue) + STATE_SLOTS[slotIndex]["attempts"] = int(attempts) + + for window in OPEN_WINDOWS: + try: + window.Refresh() + except: + pass + + +class SwitchbotWindow(ui.BoardWithTitleBar): + CANDIDATE_PAGE_SIZE = 12 + + def __init__(self): + ui.BoardWithTitleBar.__init__(self) + + OPEN_WINDOWS.append(self) + + self.selectedSlotIndex = 0 + self.pageIndex = 0 + self.candidateSlots = [] + self.slotWidgets = [] + self.candidateButtons = [] + self.speedLabels = {} + self.attrLabels = {} + + self.AddFlag("float") + self.AddFlag("movable") + self.SetSize(535, 406) + self.SetTitleName("Switchbot") + self.SetCloseEvent(self.Hide) + + self.__CreateChildren() + self.Hide() + + def __del__(self): + if self in OPEN_WINDOWS: + OPEN_WINDOWS.remove(self) + ui.BoardWithTitleBar.__del__(self) + + def Destroy(self): + if self in OPEN_WINDOWS: + OPEN_WINDOWS.remove(self) + self.ClearDictionary() + self.slotWidgets = [] + self.candidateButtons = [] + self.candidateSlots = [] + self.speedLabels = {} + self.attrLabels = {} + + def __CreateChildren(self): + self.statusLine = self.__CreateLine(15, 36) + self.scrollLine = self.__CreateLine(15, 56) + self.etaLine = self.__CreateLine(15, 76) + self.helpLine = self.__CreateLine(15, 96) + + self.speedCombo = ui.ComboBox() + self.speedCombo.SetParent(self) + self.speedCombo.SetPosition(398, 34) + self.speedCombo.SetSize(120, 18) + self.speedCombo.SetEvent(self.__OnChangeSpeed) + self.speedCombo.Show() + + for (speedIndex, label) in SPEED_OPTIONS: + self.speedCombo.InsertItem(speedIndex, label) + self.speedLabels[speedIndex] = label + + for slotIndex in range(5): + baseY = 126 + slotIndex * 38 + row = {} + + row["selectButton"] = self.__CreateButton(15, baseY, 48, "Slot %d" % (slotIndex + 1)) + row["selectButton"].SetEvent(self.__SelectSlot, slotIndex) + + row["itemLine"] = self.__CreateLine(72, baseY + 2) + row["statusLine"] = self.__CreateLine(72, baseY + 18) + + row["attrCombo"] = ui.ComboBox() + row["attrCombo"].SetParent(self) + row["attrCombo"].SetPosition(220, baseY) + row["attrCombo"].SetSize(165, 18) + row["attrCombo"].SetEvent(lambda attrType, line=slotIndex: self.__SetAttrType(line, attrType)) + row["attrCombo"].Show() + + for (attrType, label) in ATTR_OPTIONS: + row["attrCombo"].InsertItem(attrType, label) + + attrBar = ui.SlotBar() + attrBar.SetParent(self) + attrBar.SetPosition(392, baseY) + attrBar.SetSize(48, 18) + attrBar.Show() + + row["valueBar"] = attrBar + row["valueEdit"] = ui.EditLine() + row["valueEdit"].SetParent(attrBar) + row["valueEdit"].SetPosition(3, 3) + row["valueEdit"].SetSize(42, 12) + row["valueEdit"].SetMax(5) + row["valueEdit"].SetNumberMode() + row["valueEdit"].Show() + + row["startButton"] = self.__CreateButton(447, baseY, 40, "Start") + row["startButton"].SetEvent(self.__ToggleSlot, slotIndex) + row["clearButton"] = self.__CreateButton(492, baseY, 28, "X") + row["clearButton"].SetEvent(self.__ClearSlot, slotIndex) + + self.slotWidgets.append(row) + + self.prevButton = self.__CreateButton(15, 330, 44, "Prev") + self.prevButton.SetEvent(self.__ChangePage, -1) + self.pageLine = self.__CreateLine(80, 334) + self.nextButton = self.__CreateButton(170, 330, 44, "Next") + self.nextButton.SetEvent(self.__ChangePage, 1) + + for index in range(self.CANDIDATE_PAGE_SIZE): + column = index % 2 + row = index // 2 + button = self.__CreateButton(15 + column * 155, 356 + row * 18, 148, "-") + button.SetEvent(self.__AssignCandidate, index) + self.candidateButtons.append(button) + + self.syncButton = self.__CreateButton(305, 330, 54, "Sync") + self.syncButton.SetEvent(self.__Sync) + self.startAllButton = self.__CreateButton(366, 330, 74, "Start All") + self.startAllButton.SetEvent(self.__StartAll) + self.stopAllButton = self.__CreateButton(446, 330, 74, "Stop All") + self.stopAllButton.SetEvent(self.__StopAll) + + def __CreateLine(self, x, y): + textLine = ui.TextLine() + textLine.SetParent(self) + textLine.SetPosition(x, y) + textLine.SetOutline() + textLine.Show() + return textLine + + def __CreateButton(self, x, y, width, text): + button = ui.Button() + button.SetParent(self) + button.SetPosition(x, y) + button.SetUpVisual("d:/ymir work/ui/public/small_thin_button_01.sub") + button.SetOverVisual("d:/ymir work/ui/public/small_thin_button_02.sub") + button.SetDownVisual("d:/ymir work/ui/public/small_thin_button_03.sub") + button.SetDisableVisual("d:/ymir work/ui/public/small_thin_button_01.sub") + button.SetSize(width, 17) + button.SetText(text) + button.Show() + return button + + def __Send(self, command): + net.SendChatPacket(command, 0) + + def __IsEligibleItem(self, slotPos): + itemVnum = player.GetItemIndex(slotPos) + if itemVnum == 0: + return False + + item.SelectItem(itemVnum) + if item.GetItemType() == item.ITEM_TYPE_COSTUME: + return False + + if item.GetItemType() not in (item.ITEM_TYPE_WEAPON, item.ITEM_TYPE_ARMOR): + return False + + for attrIndex in range(player.ATTRIBUTE_SLOT_MAX_NUM): + if player.GetItemAttribute(slotPos, attrIndex)[0] != 0: + return True + + return False + + def __GetItemLabel(self, slotPos): + itemVnum = player.GetItemIndex(slotPos) + if itemVnum == 0: + return "Slot %d" % slotPos + + item.SelectItem(itemVnum) + return "%d: %s" % (slotPos, item.GetItemName()) + + def __BuildCandidateSlots(self): + self.candidateSlots = [] + + for slotPos in range(player.INVENTORY_PAGE_SIZE * player.INVENTORY_PAGE_COUNT): + if self.__IsEligibleItem(slotPos): + self.candidateSlots.append(slotPos) + + def __FormatEta(self): + if STATE_ETA_SECONDS <= 0: + return "ETA: -" + + minutes = STATE_ETA_SECONDS // 60 + seconds = STATE_ETA_SECONDS % 60 + return "ETA: %02d:%02d" % (minutes, seconds) + + def __GetAttrType(self, slotIndex): + return STATE_SLOTS[slotIndex]["attrType"] + + def __GetMinValue(self, slotIndex): + text = self.slotWidgets[slotIndex]["valueEdit"].GetText() + if not text: + return STATE_SLOTS[slotIndex]["minValue"] + return int(text) + + def __SelectSlot(self, slotIndex): + self.selectedSlotIndex = slotIndex + self.Refresh() + + def __SetAttrType(self, slotIndex, attrType): + SetSwitchbotSlotState( + slotIndex, + STATE_SLOTS[slotIndex]["active"], + STATE_SLOTS[slotIndex]["itemCell"], + attrType, + STATE_SLOTS[slotIndex]["minValue"], + STATE_SLOTS[slotIndex]["attempts"], + ) + + def __AssignCandidate(self, localIndex): + candidateIndex = self.pageIndex * self.CANDIDATE_PAGE_SIZE + localIndex + if candidateIndex < 0 or candidateIndex >= len(self.candidateSlots): + return + + slotIndex = self.selectedSlotIndex + SetSwitchbotSlotState( + slotIndex, + 0, + self.candidateSlots[candidateIndex], + STATE_SLOTS[slotIndex]["attrType"], + STATE_SLOTS[slotIndex]["minValue"], + 0, + ) + + def __ToggleSlot(self, slotIndex): + slotState = STATE_SLOTS[slotIndex] + if slotState["active"]: + self.__Send("/switchbot stop %d" % slotIndex) + return + + itemCell = slotState["itemCell"] + attrType = self.__GetAttrType(slotIndex) + minValue = self.__GetMinValue(slotIndex) + if not self.__IsEligibleItem(itemCell) or attrType <= 0 or minValue <= 0: + return + + SetSwitchbotSlotState(slotIndex, 0, itemCell, attrType, minValue, slotState["attempts"]) + self.__Send("/switchbot start %d %d %d %d" % (slotIndex, itemCell, attrType, minValue)) + + def __ClearSlot(self, slotIndex): + self.__Send("/switchbot clear %d" % slotIndex) + + def __ChangePage(self, delta): + totalPages = max(1, (len(self.candidateSlots) + self.CANDIDATE_PAGE_SIZE - 1) // self.CANDIDATE_PAGE_SIZE) + self.pageIndex = max(0, min(totalPages - 1, self.pageIndex + delta)) + self.Refresh() + + def __OnChangeSpeed(self, speedIndex): + self.__Send("/switchbot speed %d" % int(speedIndex)) + + def __Sync(self): + self.__Send("/switchbot sync") + + def __StartAll(self): + for slotIndex in range(len(STATE_SLOTS)): + slotState = STATE_SLOTS[slotIndex] + if slotState["active"]: + continue + + itemCell = slotState["itemCell"] + attrType = self.__GetAttrType(slotIndex) + minValue = self.__GetMinValue(slotIndex) + if not self.__IsEligibleItem(itemCell) or attrType <= 0 or minValue <= 0: + continue + + self.__Send("/switchbot start %d %d %d %d" % (slotIndex, itemCell, attrType, minValue)) + + def __StopAll(self): + self.__Send("/switchbot stop_all") + + def Refresh(self): + self.__BuildCandidateSlots() + + self.statusLine.SetText("Status: %d active slot(s)" % STATE_ACTIVE_COUNT) + self.scrollLine.SetText("Switch items: %d" % STATE_SCROLL_COUNT) + self.etaLine.SetText(self.__FormatEta()) + self.helpLine.SetText("Select a row, assign an item, pick a target bonus and min value") + self.speedCombo.SetCurrentItem(self.speedLabels.get(STATE_SPEED, SPEED_OPTIONS[1][1])) + + for slotIndex in range(len(self.slotWidgets)): + slotState = STATE_SLOTS[slotIndex] + row = self.slotWidgets[slotIndex] + row["selectButton"].SetText(("> " if self.selectedSlotIndex == slotIndex else "") + "S%d" % (slotIndex + 1)) + row["itemLine"].SetText("Item: %s" % ("-" if not self.__IsEligibleItem(slotState["itemCell"]) else self.__GetItemLabel(slotState["itemCell"]))) + row["statusLine"].SetText("Target: %s / tries %d / %s" % ( + _GetAffectLabel(slotState["attrType"]), + slotState["attempts"], + "Running" if slotState["active"] else "Ready", + )) + + if slotState["attrType"] > 0: + row["attrCombo"].SetCurrentItem(_GetAffectLabel(slotState["attrType"])) + else: + row["attrCombo"].SetCurrentItem("-") + + if slotState["minValue"] > 0 and row["valueEdit"].GetText() != str(slotState["minValue"]): + row["valueEdit"].SetText(str(slotState["minValue"])) + elif slotState["minValue"] <= 0 and row["valueEdit"].GetText(): + row["valueEdit"].SetText("") + + row["startButton"].SetText("Stop" if slotState["active"] else "Start") + + start = self.pageIndex * self.CANDIDATE_PAGE_SIZE + end = start + self.CANDIDATE_PAGE_SIZE + pageSlots = self.candidateSlots[start:end] + totalPages = max(1, (len(self.candidateSlots) + self.CANDIDATE_PAGE_SIZE - 1) // self.CANDIDATE_PAGE_SIZE) + self.pageLine.SetText("Candidates %d / %d" % (self.pageIndex + 1, totalPages)) + + for localIndex in range(len(self.candidateButtons)): + button = self.candidateButtons[localIndex] + if localIndex < len(pageSlots): + button.SetText(self.__GetItemLabel(pageSlots[localIndex])) + button.Enable() + else: + button.SetText("-") + button.Disable() + + if self.pageIndex > 0: + self.prevButton.Enable() + else: + self.prevButton.Disable() + + if end < len(self.candidateSlots): + self.nextButton.Enable() + else: + self.nextButton.Disable() + + def Show(self): + self.__Sync() + self.Refresh() + ui.BoardWithTitleBar.Show(self) diff --git a/assets/root/uiminimap.py b/assets/root/uiminimap.py index cc2e1d97..bb249cad 100644 --- a/assets/root/uiminimap.py +++ b/assets/root/uiminimap.py @@ -228,6 +228,9 @@ class MiniMap(ui.ScriptWindow): self.tooltipTeleport = MapTextToolTip() self.tooltipTeleport.SetText("Teleport") self.tooltipTeleport.Show() + self.tooltipSwitchbot = MapTextToolTip() + self.tooltipSwitchbot.SetText("Switchbot") + self.tooltipSwitchbot.Show() self.tooltipInfo = MapTextToolTip() self.tooltipInfo.Show() @@ -267,8 +270,10 @@ class MiniMap(ui.ScriptWindow): self.AtlasShowButton = 0 self.BiologButton = 0 self.TeleportButton = 0 + self.SwitchbotButton = 0 self.biologButtonEvent = None self.teleportButtonEvent = None + self.switchbotButtonEvent = None self.tooltipMiniMapOpen = 0 self.tooltipMiniMapClose = 0 @@ -277,6 +282,7 @@ class MiniMap(ui.ScriptWindow): self.tooltipAtlasOpen = 0 self.tooltipBiolog = 0 self.tooltipTeleport = 0 + self.tooltipSwitchbot = 0 self.tooltipInfo = None self.serverInfo = None @@ -380,6 +386,17 @@ class MiniMap(ui.ScriptWindow): self.TeleportButton.SetEvent(self.teleportButtonEvent) self.TeleportButton.Show() + self.SwitchbotButton = ui.Button() + self.SwitchbotButton.SetParent(self.OpenWindow) + self.SwitchbotButton.SetPosition(9, 153) + self.SwitchbotButton.SetUpVisual("d:/ymir work/ui/public/small_thin_button_01.sub") + self.SwitchbotButton.SetOverVisual("d:/ymir work/ui/public/small_thin_button_02.sub") + self.SwitchbotButton.SetDownVisual("d:/ymir work/ui/public/small_thin_button_03.sub") + self.SwitchbotButton.SetText("Sw") + if self.switchbotButtonEvent: + self.SwitchbotButton.SetEvent(self.switchbotButtonEvent) + self.SwitchbotButton.Show() + (ButtonPosX, ButtonPosY) = self.MiniMapShowButton.GetGlobalPosition() self.tooltipMiniMapOpen.SetTooltipPosition(ButtonPosX, ButtonPosY) @@ -401,6 +418,9 @@ class MiniMap(ui.ScriptWindow): (ButtonPosX, ButtonPosY) = self.TeleportButton.GetGlobalPosition() self.tooltipTeleport.SetTooltipPosition(ButtonPosX, ButtonPosY) + (ButtonPosX, ButtonPosY) = self.SwitchbotButton.GetGlobalPosition() + self.tooltipSwitchbot.SetTooltipPosition(ButtonPosX, ButtonPosY) + self.ShowMiniMap() def Destroy(self): @@ -491,6 +511,11 @@ class MiniMap(ui.ScriptWindow): else: self.tooltipTeleport.Hide() + if True == self.SwitchbotButton.IsIn(): + self.tooltipSwitchbot.Show() + else: + self.tooltipSwitchbot.Hide() + def OnRender(self): (x, y) = self.GetGlobalPosition() fx = float(x) @@ -545,3 +570,8 @@ class MiniMap(ui.ScriptWindow): self.teleportButtonEvent = event if self.TeleportButton: self.TeleportButton.SetEvent(event) + + def SetSwitchbotButtonEvent(self, event): + self.switchbotButtonEvent = event + if self.SwitchbotButton: + self.SwitchbotButton.SetEvent(event) From 12df5a2bd6f8c7765c2ed973f441a45a7394e8ca Mon Sep 17 00:00:00 2001 From: server Date: Thu, 16 Apr 2026 21:08:13 +0200 Subject: [PATCH 5/5] issue-9: add sash inventory integration --- assets/root/playersettingmodule.py | 5 +- assets/root/uiinventory.py | 114 +++++++++++++++++++- assets/root/uitooltip.py | 11 ++ assets/uiscript/uiscript/costumewindow.py | 8 +- assets/uiscript/uiscript/equipmentdialog.py | 1 + 5 files changed, 131 insertions(+), 8 deletions(-) diff --git a/assets/root/playersettingmodule.py b/assets/root/playersettingmodule.py index d01c4da5..75e00183 100644 --- a/assets/root/playersettingmodule.py +++ b/assets/root/playersettingmodule.py @@ -659,6 +659,7 @@ def __LoadGameWarriorEx(race, path): ## Bone chrmgr.RegisterAttachingBoneName(chr.PART_WEAPON, "equip_right_hand") + chrmgr.RegisterAttachingBoneName(chr.PART_ACCE, "Bip01 Spine2") def __LoadGameAssassinEx(race, path): ## Assassin @@ -872,6 +873,7 @@ def __LoadGameAssassinEx(race, path): chrmgr.RegisterAttachingBoneName(chr.PART_WEAPON, "equip_right") chrmgr.RegisterAttachingBoneName(chr.PART_WEAPON_LEFT, "equip_left") + chrmgr.RegisterAttachingBoneName(chr.PART_ACCE, "Bip01 Spine2") def __LoadGameSuraEx(race, path): ## Sura @@ -1189,6 +1191,7 @@ def __LoadGameShamanEx(race, path): chrmgr.RegisterAttachingBoneName(chr.PART_WEAPON, "equip_right") chrmgr.RegisterAttachingBoneName(chr.PART_WEAPON_LEFT, "equip_left") + chrmgr.RegisterAttachingBoneName(chr.PART_ACCE, "Bip01 Spine2") def __LoadGameSkill(): @@ -1456,4 +1459,4 @@ def SetGuildBuilding(race, name, grade): chrmgr.SetPathName("d:/ymir work/guild/building/%s/" % name) chrmgr.LoadRaceData("%s%02d.msm" % (name, grade)) chrmgr.RegisterMotionMode(chr.MOTION_MODE_GENERAL) - #chrmgr.RegisterMotionData(chr.MOTION_MODE_GENERAL, chr.MOTION_DEAD, name + "_destruction.msa") \ No newline at end of file + #chrmgr.RegisterMotionData(chr.MOTION_MODE_GENERAL, chr.MOTION_DEAD, name + "_destruction.msa") diff --git a/assets/root/uiinventory.py b/assets/root/uiinventory.py index 6686922d..0c673aa9 100644 --- a/assets/root/uiinventory.py +++ b/assets/root/uiinventory.py @@ -24,6 +24,12 @@ ITEM_FLAG_APPLICABLE = 1 << 14 class CostumeWindow(ui.ScriptWindow): + SLOT_ORDER = ( + item.COSTUME_SLOT_BODY, + item.COSTUME_SLOT_HAIR, + item.COSTUME_SLOT_SASH, + ) + def __init__(self, wndInventory): import exception @@ -88,8 +94,7 @@ class CostumeWindow(ui.ScriptWindow): def RefreshCostumeSlot(self): getItemVNum=player.GetItemIndex - for i in range(item.COSTUME_SLOT_COUNT): - slotNumber = item.COSTUME_SLOT_START + i + for slotNumber in self.SLOT_ORDER: self.wndEquip.SetItemSlot(slotNumber, getItemVNum(slotNumber), 0) self.wndEquip.RefreshSlot() @@ -252,11 +257,13 @@ class InventoryWindow(ui.ScriptWindow): isLoaded = 0 isOpenedCostumeWindowWhenClosingInventory = 0 # Whether costume window was open when closing inventory isOpenedBeltWindowWhenClosingInventory = 0 # Whether belt inventory was open when closing inventory + sashAbsorbSlot = -1 def __init__(self): ui.ScriptWindow.__init__(self) self.isOpenedBeltWindowWhenClosingInventory = 0 # Whether belt inventory was open when closing inventory + self.sashAbsorbSlot = -1 self.__LoadWindow() @@ -439,6 +446,7 @@ class InventoryWindow(ui.ScriptWindow): self.equipmentTab = [] def Hide(self): + self.__AbortSashAbsorb(False) if constInfo.GET_ITEM_QUESTION_DIALOG_STATUS(): self.OnCloseQuestionDialog() return @@ -463,6 +471,91 @@ class InventoryWindow(ui.ScriptWindow): def Close(self): self.Hide() + def __AbortSashAbsorb(self, showMessage=True): + if self.sashAbsorbSlot == -1: + return + + self.sashAbsorbSlot = -1 + if showMessage: + chat.AppendChat(chat.CHAT_TYPE_INFO, "Sash absorb canceled.") + + def __IsSashItem(self, slotIndex): + itemVnum = player.GetItemIndex(slotIndex) + if not itemVnum: + return False + + item.SelectItem(itemVnum) + return item.GetItemType() == item.ITEM_TYPE_COSTUME and item.GetItemSubType() == item.COSTUME_TYPE_SASH + + def __CanAbsorbIntoSash(self, sashSlot, targetSlot): + if sashSlot == targetSlot: + return False + + if player.IsEquipmentSlot(sashSlot) or player.IsEquipmentSlot(targetSlot): + return False + + if player.GetItemMetinSocket(sashSlot, 0): + return False + + targetVnum = player.GetItemIndex(targetSlot) + if not targetVnum: + return False + + item.SelectItem(targetVnum) + itemType = item.GetItemType() + itemSubType = item.GetItemSubType() + + if itemType == item.ITEM_TYPE_WEAPON: + return itemSubType != item.WEAPON_ARROW + + if itemType != item.ITEM_TYPE_ARMOR: + return False + + return itemSubType in ( + item.ARMOR_BODY, + item.ARMOR_HEAD, + item.ARMOR_SHIELD, + item.ARMOR_WRIST, + item.ARMOR_FOOTS, + item.ARMOR_NECK, + item.ARMOR_EAR, + ) + + def __BeginSashAbsorb(self, sashSlot): + if player.IsEquipmentSlot(sashSlot): + chat.AppendChat(chat.CHAT_TYPE_INFO, "Unequip the sash before absorbing bonuses.") + return + + if player.GetItemMetinSocket(sashSlot, 0): + chat.AppendChat(chat.CHAT_TYPE_INFO, "This sash already contains absorbed bonuses.") + return + + if self.sashAbsorbSlot == sashSlot: + self.__AbortSashAbsorb() + return + + self.sashAbsorbSlot = sashSlot + chat.AppendChat(chat.CHAT_TYPE_INFO, "Select a weapon or armor piece to absorb into the sash.") + + def __OpenSashAbsorbQuestion(self, sashSlot, targetSlot): + targetVnum = player.GetItemIndex(targetSlot) + item.SelectItem(targetVnum) + targetName = item.GetItemName() + + self.questionDialog = uiCommon.QuestionDialog() + self.questionDialog.SetText("Absorb bonuses from %s? The source item will be destroyed." % targetName) + self.questionDialog.SetAcceptEvent(ui.__mem_func__(self.__AcceptSashAbsorb)) + self.questionDialog.SetCancelEvent(ui.__mem_func__(self.OnCloseQuestionDialog)) + self.questionDialog.sashSlot = sashSlot + self.questionDialog.targetSlot = targetSlot + self.questionDialog.Open() + constInfo.SET_ITEM_QUESTION_DIALOG_STATUS(1) + + def __AcceptSashAbsorb(self): + net.SendChatPacket("/sash absorb %d %d" % (self.questionDialog.sashSlot, self.questionDialog.targetSlot), 0) + self.__AbortSashAbsorb(False) + self.OnCloseQuestionDialog() + def SetInventoryPage(self, page): self.inventoryPageIndex = page self.inventoryTab[1-page].SetUp() @@ -709,6 +802,18 @@ class InventoryWindow(ui.ScriptWindow): itemSlotIndex = self.__InventoryLocalSlotPosToGlobalSlotPos(itemSlotIndex) + if self.sashAbsorbSlot != -1 and not mouseModule.mouseController.isAttached(): + if itemSlotIndex == self.sashAbsorbSlot: + self.__AbortSashAbsorb() + return + + if self.__CanAbsorbIntoSash(self.sashAbsorbSlot, itemSlotIndex): + self.__OpenSashAbsorbQuestion(self.sashAbsorbSlot, itemSlotIndex) + else: + chat.AppendChat(chat.CHAT_TYPE_INFO, "Only unequipped weapon or armor items can be absorbed.") + self.__AbortSashAbsorb(False) + return + if mouseModule.mouseController.isAttached(): attachedSlotType = mouseModule.mouseController.GetAttachedType() attachedSlotPos = mouseModule.mouseController.GetAttachedSlotNumber() @@ -1205,6 +1310,11 @@ class InventoryWindow(ui.ScriptWindow): def __UseItem(self, slotIndex): ItemVNum = player.GetItemIndex(slotIndex) item.SelectItem(ItemVNum) + + if self.__IsSashItem(slotIndex): + self.__BeginSashAbsorb(slotIndex) + return + if item.IsFlag(item.ITEM_FLAG_CONFIRM_WHEN_USE): self.questionDialog = uiCommon.QuestionDialog() self.questionDialog.SetText(localeInfo.INVENTORY_REALLY_USE_ITEM) diff --git a/assets/root/uitooltip.py b/assets/root/uitooltip.py index 3ff2dc74..7cce17ce 100644 --- a/assets/root/uitooltip.py +++ b/assets/root/uitooltip.py @@ -1021,12 +1021,14 @@ class ItemToolTip(ToolTip): isCostumeItem = 0 isCostumeHair = 0 isCostumeBody = 0 + isCostumeSash = 0 if app.ENABLE_COSTUME_SYSTEM: if item.ITEM_TYPE_COSTUME == itemType: isCostumeItem = 1 isCostumeHair = item.COSTUME_TYPE_HAIR == itemSubType isCostumeBody = item.COSTUME_TYPE_BODY == itemSubType + isCostumeSash = item.COSTUME_TYPE_SASH == itemSubType #dbg.TraceError("IS_COSTUME_ITEM! body(%d) hair(%d)" % (isCostumeBody, isCostumeHair)) @@ -1124,6 +1126,15 @@ class ItemToolTip(ToolTip): self.__AppendAttributeInformation(attrSlot) self.AppendWearableInformation() + + if isCostumeSash: + self.AppendSpace(5) + absorbPct = metinSlot[1] if metinSlot and metinSlot[1] else item.GetValue(0) + self.AppendTextLine("Absorb rate: %d%%" % absorbPct, self.NORMAL_COLOR) + if metinSlot and metinSlot[0]: + self.AppendTextLine("Absorbed item vnum: %d" % metinSlot[0], self.NORMAL_COLOR) + else: + self.AppendTextLine("Right-click the sash, then select an item to absorb.", self.CONDITION_COLOR) bHasRealtimeFlag = 0 for i in range(item.LIMIT_MAX_NUM): diff --git a/assets/uiscript/uiscript/costumewindow.py b/assets/uiscript/uiscript/costumewindow.py index 5ff7ce0b..71a27652 100644 --- a/assets/uiscript/uiscript/costumewindow.py +++ b/assets/uiscript/uiscript/costumewindow.py @@ -1,8 +1,6 @@ import uiScriptLocale import item -COSTUME_START_INDEX = item.COSTUME_SLOT_START - window = { "name" : "CostumeWindow", @@ -71,9 +69,9 @@ window = { "height" : 145, "slot" : ( - {"index":COSTUME_START_INDEX+0, "x":61, "y":45, "width":32, "height":64}, - {"index":COSTUME_START_INDEX+1, "x":61, "y": 8, "width":32, "height":32}, - {"index":COSTUME_START_INDEX+2, "x":5, "y":145, "width":32, "height":32}, + {"index":item.COSTUME_SLOT_BODY, "x":61, "y":45, "width":32, "height":64}, + {"index":item.COSTUME_SLOT_HAIR, "x":61, "y": 8, "width":32, "height":32}, + {"index":item.COSTUME_SLOT_SASH, "x":5, "y":145, "width":32, "height":32}, ), }, ), diff --git a/assets/uiscript/uiscript/equipmentdialog.py b/assets/uiscript/uiscript/equipmentdialog.py index 8089642f..f09c1c4b 100644 --- a/assets/uiscript/uiscript/equipmentdialog.py +++ b/assets/uiscript/uiscript/equipmentdialog.py @@ -69,6 +69,7 @@ window = { ##{"index":22, "x":75, "y":106, "width":32, "height":32}, ## 새 벨트 {"index":23, "x":39, "y":106, "width":32, "height":32}, + {"index":24, "x":2, "y":106, "width":32, "height":32}, ), },