diff --git a/pkg/data/data.go b/pkg/data/data.go index a913b91..c1dfbd0 100644 --- a/pkg/data/data.go +++ b/pkg/data/data.go @@ -38,6 +38,7 @@ type Data struct { AdjacentLevels []Level Rooms []Room OpenMenus OpenMenus + Widgets map[string]map[string]interface{} Roster Roster HoverData HoverData TerrorZones []area.ID diff --git a/pkg/memory/game_reader.go b/pkg/memory/game_reader.go index 573da1f..36cda60 100644 --- a/pkg/memory/game_reader.go +++ b/pkg/memory/game_reader.go @@ -56,6 +56,7 @@ func (gd *GameReader) GetData() data.Data { Inventory: gd.Inventory(rawPlayerUnits, hover), Objects: gd.Objects(pu.Position, hover), OpenMenus: gd.openMenus(), + Widgets: gd.UpdateWidgets(), Roster: roster, HoverData: hover, TerrorZones: gd.TerrorZones(), @@ -191,9 +192,10 @@ func (gd *GameReader) getStatsList(statListPtr uintptr) stat.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 { - uiBase := gd.Process.moduleBaseAddressPtr + gd.offset.UI - 0xA - - return gd.Process.ReadUInt(uiBase, Uint8) != 1 && gd.Process.ReadUInt(gd.moduleBaseAddressPtr+2136526, Uint8) == 0 + cs_visible, err := gd.IsWidgetVisible("CharacterSelectPanel") + if err != nil { + } + return cs_visible } func (gd *GameReader) GetSelectedCharacterName() string { @@ -224,6 +226,7 @@ func (gd *GameReader) IsInCharacterSelectionScreen() bool { */ func (gd *GameReader) IsInCharacterCreationScreen() bool { + // Dont use this ;) // This will bug out if you switch to legacy graphics in the character select screen and return 1 until you go back to character screen with d2r graphics return gd.ReadUInt(gd.moduleBaseAddressPtr+0x234A1CE, 1) == 1 } @@ -239,3 +242,42 @@ func (gd *GameReader) LastGamePass() string { func (gd *GameReader) FPS() int { return int(gd.ReadUInt(gd.moduleBaseAddressPtr+0x2140DF4, 4)) } + +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 +} diff --git a/pkg/memory/offset.go b/pkg/memory/offset.go index 1ea762f..671b9fa 100644 --- a/pkg/memory/offset.go +++ b/pkg/memory/offset.go @@ -1,14 +1,17 @@ package memory -import "encoding/binary" +import ( + "encoding/binary" +) type Offset struct { - GameData uintptr - UnitTable uintptr - UI uintptr - Hover uintptr - Expansion uintptr - RosterOffset uintptr + GameData uintptr + UnitTable uintptr + UI uintptr + Hover uintptr + Expansion uintptr + RosterOffset uintptr + PanelManagerContainerOffset uintptr } func calculateOffsets(process Process) Offset { @@ -45,12 +48,18 @@ func calculateOffsets(process Process) Offset { offsetPtr = uintptr(process.ReadUInt(pattern-3, Uint32)) rosterOffset := pattern - process.moduleBaseAddressPtr + 1 + offsetPtr + // PanelManagerContainer + pattern = process.FindPatternByOperand(memory, "\x48\x89\x05\x00\x00\x00\x00\x48\x85\xDB\x74\x1E", "xxx????xxxxx") + bytes = process.ReadBytesFromMemory(pattern, 8) + panelManagerContainerOffset := (pattern - process.moduleBaseAddressPtr) // uintptr(binary.LittleEndian.Uint64(bytes)) + return Offset{ - GameData: gameDataOffset, - UnitTable: unitTableOffset, - UI: uiOffsetPtr, - Hover: uintptr(hoverOffset), - Expansion: expOffset, - RosterOffset: rosterOffset, + GameData: gameDataOffset, + UnitTable: unitTableOffset, + UI: uiOffsetPtr, + Hover: uintptr(hoverOffset), + Expansion: expOffset, + RosterOffset: rosterOffset, + PanelManagerContainerOffset: panelManagerContainerOffset, } } diff --git a/pkg/memory/process.go b/pkg/memory/process.go index 63a87c3..a55dfa3 100644 --- a/pkg/memory/process.go +++ b/pkg/memory/process.go @@ -185,6 +185,18 @@ func (p Process) FindPattern(memory []byte, pattern, mask string) uintptr { return 0 } +func (p Process) FindPatternByOperand(memory []byte, pattern, mask string) uintptr { + if offset := p.findPattern(memory, pattern, mask); offset != 0 { + // Adjust the address based on the operand value + operandAddress := p.moduleBaseAddressPtr + uintptr(offset) + operandValue := binary.LittleEndian.Uint32(memory[offset+3 : offset+7]) + finalAddress := operandAddress + uintptr(operandValue) + 7 // 7 is the length of the instruction + return finalAddress + } + + return 0 +} + func (p Process) GetPID() uint32 { return p.pid } @@ -234,3 +246,97 @@ func GetProcessModules(processID uint32) ([]ModuleInfo, error) { return moduleInfos, nil } + +// ReadPointer reads a pointer from the specified memory address. +func (p *Process) ReadPointer(address uintptr, size int) (uintptr, error) { + buffer := p.ReadBytesFromMemory(address, uint(size)) + if len(buffer) == 0 { + return 0, errors.New("failed to read memory") + } + + return uintptr(*(*uint64)(unsafe.Pointer(&buffer[0]))), nil +} + +// ReadWidgetContainer reads the WidgetContainer structure. +func (p *Process) ReadWidgetContainer(address uintptr, full bool) (map[string]interface{}, error) { + widgetPtr, err := p.ReadPointer(address+0x8, 8) + if err != nil { + return nil, err + } + + widgetNameLength := p.ReadUInt(address+0x10, 4) + + widgetName := p.ReadStringFromMemory(widgetPtr, uint(widgetNameLength)) + if widgetName == "" { + return nil, errors.New("failed to read widget name") + } + + widget_visible := p.ReadUInt(address+0x51, 1) == 1 + widget_active := p.ReadUInt(address+0x50, 1) == 1 + + result := map[string]interface{}{ + "WidgetNameString": widgetName, + "WidgetNameLength": widgetNameLength, + "WidgetVisible": widget_visible, + "WidgetActive": widget_active, + } + + if full { + childWidgetsListPtr, err := p.ReadPointer(widgetPtr+0x38, 8) + if err != nil { + return nil, err + } + + childWidgetSize := p.ReadUInt(widgetPtr+0x40, 4) + + widgetListPtr, err := p.ReadPointer(widgetPtr+0x68, 8) + if err != nil { + return nil, err + } + + widgetListSize := p.ReadUInt(widgetPtr+0x78, 4) + + widgetList2Ptr, err := p.ReadPointer(widgetPtr+0x80, 8) + if err != nil { + return nil, err + } + + widgetList2Size := p.ReadUInt(widgetPtr+0x90, 4) + + result["ChildWidgetsListPointer"] = childWidgetsListPtr + result["ChildWidgetSize"] = childWidgetSize + result["WidgetListPointer"] = widgetListPtr + result["WidgetListSize"] = widgetListSize + result["WidgetList2Pointer"] = widgetList2Ptr + result["WidgetList2Size"] = widgetList2Size + } + + return result, nil +} + +// ReadWidgetList iterates through a list of widgets given a pointer to the list and its size. +func (p *Process) ReadWidgetList(listPointer uintptr, listSize int) (map[string]map[string]interface{}, error) { + widgetMap := make(map[string]map[string]interface{}) + widgetSize := int(unsafe.Sizeof(uintptr(0))) + + for i := 0; i < listSize; i++ { + widgetAddr, err := p.ReadPointer(listPointer+uintptr(i*widgetSize), 8) + if err != nil { + return nil, err + } + + widgetContainer, err := p.ReadWidgetContainer(widgetAddr, false) + if err != nil { + return nil, err + } + + widgetName, ok := widgetContainer["WidgetNameString"].(string) + if !ok { + return nil, errors.New("failed to read widget name") + } + + widgetMap[widgetName] = widgetContainer + } + + return widgetMap, nil +}