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

@@ -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
}

View File

@@ -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 {

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),
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
}
}