From 28da1b9a35619e9651e2dec65a8721298eb79ec0 Mon Sep 17 00:00:00 2001 From: vietdungdev Date: Fri, 22 May 2026 19:24:21 +0700 Subject: [PATCH] update offset --- pkg/data/state/states.go | 1 + pkg/memory/addresses.go | 2 +- pkg/memory/game_reader.go | 6 +- pkg/memory/item.go | 9 +- pkg/memory/offset.go | 40 ++++---- pkg/memory/panels.go | 204 ++++++++++++++++++++++++++++++++++++++ pkg/memory/player.go | 8 +- 7 files changed, 239 insertions(+), 31 deletions(-) diff --git a/pkg/data/state/states.go b/pkg/data/state/states.go index e23a6d9..a0715dd 100644 --- a/pkg/data/state/states.go +++ b/pkg/data/state/states.go @@ -245,4 +245,5 @@ const ( BindDemonUnderling State = 227 Herald State = 228 Apocalypse State = 229 + Missilelimit State = 230 ) diff --git a/pkg/memory/addresses.go b/pkg/memory/addresses.go index eaca023..2eb3b54 100644 --- a/pkg/memory/addresses.go +++ b/pkg/memory/addresses.go @@ -1,5 +1,5 @@ package memory const ( - tzOffline = 0x29B3208 + tzOffline = 0x25B2300 ) diff --git a/pkg/memory/game_reader.go b/pkg/memory/game_reader.go index 7eb3b68..daeb1a0 100644 --- a/pkg/memory/game_reader.go +++ b/pkg/memory/game_reader.go @@ -76,7 +76,7 @@ func (gd *GameReader) GetData() data.Data { // Conditionally update monsters monsters := gd.cachedMonsters - if now.Sub(gd.monstersLastUpdate) > 200*time.Millisecond { + if now.Sub(gd.monstersLastUpdate) > 100*time.Millisecond { monsters = gd.Monsters(pu.Position, hover) gd.cachedMonsters = monsters gd.monstersLastUpdate = now @@ -88,7 +88,7 @@ func (gd *GameReader) GetData() data.Data { // Conditionally update inventory 500ms // Except when hovering over an item inventory := gd.cachedInventory - if now.Sub(gd.inventoryLastUpdate) > 500*time.Millisecond || + if now.Sub(gd.inventoryLastUpdate) > 250*time.Millisecond || (hover.IsHovered && hover.UnitType == 4) { // 4 = Item type inventory = gd.Inventory(rawPlayerUnits, merc, hover) gd.cachedInventory = inventory @@ -97,7 +97,7 @@ func (gd *GameReader) GetData() data.Data { // Conditionally update objects objects := gd.cachedObjects - if now.Sub(gd.objectsLastUpdate) > 200*time.Millisecond { + if now.Sub(gd.objectsLastUpdate) > 100*time.Millisecond { objects = gd.Objects(pu.Position, hover) gd.cachedObjects = objects gd.objectsLastUpdate = now diff --git a/pkg/memory/item.go b/pkg/memory/item.go index c2c8dc0..31e29df 100644 --- a/pkg/memory/item.go +++ b/pkg/memory/item.go @@ -384,8 +384,11 @@ func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, merc OwnMercenary // Set runeword name if applicable if itm.IsRuneword { - if runeword, exists := item.RunewordIDMap[prefixes[0]]; exists { - itm.RunewordName = runeword + for _, prefixID := range prefixes { + if runeword, exists := item.RunewordIDMap[prefixID]; exists { + itm.RunewordName = runeword + break + } } if itm.RunewordName == "" || itm.RunewordName == item.RunewordHustle { @@ -546,7 +549,7 @@ func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, merc OwnMercenary func (gd *GameReader) getItemStats(statsListExPtr uintptr) (stat.Stats, stat.Stats) { // Initial full and base stats extraction - fullStats := gd.getStatsList(statsListExPtr + 0xA8) + fullStats := gd.getStatsList(statsListExPtr + 0xE8) baseStats := gd.getStatsList(statsListExPtr + 0x30) // Create empty LevelRequire stat .We will update it from inventory diff --git a/pkg/memory/offset.go b/pkg/memory/offset.go index 1eeaf8e..8ec8e63 100644 --- a/pkg/memory/offset.go +++ b/pkg/memory/offset.go @@ -26,61 +26,61 @@ type Offset struct { func calculateOffsets(_ *Process) Offset { // UnitTable - unitTableOffset := uintptr(0x1EA73D0) + unitTableOffset := uintptr(0x1EB9430) // UI - uiOffsetPtr := uintptr(0x1EB70CA) + uiOffsetPtr := uintptr(0x1EC912A) // Hover - hoverOffset := uintptr(0x1DFB080) + hoverOffset := uintptr(0x1E0D0A0) // Expansion - expOffset := uintptr(0x1DFA4E8) + expOffset := uintptr(0x1E0C508) // Party members offset - rosterOffset := uintptr(0x1EBD6E8) + rosterOffset := uintptr(0x1ECF748) // PanelManagerContainer - panelManagerContainerOffset := uintptr(0x1E11E40) + panelManagerContainerOffset := uintptr(0x1E23E60) // WidgetStates - WidgetStatesOffset := uintptr(0x1EDF700) + WidgetStatesOffset := uintptr(0x1EF1760) // Waypoints - WaypointTableOffset := uintptr(0x1D59440) + WaypointTableOffset := uintptr(0x1D6B458) // FPS - fpsOffset := uintptr(0x1D59414) + fpsOffset := uintptr(0x1D6B42C) // KeyBindings - keyBindingsOffset := uintptr(0x19D25B4) + keyBindingsOffset := uintptr(0x19E45F4) // KeyBindings Skills - keyBindingsSkillsOffset := uintptr(0x1DFB190) + keyBindingsSkillsOffset := uintptr(0x1E0D1B0) - // QuestInfo - questInfoOffset := uintptr(0x1EC3D58) + // QuestInfo: s_questInfo resolved from live D2R AOB scan. + questInfoOffset := uintptr(0x1ED5DB8) // Terror Zones - tzOffset := uintptr(0x25B1B80) + tzOffset := uintptr(0x25B5578) // Ping - pingOffset := uintptr(0x1DFA4E8) + pingOffset := uintptr(0x1E0C508) // LegacyGraphics - legacyGfxOffset := uintptr(0x1EC3FC6) + legacyGfxOffset := uintptr(0x1ED6026) // CharData - charDataOffset := uintptr(0x1DFE7A0) + charDataOffset := uintptr(0x1E10658) // Selected Char Name - selectedCharNameOffset := uintptr(0x1D50215) + selectedCharNameOffset := uintptr(0x1D62215) // Last Game Name - lastGameNameOffset := uintptr(0x25FA4E0) + lastGameNameOffset := uintptr(0x260C4B0) // Last Game Password - lastGamePasswordOffset := uintptr(0x25FA538) + lastGamePasswordOffset := uintptr(0x260C508) return Offset{ UnitTable: unitTableOffset, diff --git a/pkg/memory/panels.go b/pkg/memory/panels.go index 9ceda88..e6e5cf7 100644 --- a/pkg/memory/panels.go +++ b/pkg/memory/panels.go @@ -1,9 +1,15 @@ 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, @@ -42,6 +48,204 @@ func GetText(p data.Panel) string { 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 } diff --git a/pkg/memory/player.go b/pkg/memory/player.go index e96dc34..e78ce73 100644 --- a/pkg/memory/player.go +++ b/pkg/memory/player.go @@ -47,7 +47,7 @@ func (gd *GameReader) GetRawPlayerUnits() RawPlayerUnits { statsListExPtr := uintptr(gd.Process.ReadUInt(playerUnit+0x88, Uint64)) baseStats := gd.getStatsList(statsListExPtr + 0x30) - stats := gd.getStatsList(statsListExPtr + 0xA8) + stats := gd.getStatsList(statsListExPtr + 0xE8) states := gd.GetStates(statsListExPtr) playerMode := mode.PlayerMode(gd.Process.ReadUInt(playerUnit+0x0c, Uint32)) @@ -184,9 +184,9 @@ func (gd *GameReader) getSkills(skillListPtr uintptr) map[skill.ID]skill.Points for skillPtr != 0 { skillTxtPtr := uintptr(gd.Process.ReadUInt(skillPtr, Uint64)) skillTxt := uintptr(gd.Process.ReadUInt(skillTxtPtr, Uint16)) - lvl := gd.Process.ReadUInt(skillPtr+0x40, Uint16) + lvl := gd.Process.ReadUInt(skillPtr+0x38, Uint16) qty := gd.Process.ReadUInt(skillPtr+0x48, Uint16) - charges := gd.Process.ReadUInt(skillPtr+0x50, Uint16) + charges := gd.Process.ReadUInt(skillPtr+0x4C, Uint16) shouldSetSkill := true existingSkill, exists := skills[skill.ID(skillTxt)] @@ -214,7 +214,7 @@ func (gd *GameReader) GetStates(statsListExPtr uintptr) state.States { var states state.States for i := 0; i < 8; i++ { offset := i * 4 - stateByte := gd.Process.ReadUInt(statsListExPtr+0xAF0+uintptr(offset), Uint32) + stateByte := gd.Process.ReadUInt(statsListExPtr+0xB30+uintptr(offset), Uint32) offset = (32 * i) - 1 states = append(states, calculateStates(stateByte, uint(offset))...)