283 lines
8.7 KiB
Go
283 lines
8.7 KiB
Go
package memory
|
|
|
|
import (
|
|
"bytes"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/hectorgimenez/d2go/pkg/data"
|
|
)
|
|
|
|
var tooltipRenderPriceSnippet = regexp.MustCompile(`[\x20-\x7e\n]{0,512}(?:Sell value|Cost|Price):?\s*\d[\d,.]*[\x20-\x7e\n]{0,160}`)
|
|
|
|
func NewPanel(panelPtr uintptr, panelParent string, depth int, gd *GameReader) *data.Panel { //itemUnitPtr = uintptr(gd.Process.ReadUInt(itemUnitPtr+0x158, Uint64))
|
|
panel := &data.Panel{
|
|
PanelPtr: panelPtr,
|
|
PanelName: gd.Process.ReadStringFromMemory(uintptr(gd.Process.ReadUInt(panelPtr+0x08, Uint64)), 0),
|
|
PanelEnabled: gd.Process.ReadUInt(panelPtr+0x50, Uint8) != 0,
|
|
PanelVisible: gd.Process.ReadUInt(panelPtr+0x51, Uint8) != 0,
|
|
PtrChild: uintptr(gd.Process.ReadUInt(panelPtr+0x58, Uint64)),
|
|
NumChildren: int(gd.Process.ReadUInt(panelPtr+0x60, Uint8)),
|
|
ExtraText: gd.Process.ReadStringFromMemory(panelPtr+0xA0, 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),
|
|
PanelParent: panelParent,
|
|
PanelChildren: make(map[string]data.Panel),
|
|
Depth: depth,
|
|
}
|
|
if panel.NumChildren > 0 && panel.NumChildren < 50 {
|
|
readPanel(panel.PtrChild, panel.NumChildren, &panel.PanelChildren, panel.PanelName, depth+1, gd)
|
|
}
|
|
return panel
|
|
}
|
|
|
|
func GetText(p data.Panel) string {
|
|
text1 := cleanString(p.ExtraText)
|
|
text2 := cleanString(p.ExtraText2)
|
|
text3 := cleanString(p.ExtraText3)
|
|
|
|
if text3 != "" && isASCII(text3) {
|
|
return text3
|
|
}
|
|
if text2 != "" && isASCII(text2) {
|
|
return text2
|
|
}
|
|
if text1 != "" && isASCII(text1) {
|
|
return text1
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// VisiblePanelTexts returns readable text from every visible panel in traversal
|
|
// order. Tooltip text is assembled by the client after hover, so callers can
|
|
// use this as a live UI source instead of recalculating item values.
|
|
func (gd *GameReader) VisiblePanelTexts() []string {
|
|
if gd == nil || gd.Process == nil {
|
|
return nil
|
|
}
|
|
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))
|
|
|
|
out := make([]string, 0, 64)
|
|
seen := make(map[string]struct{}, 64)
|
|
collectVisiblePanelTexts(panelPtr, numChildren, &out, seen, gd)
|
|
return out
|
|
}
|
|
|
|
// TooltipRenderTextsFast scans render/cache pages anchored by visible panel
|
|
// child arrays. The client keeps item tooltip strings here even when the
|
|
// regular panel tree has no readable children.
|
|
func (gd *GameReader) TooltipRenderTextsFast() []string {
|
|
return gd.tooltipRenderTexts()
|
|
}
|
|
|
|
func (gd *GameReader) tooltipRenderTexts() []string {
|
|
if gd == nil || gd.Process == nil {
|
|
return nil
|
|
}
|
|
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))
|
|
|
|
childPages := make(map[uintptr]struct{}, 64)
|
|
collectPanelChildPages(panelPtr, numChildren, childPages, gd)
|
|
|
|
scanPages := make(map[uintptr]struct{}, 256)
|
|
for page := range childPages {
|
|
for _, delta := range []int{-0x2000, -0x1000, 0, 0x1000, 0x2000} {
|
|
if addr := pageFromDelta(page, delta); addr != 0 {
|
|
scanPages[addr] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
out := make([]string, 0, 16)
|
|
seen := make(map[string]struct{}, 16)
|
|
for page := range scanPages {
|
|
buf := gd.Process.ReadBytesFromMemory(page, 0x1000)
|
|
if len(buf) == 0 {
|
|
continue
|
|
}
|
|
for _, match := range tooltipRenderPriceSnippet.FindAll(buf, -1) {
|
|
text := cleanTooltipRenderSnippet(match)
|
|
if text == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[text]; ok {
|
|
continue
|
|
}
|
|
seen[text] = struct{}{}
|
|
out = append(out, text)
|
|
}
|
|
for _, pageText := range utf16LEPrintablePages(buf) {
|
|
for _, match := range tooltipRenderPriceSnippet.FindAllString(pageText, -1) {
|
|
text := cleanTooltipRenderText(match)
|
|
if text == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[text]; ok {
|
|
continue
|
|
}
|
|
seen[text] = struct{}{}
|
|
out = append(out, text)
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func collectPanelChildPages(panelPtr uintptr, numChildren int, pages map[uintptr]struct{}, gd *GameReader) {
|
|
if numChildren <= 0 || numChildren >= 80 {
|
|
return
|
|
}
|
|
for i := 0; i < numChildren; i++ {
|
|
panelStructPtr := uintptr(gd.Process.ReadUInt(uintptr(uint64(panelPtr)+uint64(i*8)), Uint64))
|
|
if panelStructPtr == 0 {
|
|
continue
|
|
}
|
|
childPtr := uintptr(gd.Process.ReadUInt(panelStructPtr+0x58, Uint64))
|
|
childCount := int(gd.Process.ReadUInt(panelStructPtr+0x60, Uint8))
|
|
if childPtr != 0 {
|
|
pages[childPtr&^uintptr(0xfff)] = struct{}{}
|
|
}
|
|
if childCount > 0 && childCount < 50 {
|
|
collectPanelChildPages(childPtr, childCount, pages, gd)
|
|
}
|
|
}
|
|
}
|
|
|
|
func pageFromDelta(page uintptr, delta int) uintptr {
|
|
addr := int64(page) + int64(delta)
|
|
if addr <= 0 {
|
|
return 0
|
|
}
|
|
return uintptr(addr) & ^uintptr(0xfff)
|
|
}
|
|
|
|
func cleanTooltipRenderSnippet(raw []byte) string {
|
|
raw = bytes.Trim(raw, "\x00")
|
|
return cleanTooltipRenderText(string(raw))
|
|
}
|
|
|
|
func cleanTooltipRenderText(raw string) string {
|
|
s := strings.TrimSpace(raw)
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
lines := strings.Split(s, "\n")
|
|
cleaned := make([]string, 0, len(lines))
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
line = strings.TrimLeft(line, "0123456789")
|
|
line = strings.TrimSpace(line)
|
|
if line != "" {
|
|
cleaned = append(cleaned, line)
|
|
}
|
|
}
|
|
return strings.Join(cleaned, "\n")
|
|
}
|
|
|
|
func utf16LEPrintablePages(buf []byte) []string {
|
|
out := make([]string, 0, 2)
|
|
for align := 0; align < 2; align++ {
|
|
var b strings.Builder
|
|
lastWasNL := true
|
|
for i := align; i+1 < len(buf); i += 2 {
|
|
lo := buf[i]
|
|
hi := buf[i+1]
|
|
if hi == 0 && (lo == '\n' || lo == '\r' || lo == '\t' || (lo >= 0x20 && lo <= 0x7e)) {
|
|
if lo == '\r' || lo == '\t' {
|
|
lo = '\n'
|
|
}
|
|
if lo == '\n' {
|
|
if !lastWasNL {
|
|
b.WriteByte('\n')
|
|
lastWasNL = true
|
|
}
|
|
continue
|
|
}
|
|
b.WriteByte(lo)
|
|
lastWasNL = false
|
|
continue
|
|
}
|
|
if !lastWasNL {
|
|
b.WriteByte('\n')
|
|
lastWasNL = true
|
|
}
|
|
}
|
|
text := strings.TrimSpace(b.String())
|
|
if len(text) >= 16 {
|
|
out = append(out, text)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func collectVisiblePanelTexts(panelPtr uintptr, numChildren int, out *[]string, seen map[string]struct{}, gd *GameReader) {
|
|
if numChildren <= 0 || numChildren >= 50 {
|
|
return
|
|
}
|
|
for i := 0; i < numChildren; i++ {
|
|
panelStructPtr := uintptr(gd.Process.ReadUInt(uintptr(uint64(panelPtr)+uint64(i*8)), Uint64))
|
|
if panelStructPtr == 0 {
|
|
continue
|
|
}
|
|
p := data.Panel{
|
|
PanelEnabled: gd.Process.ReadUInt(panelStructPtr+0x50, Uint8) != 0,
|
|
PanelVisible: gd.Process.ReadUInt(panelStructPtr+0x51, Uint8) != 0,
|
|
PtrChild: uintptr(gd.Process.ReadUInt(panelStructPtr+0x58, Uint64)),
|
|
NumChildren: int(gd.Process.ReadUInt(panelStructPtr+0x60, Uint8)),
|
|
ExtraText: gd.Process.ReadStringFromMemory(panelStructPtr+0xA0, 0),
|
|
ExtraText2: gd.Process.ReadStringFromMemory(uintptr(gd.Process.ReadUInt(panelStructPtr+0x290, Uint64)), 0),
|
|
ExtraText3: gd.Process.ReadStringFromMemory(uintptr(gd.Process.ReadUInt(panelStructPtr+0x88, Uint64)), 0),
|
|
}
|
|
if p.PanelVisible && p.PanelEnabled {
|
|
if text := strings.TrimSpace(GetText(p)); text != "" {
|
|
if _, ok := seen[text]; !ok {
|
|
seen[text] = struct{}{}
|
|
*out = append(*out, text)
|
|
}
|
|
}
|
|
collectVisiblePanelTexts(p.PtrChild, p.NumChildren, out, seen, gd)
|
|
}
|
|
}
|
|
}
|
|
|
|
func cleanString(input string) string {
|
|
return input // Replace newlines and carriage returns as needed
|
|
}
|
|
|
|
func isASCII(s string) bool {
|
|
for _, r := range s {
|
|
if r < 32 || r > 126 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// 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(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 *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)[thisPanel.PanelName] = *thisPanel
|
|
}
|
|
}
|