OffSetFix-91403
This commit is contained in:
@@ -318,7 +318,7 @@ func (gd *GameReader) InCharacterSelectionScreen() bool {
|
||||
}
|
||||
|
||||
func (gd *GameReader) GetSelectedCharacterName() string {
|
||||
return gd.Process.ReadStringFromMemory(gd.Process.moduleBaseAddressPtr+0x2120FF4, 0)
|
||||
return gd.Process.ReadStringFromMemory(gd.Process.moduleBaseAddressPtr+0x1C3D694, 16)
|
||||
}
|
||||
|
||||
func (gd *GameReader) LegacyGraphics() bool {
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
)
|
||||
|
||||
func (gd *GameReader) GetKeyBindings() data.KeyBindings {
|
||||
blob := gd.ReadBytesFromMemory(gd.moduleBaseAddressPtr+0x1DE4CC4, 0x500)
|
||||
blobSkills := gd.ReadBytesFromMemory(gd.offset.KeyBindingsSkillsOffset, 0x500)
|
||||
blob := gd.ReadBytesFromMemory(gd.moduleBaseAddressPtr+0x18C2894, 0x500)
|
||||
blobSkills := gd.ReadBytesFromMemory(gd.moduleBaseAddressPtr+0x1CE8510, 0x500)
|
||||
|
||||
skillsKB := [16]data.SkillBinding{}
|
||||
for i := 0; i < 7; i++ {
|
||||
@@ -199,13 +199,5 @@ func (gd *GameReader) GetKeyBindings() data.KeyBindings {
|
||||
Key1: [2]byte{blob[0x488], blob[0x489]},
|
||||
Key2: [2]byte{blob[0x492], blob[0x493]},
|
||||
},
|
||||
PreviousStashPage: data.KeyBinding{
|
||||
Key1: [2]byte{blob[0x4c4], blob[0x4c5]},
|
||||
Key2: [2]byte{blob[0x4ce], blob[0x4cf]},
|
||||
},
|
||||
NextStashPage: data.KeyBinding{
|
||||
Key1: [2]byte{blob[0x4d8], blob[0x4d9]},
|
||||
Key2: [2]byte{blob[0x4e2], blob[0x4e3]},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
type Offset struct {
|
||||
GameData uintptr
|
||||
UnitTable uintptr
|
||||
@@ -26,119 +22,70 @@ type Offset struct {
|
||||
}
|
||||
|
||||
func calculateOffsets(process *Process) Offset {
|
||||
// ignoring errors, always best practices
|
||||
memory, _ := process.getProcessMemory()
|
||||
|
||||
// GameReader
|
||||
pattern := process.FindPattern(memory, "\x44\x88\x25\x00\x00\x00\x00\x66\x44\x89\x25\x00\x00\x00\x00", "xxx????xxxx????")
|
||||
bytes := process.ReadBytesFromMemory(pattern+0x3, 4)
|
||||
offsetInt := uintptr(binary.LittleEndian.Uint32(bytes))
|
||||
gameDataOffset := (pattern - process.moduleBaseAddressPtr) - 0x121 + offsetInt
|
||||
memory, err := process.getProcessMemory()
|
||||
if err != nil || len(memory) == 0 {
|
||||
// Return empty offsets - will be recalculated when game is ready
|
||||
return Offset{}
|
||||
}
|
||||
|
||||
// UnitTable
|
||||
pattern = process.FindPattern(memory, "\x48\x03\xC7\x49\x8B\x8C\xC6", "xxxxxxx")
|
||||
bytes = process.ReadBytesFromMemory(pattern+7, 4)
|
||||
unitTableOffset := uintptr(binary.LittleEndian.Uint32(bytes))
|
||||
unitTableOffset := uintptr(0x1D95AF0)
|
||||
|
||||
// UI
|
||||
pattern = process.FindPattern(memory, "\x40\x84\xed\x0f\x94\x05", "xxxxxx")
|
||||
uiOffset := process.ReadUInt(pattern+6, Uint32)
|
||||
uiOffsetPtr := (pattern - process.moduleBaseAddressPtr) + 10 + uintptr(uiOffset)
|
||||
uiOffsetPtr := uintptr(0x1DA57DA)
|
||||
|
||||
// Hover
|
||||
pattern = process.FindPattern(memory, "\xc6\x84\xc2\x00\x00\x00\x00\x00\x48\x8b\x74", "xxx?????xxx")
|
||||
hoverOffset := process.ReadUInt(pattern+3, Uint32) - 1
|
||||
hoverOffset := uintptr(0x1CE8400)
|
||||
|
||||
// Expansion
|
||||
pattern = process.FindPattern(memory, "\x48\x8B\x05\x00\x00\x00\x00\x48\x8B\xD9\xF3\x0F\x10\x50\x00", "xxx????xxxxxxx?")
|
||||
offsetPtr := uintptr(process.ReadUInt(pattern+3, Uint32))
|
||||
expOffset := pattern - process.moduleBaseAddressPtr + 7 + offsetPtr
|
||||
expOffset := uintptr(0x1CE78D0)
|
||||
|
||||
// Party members offset
|
||||
pattern = process.FindPattern(memory, "\x02\x45\x33\xD2\x4D\x8B", "xxxxxx")
|
||||
offsetPtr = uintptr(process.ReadUInt(pattern-3, Uint32))
|
||||
rosterOffset := pattern - process.moduleBaseAddressPtr + 1 + offsetPtr
|
||||
rosterOffset := uintptr(0x1DABD60)
|
||||
|
||||
// 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))
|
||||
panelManagerContainerOffset := uintptr(0x1D00968)
|
||||
|
||||
// WidgetStates
|
||||
pattern = process.FindPattern(memory, "\x48\x8B\x0D\x00\x00\x00\x00\x4C\x8D\x44\x24\x00\x48\x03\xC2", "xxx????xxxx?xxx")
|
||||
WidgetStatesPtr := process.ReadUInt(pattern+3, Uint32)
|
||||
WidgetStatesOffset := pattern - process.moduleBaseAddressPtr + 7 + uintptr(WidgetStatesPtr)
|
||||
WidgetStatesOffset := uintptr(0x1DCDD40)
|
||||
|
||||
// Waypoints
|
||||
pattern = process.FindPattern(memory, "\x48\x89\x05\x00\x00\x00\x00\x0F\x11\x00", "xxx????xxx")
|
||||
offsetBuffer := process.ReadUInt(pattern+3, Uint32)
|
||||
WaypointTableOffset := pattern - process.moduleBaseAddressPtr + 7 + uintptr(offsetBuffer)
|
||||
WaypointTableOffset := uintptr(0x1C468B0)
|
||||
|
||||
// FPS
|
||||
pattern = process.FindPattern(memory, "\x8B\x1D\x00\x00\x00\x00\x48\x8D\x05\x00\x00\x00\x00\x48\x8D\x4C\x24\x40", "xx????xxx????xxxxx")
|
||||
fpsOffsetPtr := uintptr(process.ReadUInt(pattern+2, Uint32))
|
||||
fpsOffset := pattern - process.moduleBaseAddressPtr + 6 + fpsOffsetPtr
|
||||
|
||||
// Keybindings
|
||||
pattern = process.FindPattern(memory, "\x48\x8D\x05\xAF\xEE", "xxxxx")
|
||||
bytes = process.ReadBytesFromMemory(pattern+3, 4)
|
||||
relativeOffset := int32(binary.LittleEndian.Uint32(bytes))
|
||||
keyBindingsOffset := pattern - process.moduleBaseAddressPtr + 7 + uintptr(relativeOffset)
|
||||
fpsOffset := uintptr(0x1C46894)
|
||||
|
||||
// KeyBindings Skills
|
||||
pattern = process.FindPattern(memory, "\x0F\x10\x04\x24\x48\x6B\xC8\x1C\x48\x8D\x05", "xxxxxxxxxxx")
|
||||
var keyBindingsSkillsOffset uintptr
|
||||
bytes = process.ReadBytesFromMemory(pattern+11, 4)
|
||||
relativeOffset = int32(binary.LittleEndian.Uint32(bytes))
|
||||
keyBindingsSkillsOffset = uintptr(int64(pattern) + 15 + int64(relativeOffset))
|
||||
keyBindingsSkillsOffset := uintptr(0x1CE8510)
|
||||
|
||||
// QuestInfo
|
||||
pattern = process.FindPattern(memory, "\x48\x8B\x0D\x00\x00\x00\x00\xE8\x00\x00\x00\x00\x45\x33\xE4", "xxx????x????xxx")
|
||||
questInfoOffsetPtr := uintptr(process.ReadUInt(pattern+3, Uint32))
|
||||
questInfoOffset := pattern - process.moduleBaseAddressPtr + 7 + questInfoOffsetPtr
|
||||
questInfoOffset := uintptr(0)
|
||||
|
||||
// Terror Zones
|
||||
tzOffset := uintptr(0x29B2DF0)
|
||||
|
||||
// Quest Bytes Data
|
||||
pattern = process.FindPattern(memory, "\x42\xc6\x84\x28\x00\x00\x00\x00\x00\x49\xff\xc5\x49\x83\xfd\x29", "xxxx?????xxxxxxx")
|
||||
bytes = process.ReadBytesFromMemory(pattern+4, 4)
|
||||
questOffset := uintptr(binary.LittleEndian.Uint32(bytes))
|
||||
questDataOffset := questOffset + 1
|
||||
tzOffset := uintptr(0x248D4C8)
|
||||
|
||||
// Ping
|
||||
pattern = process.FindPattern(memory, "\x48\x8B\x0D\xCC\xCC\xCC\xCC\x49\x2B\xC7", "xxx????xxx")
|
||||
bytes = process.ReadBytesFromMemory(pattern+3, 4)
|
||||
relativeOffset = int32(binary.LittleEndian.Uint32(bytes))
|
||||
pingOffset := pattern - process.moduleBaseAddressPtr + 7 + uintptr(relativeOffset)
|
||||
pingOffset := uintptr(0)
|
||||
|
||||
// LegacyGraphics
|
||||
pattern = process.FindPattern(memory, "\x80\x3D\x00\x00\x00\x00\x00\x48\x8D\x54\x24\x30", "xx?????xxxxx")
|
||||
legacyGfxPtr := uintptr(process.ReadUInt(pattern+2, Uint32))
|
||||
legacyGfxOffset := pattern - process.moduleBaseAddressPtr + 7 + legacyGfxPtr
|
||||
legacyGfxOffset := uintptr(0x1DB263E)
|
||||
|
||||
// CharData
|
||||
pattern = process.FindPattern(memory, "\x48\x8D\x05\x00\x00\x00\x00\x89\x93\xF4\x0C\x00\x00", "xxx????xxxxxx")
|
||||
bytes = process.ReadBytesFromMemory(pattern+3, 4)
|
||||
relativeOffset = int32(binary.LittleEndian.Uint32(bytes))
|
||||
charDataOffset := pattern - process.moduleBaseAddressPtr + 7 + uintptr(relativeOffset)
|
||||
charDataOffset := uintptr(0x1CED5E8)
|
||||
|
||||
return Offset{
|
||||
GameData: gameDataOffset,
|
||||
UnitTable: unitTableOffset,
|
||||
UI: uiOffsetPtr,
|
||||
Hover: uintptr(hoverOffset),
|
||||
Hover: hoverOffset,
|
||||
Expansion: expOffset,
|
||||
RosterOffset: rosterOffset,
|
||||
PanelManagerContainerOffset: panelManagerContainerOffset,
|
||||
WidgetStatesOffset: WidgetStatesOffset,
|
||||
WaypointTableOffset: WaypointTableOffset,
|
||||
FPS: fpsOffset,
|
||||
KeyBindingsOffset: keyBindingsOffset,
|
||||
KeyBindingsSkillsOffset: keyBindingsSkillsOffset,
|
||||
QuestInfo: questInfoOffset,
|
||||
TZ: tzOffset,
|
||||
Quests: questDataOffset,
|
||||
Ping: pingOffset,
|
||||
LegacyGraphics: legacyGfxOffset,
|
||||
CharData: charDataOffset,
|
||||
|
||||
@@ -105,10 +105,44 @@ func getMainModule(pid uint32) (ModuleInfo, bool) {
|
||||
}
|
||||
|
||||
func (p *Process) getProcessMemory() ([]byte, error) {
|
||||
var data = make([]byte, p.moduleBaseSize)
|
||||
err := windows.ReadProcessMemory(p.handler, p.moduleBaseAddressPtr, &data[0], uintptr(p.moduleBaseSize), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Use chunked reading as primary method since VirtualQueryEx is often blocked
|
||||
return p.getProcessMemoryChunked()
|
||||
}
|
||||
|
||||
// getProcessMemoryChunked reads memory in small chunks, skipping protected regions
|
||||
func (p *Process) getProcessMemoryChunked() ([]byte, error) {
|
||||
return ReadMemoryChunked(p.handler, p.moduleBaseAddressPtr, p.moduleBaseSize)
|
||||
}
|
||||
|
||||
// ReadMemoryChunked reads memory in small chunks, skipping protected regions
|
||||
// This is useful for reading large modules where a single ReadProcessMemory call may fail
|
||||
func ReadMemoryChunked(handle windows.Handle, baseAddress uintptr, size uint32) ([]byte, error) {
|
||||
var data = make([]byte, size)
|
||||
const pageSize = uintptr(4096)
|
||||
|
||||
successfulReads := 0
|
||||
failedReads := 0
|
||||
|
||||
for offset := uintptr(0); offset < uintptr(size); offset += pageSize {
|
||||
address := baseAddress + offset
|
||||
chunkSize := pageSize
|
||||
|
||||
// Adjust last chunk
|
||||
if offset+pageSize > uintptr(size) {
|
||||
chunkSize = uintptr(size) - offset
|
||||
}
|
||||
|
||||
// Try to read, but don't fail if it's protected
|
||||
err := windows.ReadProcessMemory(handle, address, &data[offset], chunkSize, nil)
|
||||
if err != nil {
|
||||
failedReads++
|
||||
// Fill with zeros and continue (pattern matching will fail gracefully)
|
||||
for i := offset; i < offset+chunkSize; i++ {
|
||||
data[i] = 0
|
||||
}
|
||||
} else {
|
||||
successfulReads++
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
|
||||
@@ -47,6 +47,11 @@ var (
|
||||
procGetThreadTimes = kernel32.NewProc("GetThreadTimes")
|
||||
procVirtualProtectEx = kernel32.NewProc("VirtualProtectEx")
|
||||
procSetProcessValidCallTargets = kernel32.NewProc("SetProcessValidCallTargets")
|
||||
procCreateRemoteThread = kernel32.NewProc("CreateRemoteThread")
|
||||
|
||||
ntdll = windows.NewLazySystemDLL("ntdll.dll")
|
||||
procNtGetContextThread = ntdll.NewProc("NtGetContextThread")
|
||||
procNtSetContextThread = ntdll.NewProc("NtSetContextThread")
|
||||
|
||||
d2gsCachedFn uintptr
|
||||
d2gsCacheMu sync.RWMutex
|
||||
@@ -57,6 +62,7 @@ var (
|
||||
return new([sendPacketMetaSize]byte)
|
||||
},
|
||||
}
|
||||
sendPacketCallCount int
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -88,6 +94,9 @@ type sendPacketState struct {
|
||||
processPID uint32
|
||||
threadLastValidated time.Time
|
||||
leakedBuffers []uintptr
|
||||
lastSendTime time.Time
|
||||
sendCount int
|
||||
isExiting bool // Set to true when game is exiting to skip all packets
|
||||
}
|
||||
|
||||
type cfgCallTargetInfo struct {
|
||||
@@ -118,6 +127,8 @@ func findPatternOffset(memory []byte, pattern, mask string) int {
|
||||
|
||||
func (s *sendPacketState) ensureHandle(pid uint32) (windows.Handle, error) {
|
||||
if s.handle != 0 && s.processPID == pid {
|
||||
// Reset exit flag even if handle is reused - we're in a new game
|
||||
s.isExiting = false
|
||||
return s.handle, nil
|
||||
}
|
||||
|
||||
@@ -148,6 +159,10 @@ func (s *sendPacketState) ensureHandle(pid uint32) (windows.Handle, error) {
|
||||
}
|
||||
s.handle = h
|
||||
s.processPID = pid
|
||||
|
||||
// Reset exiting flag when starting new game
|
||||
s.isExiting = false
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
@@ -228,6 +243,74 @@ func (s *sendPacketState) ensurePacketBuffer(handle windows.Handle, size uintptr
|
||||
|
||||
s.packet = newPacket
|
||||
s.packetCap = allocSize
|
||||
|
||||
// Periodically attempt to clean up leaked buffers
|
||||
if len(s.leakedBuffers) > 0 {
|
||||
s.cleanupLeakedBuffers(handle)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanupLeakedBuffers attempts to free previously leaked buffers
|
||||
func (s *sendPacketState) cleanupLeakedBuffers(handle windows.Handle) {
|
||||
if len(s.leakedBuffers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
cleaned := s.leakedBuffers[:0]
|
||||
for _, addr := range s.leakedBuffers {
|
||||
if err := virtualFreeEx(handle, addr); err != nil {
|
||||
// Still can't free, keep it in the list
|
||||
cleaned = append(cleaned, addr)
|
||||
}
|
||||
}
|
||||
s.leakedBuffers = cleaned
|
||||
|
||||
if len(s.leakedBuffers) == 0 {
|
||||
log.Printf("Successfully cleaned up all leaked packet buffers")
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup sets exit flag to cancel all future packet sends
|
||||
// Pending APCs in queue will timeout naturally, which is fine since game is exiting
|
||||
// Also resets buffer pointers so they will be re-allocated on next game
|
||||
// IMPORTANT: We do NOT free remote memory here because pending APCs may still be
|
||||
// executing and trying to use that memory. Freeing it would cause D2R to crash.
|
||||
// The memory will be freed when the process exits, or orphaned if game continues.
|
||||
func (s *sendPacketState) Cleanup() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Set exiting flag to skip all future packet sends immediately
|
||||
s.isExiting = true
|
||||
|
||||
log.Printf("Packet system marked as exiting - all future packet sends will be cancelled")
|
||||
|
||||
// DO NOT free remote buffers - pending APCs may still be using them!
|
||||
// Just reset local pointers so they will be re-allocated on next game.
|
||||
// The remote memory will be orphaned but that's acceptable:
|
||||
// - If D2R exits/crashes, memory is freed with the process
|
||||
// - If game returns to menu, it's a small leak that won't accumulate
|
||||
// (next game allocates new buffers with fresh pointers)
|
||||
if s.handle != 0 {
|
||||
// Reset pointers WITHOUT freeing - memory is orphaned but safe
|
||||
s.packet = 0
|
||||
s.packetCap = 0
|
||||
s.meta = 0
|
||||
s.stub = 0
|
||||
|
||||
// Reset function pointer as it may be invalid after game exit
|
||||
s.fn = 0
|
||||
|
||||
// Close thread handle - the main thread may change between games
|
||||
if s.thread != 0 {
|
||||
windows.CloseHandle(s.thread)
|
||||
s.thread = 0
|
||||
s.threadID = 0
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -251,10 +334,15 @@ func (s *sendPacketState) ensureFunction(p *Process) (uintptr, error) {
|
||||
|
||||
func (s *sendPacketState) ensureThreadHandle(p *Process) (windows.Handle, error) {
|
||||
if s.thread != 0 && s.processPID == p.pid {
|
||||
// Validate thread is still active and belongs to the correct process
|
||||
if err := validateThreadActive(s.thread); err == nil {
|
||||
s.threadLastValidated = time.Now()
|
||||
return s.thread, nil
|
||||
// Additional validation: verify thread still belongs to this process
|
||||
if err := validateThreadBelongsToProcess(s.thread, p.pid); err == nil {
|
||||
s.threadLastValidated = time.Now()
|
||||
return s.thread, nil
|
||||
}
|
||||
}
|
||||
// Thread is invalid, close and reset
|
||||
windows.CloseHandle(s.thread)
|
||||
s.thread = 0
|
||||
s.threadID = 0
|
||||
@@ -276,6 +364,12 @@ func (s *sendPacketState) ensureThreadHandle(p *Process) (windows.Handle, error)
|
||||
return 0, fmt.Errorf("open thread %d: %w", threadID, err)
|
||||
}
|
||||
|
||||
// Validate the newly opened thread belongs to the correct process
|
||||
if err := validateThreadBelongsToProcess(handle, p.pid); err != nil {
|
||||
windows.CloseHandle(handle)
|
||||
return 0, fmt.Errorf("thread %d validation failed: %w", threadID, err)
|
||||
}
|
||||
|
||||
s.thread = handle
|
||||
s.threadID = threadID
|
||||
s.processPID = p.pid
|
||||
@@ -303,6 +397,31 @@ func validateThreadActive(thread windows.Handle) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateThreadBelongsToProcess verifies the thread belongs to the specified process
|
||||
// Uses basic process handle comparison for validation
|
||||
func validateThreadBelongsToProcess(thread windows.Handle, expectedPID uint32) error {
|
||||
// Try to get process ID of thread by checking thread times
|
||||
// If we can get thread times, the thread is valid
|
||||
var creationTime, exitTime, kernelTime, userTime windows.Filetime
|
||||
ret, _, err := procGetThreadTimes.Call(
|
||||
uintptr(thread),
|
||||
uintptr(unsafe.Pointer(&creationTime)),
|
||||
uintptr(unsafe.Pointer(&exitTime)),
|
||||
uintptr(unsafe.Pointer(&kernelTime)),
|
||||
uintptr(unsafe.Pointer(&userTime)),
|
||||
)
|
||||
if ret == 0 {
|
||||
if err != nil {
|
||||
return fmt.Errorf("thread validation failed (GetThreadTimes): %w", err)
|
||||
}
|
||||
return errors.New("thread validation failed: GetThreadTimes returned 0")
|
||||
}
|
||||
|
||||
// Thread is valid and accessible, which means we have permission
|
||||
// This is a basic check - we already validated the thread ID when opening it
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sendPacketState) dispatchAPC(thread windows.Handle, start, parameter uintptr) error {
|
||||
if thread == 0 {
|
||||
return errors.New("thread handle is zero")
|
||||
@@ -483,8 +602,8 @@ func (p *Process) GetD2GSSendPacketFn() (uintptr, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
memory := make([]byte, int(module.ModuleBaseSize))
|
||||
if err := windows.ReadProcessMemory(p.handler, module.ModuleBaseAddress, &memory[0], uintptr(module.ModuleBaseSize), nil); err != nil {
|
||||
memory, err := ReadMemoryChunked(p.handler, module.ModuleBaseAddress, module.ModuleBaseSize)
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to read module %s: %v", module.ModuleName, err)
|
||||
continue
|
||||
}
|
||||
@@ -599,7 +718,171 @@ func (p *Process) SendPacket(packet []byte) (err error) {
|
||||
return fmt.Errorf("dispatch APC: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
// Wait for completion with default timeout
|
||||
return p.waitForPacketCompletion(handle, state, 100*time.Millisecond)
|
||||
}
|
||||
|
||||
// SendPacketWithTimeout sends a packet with a custom APC timeout
|
||||
// Use this for high-ping connections where 100ms may not be enough
|
||||
func (p *Process) SendPacketWithTimeout(packet []byte, apcTimeout time.Duration) (err error) {
|
||||
if p == nil {
|
||||
return errors.New("process is nil")
|
||||
}
|
||||
|
||||
sendPacketCallCount++
|
||||
packetID := byte(0)
|
||||
if len(packet) > 0 {
|
||||
packetID = packet[0]
|
||||
}
|
||||
|
||||
// Log every increment packet send (0x3A = stat, 0x3B = skill)
|
||||
if packetID == 0x3A || packetID == 0x3B {
|
||||
packetType := "UNKNOWN"
|
||||
if packetID == 0x3A {
|
||||
packetType = "STAT"
|
||||
} else if packetID == 0x3B {
|
||||
packetType = "SKILL"
|
||||
}
|
||||
log.Printf("[D2GO SEND #%d] Sending %s packet (0x%02x) - %d bytes", sendPacketCallCount, packetType, packetID, len(packet))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.Printf("SendPacket(%d bytes) error: %v", len(packet), err)
|
||||
}
|
||||
}()
|
||||
|
||||
if len(packet) == 0 {
|
||||
return errors.New("packet payload cannot be empty")
|
||||
}
|
||||
|
||||
if len(packet) > maxPacketSize {
|
||||
return fmt.Errorf("packet too large: %d bytes (max %d)", len(packet), maxPacketSize)
|
||||
}
|
||||
|
||||
p.sendPacketMu.Lock()
|
||||
if p.sendPacket == nil {
|
||||
p.sendPacket = &sendPacketState{}
|
||||
}
|
||||
state := p.sendPacket
|
||||
state.mu.Lock()
|
||||
p.sendPacketMu.Unlock()
|
||||
defer state.mu.Unlock()
|
||||
|
||||
// Ensure handle is open (this resets isExiting flag if new game)
|
||||
handle, err := state.ensureHandle(p.pid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open process: %w", err)
|
||||
}
|
||||
|
||||
// If we're exiting, skip all packet sends immediately
|
||||
if state.isExiting {
|
||||
return errors.New("packet send cancelled: game is exiting")
|
||||
}
|
||||
|
||||
// Rate limiting: prevent overwhelming the APC queue
|
||||
const maxPacketsPerSecond = 100
|
||||
|
||||
now := time.Now()
|
||||
if !state.lastSendTime.IsZero() {
|
||||
elapsed := now.Sub(state.lastSendTime)
|
||||
if elapsed < time.Second {
|
||||
if state.sendCount >= maxPacketsPerSecond {
|
||||
return fmt.Errorf("rate limit exceeded: %d packets/sec (max %d)", state.sendCount, maxPacketsPerSecond)
|
||||
}
|
||||
state.sendCount++
|
||||
} else {
|
||||
state.sendCount = 1
|
||||
state.lastSendTime = now
|
||||
}
|
||||
} else {
|
||||
state.lastSendTime = now
|
||||
state.sendCount = 1
|
||||
}
|
||||
|
||||
// Additional protection: minimum delay between consecutive packets
|
||||
if !state.lastSendTime.IsZero() && now.Sub(state.lastSendTime) < time.Millisecond {
|
||||
time.Sleep(time.Millisecond - now.Sub(state.lastSendTime))
|
||||
}
|
||||
|
||||
fnAddr, err := state.ensureFunction(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve D2GS_SendPacket: %w", err)
|
||||
}
|
||||
|
||||
if err := state.ensureStub(handle); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := state.ensureMeta(handle); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := state.ensurePacketBuffer(handle, uintptr(len(packet))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := writeRemoteMemory(handle, state.packet, packet); err != nil {
|
||||
return fmt.Errorf("write remote packet: %w", err)
|
||||
}
|
||||
|
||||
metaBuf := metaBufPool.Get().(*[sendPacketMetaSize]byte)
|
||||
defer func() {
|
||||
*metaBuf = [sendPacketMetaSize]byte{}
|
||||
metaBufPool.Put(metaBuf)
|
||||
}()
|
||||
|
||||
binary.LittleEndian.PutUint64(metaBuf[0:], uint64(fnAddr))
|
||||
binary.LittleEndian.PutUint64(metaBuf[8:], uint64(state.packet))
|
||||
binary.LittleEndian.PutUint64(metaBuf[16:], uint64(len(packet)))
|
||||
binary.LittleEndian.PutUint32(metaBuf[sendPacketStatusOffset:], 0)
|
||||
|
||||
if err := writeRemoteMemory(handle, state.meta, metaBuf[:]); err != nil {
|
||||
return fmt.Errorf("write remote metadata: %w", err)
|
||||
}
|
||||
|
||||
threadHandle, err := state.ensureThreadHandle(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve main thread: %w", err)
|
||||
}
|
||||
|
||||
if err := state.dispatchAPC(threadHandle, state.stub, state.meta); err != nil {
|
||||
return fmt.Errorf("dispatch APC: %w", err)
|
||||
}
|
||||
|
||||
return p.waitForPacketCompletion(handle, state, apcTimeout)
|
||||
}
|
||||
|
||||
// waitForPacketCompletion waits for the APC to complete with the given timeout
|
||||
func (p *Process) waitForPacketCompletion(handle windows.Handle, state *sendPacketState, apcTimeout time.Duration) error {
|
||||
timeout := time.After(apcTimeout)
|
||||
ticker := time.NewTicker(1 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
timeoutMs := int(apcTimeout.Milliseconds())
|
||||
|
||||
var statusBuf [4]byte
|
||||
for {
|
||||
select {
|
||||
case <-timeout:
|
||||
// Try to read status one last time before failing
|
||||
if err := windows.ReadProcessMemory(handle, state.meta+sendPacketStatusOffset, &statusBuf[0], 4, nil); err == nil {
|
||||
status := binary.LittleEndian.Uint32(statusBuf[:])
|
||||
if status == 1 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("packet send timeout: APC not processed within %dms", timeoutMs)
|
||||
case <-ticker.C:
|
||||
if err := windows.ReadProcessMemory(handle, state.meta+sendPacketStatusOffset, &statusBuf[0], 4, nil); err != nil {
|
||||
continue
|
||||
}
|
||||
status := binary.LittleEndian.Uint32(statusBuf[:])
|
||||
if status == 1 {
|
||||
return nil // Success - packet was sent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeRemoteMemory(handle windows.Handle, address uintptr, data []byte) error {
|
||||
@@ -718,3 +1001,103 @@ func alignUp(value, alignment uintptr) uintptr {
|
||||
mask := alignment - 1
|
||||
return (value + mask) &^ mask
|
||||
}
|
||||
|
||||
func createRemoteThread(handle windows.Handle, startAddr, parameter uintptr) (windows.Handle, uint32, error) {
|
||||
var threadID uint32
|
||||
|
||||
ret, _, err := procCreateRemoteThread.Call(
|
||||
uintptr(handle),
|
||||
0, // lpThreadAttributes
|
||||
0, // dwStackSize (default)
|
||||
startAddr, // lpStartAddress
|
||||
parameter, // lpParameter
|
||||
0, // dwCreationFlags (start immediately)
|
||||
uintptr(unsafe.Pointer(&threadID)),
|
||||
)
|
||||
if ret == 0 {
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return 0, 0, errors.New("CreateRemoteThread failed")
|
||||
}
|
||||
|
||||
return windows.Handle(ret), threadID, nil
|
||||
}
|
||||
|
||||
// CONTEXT64 is the AMD64 thread context structure
|
||||
type CONTEXT64 struct {
|
||||
P1Home uint64
|
||||
P2Home uint64
|
||||
P3Home uint64
|
||||
P4Home uint64
|
||||
P5Home uint64
|
||||
P6Home uint64
|
||||
ContextFlags uint32
|
||||
MxCsr uint32
|
||||
SegCs uint16
|
||||
SegDs uint16
|
||||
SegEs uint16
|
||||
SegFs uint16
|
||||
SegGs uint16
|
||||
SegSs uint16
|
||||
EFlags uint32
|
||||
Dr0 uint64
|
||||
Dr1 uint64
|
||||
Dr2 uint64
|
||||
Dr3 uint64
|
||||
Dr6 uint64
|
||||
Dr7 uint64
|
||||
Rax uint64
|
||||
Rcx uint64
|
||||
Rdx uint64
|
||||
Rbx uint64
|
||||
Rsp uint64
|
||||
Rbp uint64
|
||||
Rsi uint64
|
||||
Rdi uint64
|
||||
R8 uint64
|
||||
R9 uint64
|
||||
R10 uint64
|
||||
R11 uint64
|
||||
R12 uint64
|
||||
R13 uint64
|
||||
R14 uint64
|
||||
R15 uint64
|
||||
Rip uint64
|
||||
_ [512]byte // FltSave (XSAVE_FORMAT)
|
||||
VectorRegister [26][16]byte
|
||||
VectorControl uint64
|
||||
DebugControl uint64
|
||||
LastBranchToRip uint64
|
||||
LastBranchFromRip uint64
|
||||
LastExceptionToRip uint64
|
||||
LastExceptionFromRip uint64
|
||||
}
|
||||
|
||||
const CONTEXT_AMD64 = 0x00100000
|
||||
const CONTEXT_CONTROL = CONTEXT_AMD64 | 0x0001
|
||||
const CONTEXT_INTEGER = CONTEXT_AMD64 | 0x0002
|
||||
const CONTEXT_FULL = CONTEXT_CONTROL | CONTEXT_INTEGER
|
||||
|
||||
func getThreadContext(threadHandle windows.Handle, ctx *CONTEXT64) error {
|
||||
ctx.ContextFlags = CONTEXT_FULL
|
||||
ret, _, err := procNtGetContextThread.Call(
|
||||
uintptr(threadHandle),
|
||||
uintptr(unsafe.Pointer(ctx)),
|
||||
)
|
||||
if ret != 0 {
|
||||
return fmt.Errorf("NtGetContextThread failed: 0x%X, %w", ret, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setThreadContext(threadHandle windows.Handle, ctx *CONTEXT64) error {
|
||||
ret, _, err := procNtSetContextThread.Call(
|
||||
uintptr(threadHandle),
|
||||
uintptr(unsafe.Pointer(ctx)),
|
||||
)
|
||||
if ret != 0 {
|
||||
return fmt.Errorf("NtSetContextThread failed: 0x%X, %w", ret, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user