From 844439002c089db2622f7ebc050d3747ee2333ed Mon Sep 17 00:00:00 2001 From: Arto Simonyan Date: Tue, 4 Mar 2025 01:09:22 +0200 Subject: [PATCH] Panel Manager Improvements (#85) * Panel Manager Improvements - Now maps instead of slices for easy traverse - Function for easy and safe traverse of paths - Removed constant updates of Panel Manager to reduce usage. It will now be called on demand. - Updated some functions to use the new Panel Manager. * Remove unused struct members * Added IsDismissalModalPresent (error popup) * Correct function return for char select screen and dismissal modal * Add error checking to IsDismissableModalPresent --- pkg/data/data.go | 52 +++++------- pkg/memory/game_reader.go | 172 ++++++++++++++++++++------------------ pkg/memory/panels.go | 11 +-- 3 files changed, 118 insertions(+), 117 deletions(-) diff --git a/pkg/data/data.go b/pkg/data/data.go index b662f05..203fc7c 100644 --- a/pkg/data/data.go +++ b/pkg/data/data.go @@ -26,34 +26,28 @@ const ( ) type Data struct { - AreaOrigin Position - Corpse Corpse - Monsters Monsters - Corpses Monsters - Game OnlineGame - PlayerUnit PlayerUnit - NPCs NPCs - Inventory Inventory - Objects Objects - Entrances Entrances - AdjacentLevels []Level - Rooms []Room - OpenMenus OpenMenus - Widgets map[string]map[string]interface{} - Panels []Panel - Roster Roster - HoverData HoverData - TerrorZones []area.ID - Quests quest.Quests - KeyBindings KeyBindings - LegacyGraphics bool - IsOnline bool - IsIngame bool - IsInCharCreationScreen bool - IsInCharSelectionScreen bool - IsInLobby bool - HasMerc bool - ActiveWeaponSlot int + AreaOrigin Position + Corpse Corpse + Monsters Monsters + Corpses Monsters + Game OnlineGame + PlayerUnit PlayerUnit + NPCs NPCs + Inventory Inventory + Objects Objects + Entrances Entrances + AdjacentLevels []Level + Rooms []Room + OpenMenus OpenMenus + Roster Roster + HoverData HoverData + TerrorZones []area.ID + Quests quest.Quests + KeyBindings KeyBindings + LegacyGraphics bool + IsIngame bool + HasMerc bool + ActiveWeaponSlot int } type Room struct { @@ -85,7 +79,7 @@ type Panel struct { ExtraText2 string ExtraText3 string PanelParent string - PanelChildren []Panel + PanelChildren map[string]Panel Depth int } diff --git a/pkg/memory/game_reader.go b/pkg/memory/game_reader.go index b92ab0a..b122d33 100644 --- a/pkg/memory/game_reader.go +++ b/pkg/memory/game_reader.go @@ -1,6 +1,7 @@ package memory import ( + "fmt" "math" "time" @@ -98,28 +99,28 @@ func (gd *GameReader) GetData() data.Data { LastGamePassword: gd.LastGamePass(), FPS: gd.FPS(), }, - Monsters: monsters, - Corpses: gd.Corpses(pu.Position, hover), - PlayerUnit: pu, - Inventory: inventory, - Objects: objects, - Entrances: gd.Entrances(pu.Position, hover), - OpenMenus: openMenus, - Widgets: gd.UpdateWidgets(), - Panels: gd.ReadAllPanels(), - Roster: roster, - HoverData: hover, - TerrorZones: gd.TerrorZones(), - Quests: gd.getQuests(gameQuestsBytes), - KeyBindings: gd.GetKeyBindings(), - LegacyGraphics: gd.LegacyGraphics(), - IsOnline: gd.IsOnline(), - IsIngame: gd.IsIngame(), - IsInCharCreationScreen: gd.IsInCharacterCreationScreen(), - IsInLobby: gd.IsInLobby(), - IsInCharSelectionScreen: gd.IsInCharacterSelectionScreen(), - HasMerc: gd.HasMerc(), - ActiveWeaponSlot: gd.GetActiveWeaponSlot(), + Monsters: monsters, + Corpses: gd.Corpses(pu.Position, hover), + PlayerUnit: pu, + Inventory: inventory, + Objects: objects, + Entrances: gd.Entrances(pu.Position, hover), + OpenMenus: openMenus, + Roster: roster, + HoverData: hover, + TerrorZones: gd.TerrorZones(), + Quests: gd.getQuests(gameQuestsBytes), + KeyBindings: gd.GetKeyBindings(), + LegacyGraphics: gd.LegacyGraphics(), + IsIngame: gd.IsIngame(), + + // These use the Panel Manager which is heavy to read. Use the functions below instead. + //IsOnline: gd.IsOnline(), + //IsInCharCreationScreen: gd.IsInCharacterCreationScreen(), + //IsInLobby: gd.IsInLobby(), + //IsInCharSelectionScreen: gd.IsInCharacterSelectionScreen(), + HasMerc: gd.HasMerc(), + ActiveWeaponSlot: gd.GetActiveWeaponSlot(), } return d @@ -252,12 +253,39 @@ func (gd *GameReader) getStatsList(statListPtr uintptr) stat.Stats { return stats } -// TODO: Take a look to better ways to get this data, now it's very flakky, is just a random memory position + not in game -func (gd *GameReader) InCharacterSelectionScreen() bool { - cs_visible, err := gd.IsWidgetVisible("CharacterSelectPanel") - if err != nil { +// GetPanel returns a Panel object from the specified path (starting from the root panel) +func (gd *GameReader) GetPanel(panelPath ...string) data.Panel { + if len(panelPath) == 0 { + return data.Panel{} } - return cs_visible + + // Get all panels + allPanels := gd.ReadAllPanels() + + // Start with the first panel in the path + firstPanelName := panelPath[0] + currentPanel, exists := allPanels[firstPanelName] + if !exists { + // Panel not found at top level + return data.Panel{} + } + + // Traverse the path from left to right + for i := 1; i < len(panelPath); i++ { + childName := panelPath[i] + nextPanel, exists := currentPanel.PanelChildren[childName] + if !exists { + return data.Panel{} + } + currentPanel = nextPanel + } + + return currentPanel +} + +func (gd *GameReader) InCharacterSelectionScreen() bool { + panel := gd.GetPanel("CharacterSelectPanel") + return panel.PanelName != "" && panel.PanelEnabled && panel.PanelVisible } func (gd *GameReader) GetSelectedCharacterName() string { @@ -269,8 +297,8 @@ func (gd *GameReader) LegacyGraphics() bool { } func (gd *GameReader) IsOnline() bool { - // This represents which tab (Online/Offline) we're on in the Character Selection Screen - return gd.ReadUInt(gd.moduleBaseAddressPtr+0x21864D0, 1) == 1 + panel := gd.GetPanel("MainMenuPanel", "SecondaryContextButton") + return panel.PanelName != "" && panel.PanelEnabled && panel.PanelVisible } func (gd *GameReader) IsIngame() bool { @@ -278,30 +306,45 @@ func (gd *GameReader) IsIngame() bool { } func (gd *GameReader) IsInLobby() bool { - widgets := gd.UpdateWidgets() - if lobbyWidget, found := widgets["LobbyBackgroundPanel"]; found { - return lobbyWidget["WidgetActive"].(bool) && lobbyWidget["WidgetVisible"].(bool) - } - - return false + panel := gd.GetPanel("LobbyBackgroundPanel") + return panel.PanelName != "" && panel.PanelEnabled && panel.PanelVisible } func (gd *GameReader) IsInCharacterSelectionScreen() bool { - widgets := gd.UpdateWidgets() - if csWidget, found := widgets["CharacterSelectPanel"]; found { - return csWidget["WidgetActive"].(bool) && csWidget["WidgetVisible"].(bool) - } - - return false + panel := gd.GetPanel("CharacterSelectPanel") + return panel.PanelName != "" && panel.PanelEnabled && panel.PanelVisible } func (gd *GameReader) IsInCharacterCreationScreen() bool { - widgets := gd.UpdateWidgets() - if ccWidget, found := widgets["CharacterCreatePanel"]; found { - return ccWidget["WidgetActive"].(bool) && ccWidget["WidgetVisible"].(bool) + panel := gd.GetPanel("CharacterCreatePanel") + return panel.PanelName != "" && panel.PanelEnabled && panel.PanelVisible +} + +func (gd *GameReader) GetCharacterList() []string { + containerPanel := gd.GetPanel("CharacterSelectPanel", "Background", "CharacterList", "View", "Container") + if containerPanel.PanelName == "" || containerPanel.NumChildren == 0 { + return []string{} } - return false + // Get the character names that are in the container children [ListView 0,1,2 (0 indexed)] -> children -> Name -> Extra Text 3 + characterNames := make([]string, containerPanel.NumChildren) + for i := 0; i < containerPanel.NumChildren; i++ { + characterNames[i] = containerPanel.PanelChildren[fmt.Sprintf("ListItem%d", i)].PanelChildren["Name"].ExtraText3 + } + + return characterNames +} + +// IsDismissableModalPresent checks if there's a error popup present +func (gd *GameReader) IsDismissableModalPresent() (bool, string) { + panel := gd.GetPanel("DismissableModal") + + if panel.PanelName == "" { + return false, "" + } + + modalText := panel.PanelChildren["Frame"].PanelChildren["Prompt"].ExtraText3 + return (panel.PanelName != "" && panel.PanelEnabled && panel.PanelVisible), modalText } func (gd *GameReader) LastGameName() string { @@ -319,44 +362,6 @@ func (gd *GameReader) FPS() int { func (gd *GameReader) HasMerc() bool { return gd.ReadUInt(gd.moduleBaseAddressPtr+0x22e51d0+0x12, Uint8) != 0 } -func (gd *GameReader) UpdateWidgets() map[string]map[string]interface{} { - widgets := map[string]map[string]interface{}{} - - if gd.offset.PanelManagerContainerOffset == 0 { - gd.offset = calculateOffsets(gd.Process) - } - - // Assuming PanelManagerContainer address is known and stored in gd - panelManagerContainerPtrAddr := gd.Process.moduleBaseAddressPtr + gd.offset.PanelManagerContainerOffset - - // Read the PanelManagerContainer pointer value - panelManagerContainerAddr, err := gd.Process.ReadPointer(panelManagerContainerPtrAddr, 8) - if err != nil { - return widgets - } - // Read the Panel Managers WidgetContainer - widgetContainer, err := gd.Process.ReadWidgetContainer(panelManagerContainerAddr, true) - if err != nil { - return widgets - } - // Read the list of child widgets - childWidgets, err := gd.Process.ReadWidgetList(widgetContainer["ChildWidgetsListPointer"].(uintptr), int(widgetContainer["ChildWidgetSize"].(uint))) - if err != nil { - return widgets - } - return childWidgets -} - -// IsWidgetVisible checks if any child widget on the PanelManager has the same name and has the Active and Visible booleans on. -func (gd *GameReader) IsWidgetVisible(widgetName string) (bool, error) { - widgets := gd.UpdateWidgets() - widget, exists := widgets[widgetName] - if !exists { - return false, nil - } - - return widget["WidgetActive"].(bool) && widget["WidgetVisible"].(bool), nil -} // GetWidgetState reference : https://github.com/ResurrectedTrader/ResurrectedTrade/blob/f121ec02dd3fbe1c574f713e5a0c2db92ccca821/ResurrectedTrade.AgentBase/Capture.cs#L618 func (gd *GameReader) GetWidgetState(stateFlag uint64) (int, error) { @@ -394,6 +399,7 @@ func (gd *GameReader) GetWidgetState(stateFlag uint64) (int, error) { return 0, nil } + func (gd *GameReader) GetActiveWeaponSlot() int { state, err := gd.GetWidgetState(WidgetStateFlags["WeaponSwap"]) if err != nil { diff --git a/pkg/memory/panels.go b/pkg/memory/panels.go index d220b65..9ceda88 100644 --- a/pkg/memory/panels.go +++ b/pkg/memory/panels.go @@ -16,7 +16,7 @@ func NewPanel(panelPtr uintptr, panelParent string, depth int, gd *GameReader) * ExtraText2: gd.Process.ReadStringFromMemory(uintptr(gd.Process.ReadUInt(panelPtr+0x290, Uint64)), 0), ExtraText3: gd.Process.ReadStringFromMemory(uintptr(gd.Process.ReadUInt(panelPtr+0x88, Uint64)), 0), PanelParent: panelParent, - PanelChildren: make([]data.Panel, 0), + PanelChildren: make(map[string]data.Panel), Depth: depth, } if panel.NumChildren > 0 && panel.NumChildren < 50 { @@ -55,23 +55,24 @@ func isASCII(s string) bool { return true } -func (gd *GameReader) ReadAllPanels() []data.Panel { +// ReadAllPanels reads all panels from the game memory +func (gd *GameReader) ReadAllPanels() map[string]data.Panel { base := gd.Process.moduleBaseAddressPtr + gd.offset.PanelManagerContainerOffset panelStructPtr := uintptr(gd.Process.ReadUInt(base, Uint64)) panelPtr := uintptr(gd.Process.ReadUInt(panelStructPtr+0x58, Uint64)) numChildren := int(gd.Process.ReadUInt(panelStructPtr+0x60, Uint8)) - panels := make([]data.Panel, 0) + panels := make(map[string]data.Panel) depth := 0 // recursively read all panels, starting with the Root panel readPanel(panelPtr, numChildren, &panels, "Root", depth, gd) return panels } -func readPanel(panelPtr uintptr, numChildren int, panels *[]data.Panel, panelParent string, depth int, gd *GameReader) { +func readPanel(panelPtr uintptr, numChildren int, panels *map[string]data.Panel, panelParent string, depth int, gd *GameReader) { for i := 0; i < numChildren; i++ { panelStructPtr := uintptr(gd.Process.ReadUInt(uintptr(uint64(panelPtr)+uint64(i*8)), Uint64)) thisPanel := NewPanel(panelStructPtr, panelParent, depth, gd) - *panels = append(*panels, *thisPanel) + (*panels)[thisPanel.PanelName] = *thisPanel } }