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