package memory import ( "errors" "fmt" "log" "math" "strings" "time" "github.com/hectorgimenez/d2go/pkg/data" "github.com/hectorgimenez/d2go/pkg/data/skill" "github.com/hectorgimenez/d2go/pkg/data/stat" ) type GameReader struct { offset Offset *Process monstersLastUpdate time.Time inventoryLastUpdate time.Time objectsLastUpdate time.Time cachedMonsters data.Monsters cachedInventory data.Inventory cachedObjects []data.Object } type MercOption struct { Index int Name string Skill skill.Skill Level int Life int Defense int Cost int } type CharacterFlags struct { Hardcore bool HasEverDied bool Expansion bool Ladder bool } var WidgetStateFlags = map[string]uint64{ "WeaponSwap": 0xF2D7CF8E9CC08212, } func NewGameReader(process *Process) *GameReader { return &GameReader{ offset: calculateOffsets(process), Process: process, monstersLastUpdate: time.Time{}, inventoryLastUpdate: time.Time{}, objectsLastUpdate: time.Time{}, } } func (gd *GameReader) GetData() data.Data { if gd.offset.UnitTable == 0 { gd.offset = calculateOffsets(gd.Process) } // Always refresh core player data rawPlayerUnits := gd.GetRawPlayerUnits() mainPlayerUnit := rawPlayerUnits.GetMainPlayer() pu := gd.GetPlayerUnit(mainPlayerUnit) hover := gd.HoveredData() now := time.Now() // Conditionally update monsters monsters := gd.cachedMonsters if now.Sub(gd.monstersLastUpdate) > 200*time.Millisecond { monsters = gd.Monsters(pu.Position, hover) gd.cachedMonsters = monsters gd.monstersLastUpdate = now } // Conditionally update inventory 500ms // Except when hovering over an item inventory := gd.cachedInventory if now.Sub(gd.inventoryLastUpdate) > 500*time.Millisecond || (hover.IsHovered && hover.UnitType == 4) { // 4 = Item type inventory = gd.Inventory(rawPlayerUnits, hover) gd.cachedInventory = inventory gd.inventoryLastUpdate = now } // Conditionally update objects objects := gd.cachedObjects if now.Sub(gd.objectsLastUpdate) > 200*time.Millisecond { objects = gd.Objects(pu.Position, hover) gd.cachedObjects = objects gd.objectsLastUpdate = now } // Always update other critical data corpseUnit := rawPlayerUnits.GetCorpse() roster := gd.getRoster(rawPlayerUnits) openMenus := gd.OpenMenus() // Quests questDataPtr := uintptr(gd.Process.ReadUInt(gd.moduleBaseAddressPtr+gd.offset.QuestInfo, Uint64)) flagsBufferPtr := uintptr(gd.Process.ReadUInt(questDataPtr, Uint64)) gameQuestsBytes := gd.Process.ReadBytesFromMemory(flagsBufferPtr, 82) d := data.Data{ Corpse: data.Corpse{ Found: corpseUnit.Address != 0, IsHovered: corpseUnit.IsHovered, Position: corpseUnit.Position, States: corpseUnit.States, }, Game: data.OnlineGame{ LastGameName: gd.LastGameName(), LastGamePassword: gd.LastGamePass(), FPS: gd.FPS(), Ping: gd.Ping(), }, 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 } func (gd *GameReader) GetInventory() data.Inventory { rawPlayerUnits := gd.GetRawPlayerUnits() hover := gd.HoveredData() return gd.Inventory(rawPlayerUnits, hover) } func (gd *GameReader) InGame() bool { player := gd.GetRawPlayerUnits().GetMainPlayer() return player.UnitID > 0 && player.Position.X > 0 && player.Position.Y > 0 && player.Area > 0 } func (gd *GameReader) OpenMenus() data.OpenMenus { uiBase := gd.Process.moduleBaseAddressPtr + gd.offset.UI - 0xA buffer := gd.Process.ReadBytesFromMemory(uiBase, 0x16D) isMapShown := gd.Process.ReadUInt(gd.Process.moduleBaseAddressPtr+gd.offset.UI, Uint8) return data.OpenMenus{ Inventory: buffer[0x01] != 0, LoadingScreen: buffer[0x168] != 0, NPCInteract: buffer[0x08] != 0, NPCShop: buffer[0x0B] != 0, Stash: buffer[0x18] != 0, Waypoint: buffer[0x13] != 0, MapShown: isMapShown != 0, SkillTree: buffer[0x04] != 0, NewSkills: buffer[0x07] != 0, NewStats: buffer[0x06] != 0, Character: buffer[0x02] != 0, QuitMenu: buffer[0x09] != 0, Cube: buffer[0x19] != 0, SkillSelect: buffer[0x03] != 0, Anvil: buffer[0x0D] != 0, MercInventory: buffer[0x1E] != 0, BeltRows: buffer[0x1A] != 0, QuestLog: buffer[0xE] != 0, PortraitsShown: buffer[0x1D] != 0, ChatOpen: buffer[0x05] != 0, Cinematic: buffer[0x11] != 0, } } func (gd *GameReader) HoveredData() data.HoverData { hoverAddressPtr := gd.Process.moduleBaseAddressPtr + gd.offset.Hover hoverBuffer := gd.Process.ReadBytesFromMemory(hoverAddressPtr, 12) isUnitHovered := ReadUIntFromBuffer(hoverBuffer, 0, Uint16) if isUnitHovered > 0 { hoveredType := ReadUIntFromBuffer(hoverBuffer, 0x04, Uint32) hoveredUnitID := ReadUIntFromBuffer(hoverBuffer, 0x08, Uint32) return data.HoverData{ IsHovered: true, UnitID: data.UnitID(hoveredUnitID), UnitType: int(hoveredType), } } return data.HoverData{} } func (gd *GameReader) getStatsList(statListPtr uintptr) stat.Stats { statsListBuffer := gd.ReadBytesFromMemory(statListPtr, 0x10) statList := ReadUIntFromBuffer(statsListBuffer, 0, Uint64) statCount := ReadUIntFromBuffer(statsListBuffer, 0x08, Uint64) if statCount == 0 { return []stat.Data{} } var stats = make([]stat.Data, 0) statBuffer := gd.Process.ReadBytesFromMemory(uintptr(statList), statCount*10) for i := 0; i < int(statCount); i++ { offset := uint(i * 8) statLayer := ReadUIntFromBuffer(statBuffer, offset, Uint16) statEnum := ReadUIntFromBuffer(statBuffer, offset+0x2, Uint16) statValue := ReadIntFromBuffer(statBuffer, offset+0x4, Uint32) value := statValue switch stat.ID(statEnum) { case stat.Life, stat.MaxLife, stat.Mana, stat.MaxMana, stat.Stamina, stat.MaxStamina: value = statValue >> 8 case stat.ColdLength, stat.PoisonLength: value = statValue / 25 case stat.DeadlyStrikePerLevel: value = int(float64(statValue) / .8) case stat.HitCausesMonsterToFlee: value = int(float64(statValue) / 1.28) case stat.AttackRatingUndeadPerLevel: value = statValue / 2 case stat.MagicFindPerLevel, stat.ExtraGoldPerLevel, stat.DamageDemonPerLevel, stat.DamageUndeadPerLevel, stat.DefensePerLevel, stat.MaxDamagePerLevel, stat.MaxDamagePercentPerLevel, stat.StrengthPerLevel, stat.DexterityPerLevel, stat.VitalityPerLevel, stat.ThornsPerLevel: value = int(math.Max(float64(statValue/8), 1)) case stat.LifePerLevel, stat.ManaPerLevel: value = int(math.Max(float64(statValue/2048), 1)) case stat.ReplenishDurability, stat.ReplenishQuantity: if statValue > 0 { value = int(math.Max(float64(2/statValue), 1)) } case stat.RegenStaminaPerLevel: value = int(statValue) * 10 case stat.LevelRequirePercent: value = int(statValue) * -1 case stat.AttackRatingPerLevel: value = int(math.Max(float64(statValue), 15)) } stats = append(stats, stat.Data{ ID: stat.ID(statEnum), Value: value, Layer: int(statLayer), }) } return stats } // 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{} } // 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 { return gd.Process.ReadStringFromMemory(gd.Process.moduleBaseAddressPtr+gd.offset.SelectedCharName, 0) } func (gd *GameReader) LegacyGraphics() bool { return gd.ReadUInt(gd.Process.moduleBaseAddressPtr+gd.offset.LegacyGraphics, Uint8) != 0 } func (gd *GameReader) IsOnline() bool { panel := gd.GetPanel("MainMenuPanel", "SecondaryContextButton") return panel.PanelName != "" && panel.PanelEnabled && panel.PanelVisible } func (gd *GameReader) IsIngame() bool { return gd.ReadUInt(gd.Process.moduleBaseAddressPtr+gd.offset.UI-0xA, 1) == 1 } func (gd *GameReader) IsInLobby() bool { panel := gd.GetPanel("LobbyBackgroundPanel") return panel.PanelName != "" && panel.PanelEnabled && panel.PanelVisible } func (gd *GameReader) IsInCharacterSelectionScreen() bool { panel := gd.GetPanel("CharacterSelectPanel") return panel.PanelName != "" && panel.PanelEnabled && panel.PanelVisible } func (gd *GameReader) IsInCharacterCreationScreen() 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{} } // 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 } // GetMercList returns the list of mercenaries available for hire in the Hire Menu // Only works if the Hire Menu is open in legacy graphics mode func (gd *GameReader) GetMercList() []MercOption { panel := gd.GetPanel("HireMenuPanel", "ListContainer", "View", "Container") if panel.PanelName == "" || panel.NumChildren == 0 { return []MercOption{} } mercOptions := make([]MercOption, panel.NumChildren) for i := 0; i < panel.NumChildren; i++ { merc := panel.PanelChildren[fmt.Sprintf("ListItem%d", i)].PanelChildren["TextBox"].ExtraText3 var name, skillName string var level, life, def, cost int n, err := fmt.Sscanf(merc, "%s - Lvl: %d Life: %d Def: %d Cost: %d\n", &name, &level, &life, &def, &cost) if err != nil || n < 5 { continue } lines := strings.Split(merc, "\n") skillName = strings.TrimSpace(lines[1]) sk := skill.Skill{} for _, s := range skill.Skills { if s.Name == skillName { sk = s break } } if sk.Name == "" { log.Printf("Unknown merc skill: %s", skillName) continue } mercOptions[i] = MercOption{ Index: i, Name: name, Skill: sk, Level: level, Life: life, Defense: def, Cost: cost, } } return mercOptions } // IsBlocking checks if there's a blocking popup or loading screen present func (gd *GameReader) IsBlocking() bool { panel := gd.GetPanel("BlockingPanel") panel2 := gd.GetPanel("DismissableModal") return (panel.PanelName != "" && panel.PanelEnabled && panel.PanelVisible) || (panel2.PanelName != "" && panel2.PanelEnabled && panel2.PanelVisible) } // 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 { return gd.ReadStringFromMemory(gd.moduleBaseAddressPtr+gd.offset.LastGameName, 0) } func (gd *GameReader) LastGamePass() string { return gd.ReadStringFromMemory(gd.moduleBaseAddressPtr+gd.offset.LastGamePassword, 0) } func (gd *GameReader) FPS() int { return int(gd.ReadUInt(gd.moduleBaseAddressPtr+gd.offset.FPS, Uint32)) } func (gd *GameReader) Ping() int { ptrToStructPtr := gd.moduleBaseAddressPtr + gd.offset.Ping structPtrAddr := gd.ReadUInt(ptrToStructPtr, Uint64) return int(gd.ReadUInt(uintptr(structPtrAddr+36), Uint32)) } func (gd *GameReader) HasMerc() bool { return gd.ReadUInt(gd.Process.moduleBaseAddressPtr+gd.offset.UI+0x8, Uint8) != 0 } // GetWidgetState reference : https://github.com/ResurrectedTrader/ResurrectedTrade/blob/f121ec02dd3fbe1c574f713e5a0c2db92ccca821/ResurrectedTrade.AgentBase/Capture.cs#L618 func (gd *GameReader) GetWidgetState(stateFlag uint64) (int, error) { // Get widget states pointer stateFlags := uint64(gd.Process.ReadUInt(gd.moduleBaseAddressPtr+gd.offset.WidgetStatesOffset, Uint64)) if stateFlags == 0 { return 0, nil } v2 := uint64(gd.Process.ReadUInt(uintptr(stateFlags)+8, Uint64)) if v2 == 0 { return 0, nil } flag := stateFlag v4 := uint64(0xC4CEB9FE1A85EC53) * ((uint64(0xFF51AFD7ED558CCD) * (flag ^ (flag >> 33))) ^ ((uint64(0xFF51AFD7ED558CCD) * (flag ^ (flag >> 33))) >> 33)) v5 := (uint64(gd.Process.ReadUInt(uintptr(stateFlags), Uint64)) - 1) & (v4 ^ (v4 >> 33)) v6 := uint64(gd.Process.ReadUInt(uintptr(v2)+uintptr(8*v5), Uint64)) i := uintptr(v2) + uintptr(8*v5) for ; v6 != 0; v6 = uint64(gd.Process.ReadUInt(uintptr(v6), Uint64)) { if flag == uint64(gd.Process.ReadUInt(uintptr(v6)+8, Uint64)) { break } i = uintptr(v6) } ir := uint64(gd.Process.ReadUInt(i, Uint64)) if ir != 0 { ptr1 := uint64(gd.Process.ReadUInt(uintptr(ir)+16, Uint64)) ptr2 := uint64(gd.Process.ReadUInt(uintptr(ptr1)+16, Uint64)) return int(gd.Process.ReadUInt(uintptr(ptr2), Uint8)), nil } return 0, nil } func (gd *GameReader) GetActiveWeaponSlot() int { state, err := gd.GetWidgetState(WidgetStateFlags["WeaponSwap"]) if err != nil { return 0 // Default to primary weapons on error } return state } func (gd *GameReader) GetCharacterFlags(characterName string) (CharacterFlags, error) { const ( charDataHeaderSize = 16 charNameOffset = 0x010 charFlagsOffset = 0x122 maxCharCount = 47 flagHardcore = 0x04 flagDead = 0x08 flagExpansion = 0x20 flagLadder = 0x40 ) charDataPtr := gd.moduleBaseAddressPtr + gd.offset.CharData if charDataPtr == 0 { return CharacterFlags{}, errors.New("character data pointer is invalid") } headerBuffer := gd.Process.ReadBytesFromMemory(charDataPtr, charDataHeaderSize) if len(headerBuffer) < charDataHeaderSize { return CharacterFlags{}, errors.New("failed to read character data header") } charArrayPtr := uintptr(ReadUIntFromBuffer(headerBuffer, 0x00, Uint64)) charCount := int(ReadIntFromBuffer(headerBuffer, 0x08, Uint64)) if charArrayPtr == 0 || charCount <= 0 || charCount > maxCharCount { return CharacterFlags{}, fmt.Errorf("invalid character metadata: arrayPtr=%v, count=%d", charArrayPtr, charCount) } charPointerArray := gd.Process.ReadBytesFromMemory(charArrayPtr, uint(charCount*8)) for i := 0; i < charCount; i++ { charStructPtr := uintptr(ReadUIntFromBuffer(charPointerArray, uint(i*8), Uint64)) if charStructPtr == 0 { continue } charName := gd.Process.ReadStringFromMemory(charStructPtr+charNameOffset, 0) if charName == characterName { fieldValue := uint16(gd.Process.ReadUInt(charStructPtr+charFlagsOffset, Uint16)) flags := CharacterFlags{ Hardcore: (fieldValue & flagHardcore) != 0, HasEverDied: (fieldValue & flagDead) != 0, Expansion: (fieldValue & flagExpansion) != 0, Ladder: (fieldValue & flagLadder) != 0, } return flags, nil } } return CharacterFlags{}, fmt.Errorf("character not found: %s", characterName) }