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
This commit is contained in:
Arto Simonyan
2025-03-04 01:09:22 +02:00
committed by GitHub
parent 4a7e782343
commit 844439002c
3 changed files with 118 additions and 117 deletions

View File

@@ -39,19 +39,13 @@ type Data struct {
AdjacentLevels []Level AdjacentLevels []Level
Rooms []Room Rooms []Room
OpenMenus OpenMenus OpenMenus OpenMenus
Widgets map[string]map[string]interface{}
Panels []Panel
Roster Roster Roster Roster
HoverData HoverData HoverData HoverData
TerrorZones []area.ID TerrorZones []area.ID
Quests quest.Quests Quests quest.Quests
KeyBindings KeyBindings KeyBindings KeyBindings
LegacyGraphics bool LegacyGraphics bool
IsOnline bool
IsIngame bool IsIngame bool
IsInCharCreationScreen bool
IsInCharSelectionScreen bool
IsInLobby bool
HasMerc bool HasMerc bool
ActiveWeaponSlot int ActiveWeaponSlot int
} }
@@ -85,7 +79,7 @@ type Panel struct {
ExtraText2 string ExtraText2 string
ExtraText3 string ExtraText3 string
PanelParent string PanelParent string
PanelChildren []Panel PanelChildren map[string]Panel
Depth int Depth int
} }

View File

@@ -1,6 +1,7 @@
package memory package memory
import ( import (
"fmt"
"math" "math"
"time" "time"
@@ -105,19 +106,19 @@ func (gd *GameReader) GetData() data.Data {
Objects: objects, Objects: objects,
Entrances: gd.Entrances(pu.Position, hover), Entrances: gd.Entrances(pu.Position, hover),
OpenMenus: openMenus, OpenMenus: openMenus,
Widgets: gd.UpdateWidgets(),
Panels: gd.ReadAllPanels(),
Roster: roster, Roster: roster,
HoverData: hover, HoverData: hover,
TerrorZones: gd.TerrorZones(), TerrorZones: gd.TerrorZones(),
Quests: gd.getQuests(gameQuestsBytes), Quests: gd.getQuests(gameQuestsBytes),
KeyBindings: gd.GetKeyBindings(), KeyBindings: gd.GetKeyBindings(),
LegacyGraphics: gd.LegacyGraphics(), LegacyGraphics: gd.LegacyGraphics(),
IsOnline: gd.IsOnline(),
IsIngame: gd.IsIngame(), IsIngame: gd.IsIngame(),
IsInCharCreationScreen: gd.IsInCharacterCreationScreen(),
IsInLobby: gd.IsInLobby(), // These use the Panel Manager which is heavy to read. Use the functions below instead.
IsInCharSelectionScreen: gd.IsInCharacterSelectionScreen(), //IsOnline: gd.IsOnline(),
//IsInCharCreationScreen: gd.IsInCharacterCreationScreen(),
//IsInLobby: gd.IsInLobby(),
//IsInCharSelectionScreen: gd.IsInCharacterSelectionScreen(),
HasMerc: gd.HasMerc(), HasMerc: gd.HasMerc(),
ActiveWeaponSlot: gd.GetActiveWeaponSlot(), ActiveWeaponSlot: gd.GetActiveWeaponSlot(),
} }
@@ -252,12 +253,39 @@ func (gd *GameReader) getStatsList(statListPtr uintptr) stat.Stats {
return 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 // GetPanel returns a Panel object from the specified path (starting from the root panel)
func (gd *GameReader) InCharacterSelectionScreen() bool { func (gd *GameReader) GetPanel(panelPath ...string) data.Panel {
cs_visible, err := gd.IsWidgetVisible("CharacterSelectPanel") if len(panelPath) == 0 {
if err != nil { 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 { func (gd *GameReader) GetSelectedCharacterName() string {
@@ -269,8 +297,8 @@ func (gd *GameReader) LegacyGraphics() bool {
} }
func (gd *GameReader) IsOnline() bool { func (gd *GameReader) IsOnline() bool {
// This represents which tab (Online/Offline) we're on in the Character Selection Screen panel := gd.GetPanel("MainMenuPanel", "SecondaryContextButton")
return gd.ReadUInt(gd.moduleBaseAddressPtr+0x21864D0, 1) == 1 return panel.PanelName != "" && panel.PanelEnabled && panel.PanelVisible
} }
func (gd *GameReader) IsIngame() bool { func (gd *GameReader) IsIngame() bool {
@@ -278,30 +306,45 @@ func (gd *GameReader) IsIngame() bool {
} }
func (gd *GameReader) IsInLobby() bool { func (gd *GameReader) IsInLobby() bool {
widgets := gd.UpdateWidgets() panel := gd.GetPanel("LobbyBackgroundPanel")
if lobbyWidget, found := widgets["LobbyBackgroundPanel"]; found { return panel.PanelName != "" && panel.PanelEnabled && panel.PanelVisible
return lobbyWidget["WidgetActive"].(bool) && lobbyWidget["WidgetVisible"].(bool)
}
return false
} }
func (gd *GameReader) IsInCharacterSelectionScreen() bool { func (gd *GameReader) IsInCharacterSelectionScreen() bool {
widgets := gd.UpdateWidgets() panel := gd.GetPanel("CharacterSelectPanel")
if csWidget, found := widgets["CharacterSelectPanel"]; found { return panel.PanelName != "" && panel.PanelEnabled && panel.PanelVisible
return csWidget["WidgetActive"].(bool) && csWidget["WidgetVisible"].(bool)
}
return false
} }
func (gd *GameReader) IsInCharacterCreationScreen() bool { func (gd *GameReader) IsInCharacterCreationScreen() bool {
widgets := gd.UpdateWidgets() panel := gd.GetPanel("CharacterCreatePanel")
if ccWidget, found := widgets["CharacterCreatePanel"]; found { return panel.PanelName != "" && panel.PanelEnabled && panel.PanelVisible
return ccWidget["WidgetActive"].(bool) && ccWidget["WidgetVisible"].(bool) }
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 { func (gd *GameReader) LastGameName() string {
@@ -319,44 +362,6 @@ func (gd *GameReader) FPS() int {
func (gd *GameReader) HasMerc() bool { func (gd *GameReader) HasMerc() bool {
return gd.ReadUInt(gd.moduleBaseAddressPtr+0x22e51d0+0x12, Uint8) != 0 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 // GetWidgetState reference : https://github.com/ResurrectedTrader/ResurrectedTrade/blob/f121ec02dd3fbe1c574f713e5a0c2db92ccca821/ResurrectedTrade.AgentBase/Capture.cs#L618
func (gd *GameReader) GetWidgetState(stateFlag uint64) (int, error) { func (gd *GameReader) GetWidgetState(stateFlag uint64) (int, error) {
@@ -394,6 +399,7 @@ func (gd *GameReader) GetWidgetState(stateFlag uint64) (int, error) {
return 0, nil return 0, nil
} }
func (gd *GameReader) GetActiveWeaponSlot() int { func (gd *GameReader) GetActiveWeaponSlot() int {
state, err := gd.GetWidgetState(WidgetStateFlags["WeaponSwap"]) state, err := gd.GetWidgetState(WidgetStateFlags["WeaponSwap"])
if err != nil { if err != nil {

View File

@@ -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), ExtraText2: gd.Process.ReadStringFromMemory(uintptr(gd.Process.ReadUInt(panelPtr+0x290, Uint64)), 0),
ExtraText3: gd.Process.ReadStringFromMemory(uintptr(gd.Process.ReadUInt(panelPtr+0x88, Uint64)), 0), ExtraText3: gd.Process.ReadStringFromMemory(uintptr(gd.Process.ReadUInt(panelPtr+0x88, Uint64)), 0),
PanelParent: panelParent, PanelParent: panelParent,
PanelChildren: make([]data.Panel, 0), PanelChildren: make(map[string]data.Panel),
Depth: depth, Depth: depth,
} }
if panel.NumChildren > 0 && panel.NumChildren < 50 { if panel.NumChildren > 0 && panel.NumChildren < 50 {
@@ -55,23 +55,24 @@ func isASCII(s string) bool {
return true 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 base := gd.Process.moduleBaseAddressPtr + gd.offset.PanelManagerContainerOffset
panelStructPtr := uintptr(gd.Process.ReadUInt(base, Uint64)) panelStructPtr := uintptr(gd.Process.ReadUInt(base, Uint64))
panelPtr := uintptr(gd.Process.ReadUInt(panelStructPtr+0x58, Uint64)) panelPtr := uintptr(gd.Process.ReadUInt(panelStructPtr+0x58, Uint64))
numChildren := int(gd.Process.ReadUInt(panelStructPtr+0x60, Uint8)) numChildren := int(gd.Process.ReadUInt(panelStructPtr+0x60, Uint8))
panels := make([]data.Panel, 0) panels := make(map[string]data.Panel)
depth := 0 depth := 0
// recursively read all panels, starting with the Root panel // recursively read all panels, starting with the Root panel
readPanel(panelPtr, numChildren, &panels, "Root", depth, gd) readPanel(panelPtr, numChildren, &panels, "Root", depth, gd)
return panels 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++ { for i := 0; i < numChildren; i++ {
panelStructPtr := uintptr(gd.Process.ReadUInt(uintptr(uint64(panelPtr)+uint64(i*8)), Uint64)) panelStructPtr := uintptr(gd.Process.ReadUInt(uintptr(uint64(panelPtr)+uint64(i*8)), Uint64))
thisPanel := NewPanel(panelStructPtr, panelParent, depth, gd) thisPanel := NewPanel(panelStructPtr, panelParent, depth, gd)
*panels = append(*panels, *thisPanel) (*panels)[thisPanel.PanelName] = *thisPanel
} }
} }