Skip to main content
The Address Resolver is a high-performance signature scanning system that locates native code patterns in memory and resolves function pointers and static addresses at runtime.

Overview

The resolver:
  • Scans the .text section of native modules for byte patterns
  • Supports wildcard patterns (?? for unknown bytes)
  • Follows relative jumps/calls to locate final addresses
  • Caches results for fast subsequent launches
  • Registers all Address objects generated by InteropGenerator

Core API

Resolver.GetInstance

Access the singleton resolver instance:
var resolver = Resolver.GetInstance;
Type: Resolver
Thread-safety: Singleton is thread-safe, but setup/resolve should be called once

Setup Methods

Setup (Auto-detect)

Automatically detects the current process’s main module:
public void Setup(
    nint moduleCopyPointer = 0,
    string version = "",
    FileInfo? cacheFile = null
)
Parameters:
  • moduleCopyPointer - Optional pointer to a copy of the module in memory (for scanning without touching live memory)
  • version - Version string for cache validation (e.g., game version)
  • cacheFile - Optional file path for caching resolved addresses
Example:
var resolver = Resolver.GetInstance;
var cacheFile = new FileInfo("sig_cache.json");
resolver.Setup(
    moduleCopyPointer: 0,
    version: "2024.01.01.0000.0000",
    cacheFile: cacheFile
);

Setup (Manual Module)

Manually specify the module to scan:
public void Setup(
    nint modulePointer,
    int moduleSize,
    nint moduleCopyPointer = 0,
    string version = "",
    FileInfo? cacheFile = null
)
Parameters:
  • modulePointer - Base address of the native module
  • moduleSize - Size of the module in bytes
  • moduleCopyPointer - Optional pointer to a memory copy for scanning
  • version - Version string for cache validation
  • cacheFile - Optional cache file path
Example:
var module = Process.GetCurrentProcess().MainModule!;
resolver.Setup(
    modulePointer: module.BaseAddress,
    moduleSize: module.ModuleMemorySize,
    version: gameVersion,
    cacheFile: new FileInfo(Path.Combine(configDir, "addresses.json"))
);

Setup (Manual Text Section)

Directly specify the .text section for maximum control:
public void Setup(
    nint memoryPointer,
    int memorySize,
    int textSectionOffset,
    int textSectionSize,
    string version = "",
    FileInfo? cacheFile = null
)
Parameters:
  • memoryPointer - Base address of the memory region
  • memorySize - Total size of the memory region
  • textSectionOffset - Offset to the .text section
  • textSectionSize - Size of the .text section
  • version - Version string for cache validation
  • cacheFile - Optional cache file path
Use when:
  • You’ve already parsed PE headers
  • Working with non-standard module layouts
  • Need precise control over scan regions

Address Registration

RegisterAddress

Register an Address object for resolution:
public void RegisterAddress(Address address)
Example:
var myAddress = new Address(
    name: "MyFunction",
    signature: "E8 ?? ?? ?? ?? 48 8B C8",
    relativeFollowOffsets: Array.Empty<ushort>(),
    bytes: new ulong[] { 0xC88B48000000E8 },
    mask: new ulong[] { 0xFFFFFF0000FFFF },
    value: 0
);

resolver.RegisterAddress(myAddress);
Note: InteropGenerator automatically creates a registration method that registers all generated addresses. You typically don’t need to call this manually.

UnregisterAddress

Remove an address from the resolver:
public void UnregisterAddress(Address address)
Example:
resolver.UnregisterAddress(myAddress);
Use when:
  • Dynamically unloading code
  • Cleaning up temporary addresses
  • Managing address lifetime manually

Resolution

Resolve

Scan memory and resolve all registered addresses:
public void Resolve()
Example:
var resolver = Resolver.GetInstance;
resolver.Setup(version: gameVersion, cacheFile: cacheFile);
AddressResolver.Register(); // Generated by InteropGenerator
resolver.Resolve();

// All addresses are now resolved
var instance = ActionManager.Instance(); // Uses resolved address
Process:
  1. Check cache for previously resolved addresses
  2. Scan .text section for remaining signatures
  3. Follow relative offsets to final addresses
  4. Update cache with new results
  5. Save cache to disk (if configured)
Performance:
  • First run: Full scan (typically 50-500ms depending on module size)
  • Cached runs: Near-instant (1-10ms)
  • Incremental: Only scans for missing addresses

Address Object

Address Structure

Represents a signature pattern and its resolved address:
public sealed class Address {
    public readonly string Name;                    // Identifier for debugging
    public readonly string String;                  // Original signature pattern
    public readonly ushort[] RelativeFollowOffsets; // Offsets for relative addressing
    public readonly ulong[] Bytes;                  // Pattern as ulong array
    public readonly ulong[] Mask;                   // Mask for wildcards
    public nint Value;                              // Resolved address (0 if not resolved)
    public readonly string CacheKey;                // Cache lookup key
}
Example:
var address = new Address(
    name: "ActionManager.Instance",
    signature: "48 8D 0D ?? ?? ?? ?? F3 0F 10 13",
    relativeFollowOffsets: new ushort[] { 3 },
    bytes: new ulong[] { 0x100FF3000D8D48, 0x0000000000130F },
    mask: new ulong[] { 0xFFFF00FFFFFFFF, 0x00000000FFFFFF },
    value: 0
);

RelativeFollowOffsets

Array of offsets to follow relative addresses (common in x64 code): Example pattern:
; Signature: 48 8D 0D ?? ?? ?? ??
; Offset:    0  1  2  3  4  5  6
lea rcx, [rip + offset]  ; RelativeFollowOffsets = [3]
Process:
  1. Find pattern in memory
  2. Read 4-byte relative offset at position + 3
  3. Calculate final address: position + 3 + 4 + relative_offset
  4. Set Value to final address
Multiple offsets:
// Follow two relative addresses
relativeFollowOffsets: new ushort[] { 3, 5 }

Caching

Cache File Format

The cache is stored as JSON:
{
  "Version": "2024.01.01.0000.0000",
  "Cache": {
    "YourNamespace.ActionManager.Instance": 5234567,
    "YourNamespace.CharacterManager.LookupByEntityId": 5234890,
    "YourNamespace.Example.DoSomething+relfollow[3]": 5235100
  }
}
Cache keys:
  • Simple signatures: {signature}
  • With relative follow: {signature}+relfollow[{offset1},{offset2}]
Version validation:
  • Cache is invalidated if version string doesn’t match
  • Prevents using stale addresses after game updates

Cache Performance

Benefits:
  • 100x+ faster startup after first launch
  • Reduces memory scanning overhead
  • Persists across application restarts
Invalidation:
  • Version mismatch
  • Cache file corruption
  • Manual deletion
Example:
var cacheFile = new FileInfo("game_v1.0.0_addresses.json");
resolver.Setup(
    version: "1.0.0",
    cacheFile: cacheFile
);
resolver.Resolve();
// Cache is automatically saved after resolution

Complete Workflow

Basic Setup

using InteropGenerator.Runtime;

// 1. Get resolver instance
var resolver = Resolver.GetInstance;

// 2. Setup with cache
var cacheFile = new FileInfo("addresses.json");
resolver.Setup(
    version: "2024.01.01.0000.0000",
    cacheFile: cacheFile
);

// 3. Register all generated addresses
AddressResolver.Register();

// 4. Resolve addresses
resolver.Resolve();

// 5. Use resolved addresses
var actionManager = ActionManager.Instance();
actionManager->CastSpellId = 1234;

Manual Module Scanning

using System.Diagnostics;
using InteropGenerator.Runtime;

// Get process module
var process = Process.GetCurrentProcess();
var module = process.MainModule!;

// Setup resolver
var resolver = Resolver.GetInstance;
resolver.Setup(
    modulePointer: module.BaseAddress,
    moduleSize: module.ModuleMemorySize,
    version: GetGameVersion(),
    cacheFile: new FileInfo("cache.json")
);

// Register and resolve
AddressResolver.Register();
resolver.Resolve();

With Module Copy

using System.Runtime.InteropServices;

// Allocate memory copy for safer scanning
var module = Process.GetCurrentProcess().MainModule!;
var copyPointer = Marshal.AllocHGlobal(module.ModuleMemorySize);
try {
    // Copy module to separate memory
    Buffer.MemoryCopy(
        module.BaseAddress.ToPointer(),
        copyPointer.ToPointer(),
        module.ModuleMemorySize,
        module.ModuleMemorySize
    );
    
    // Setup resolver with copy
    var resolver = Resolver.GetInstance;
    resolver.Setup(
        modulePointer: module.BaseAddress,
        moduleSize: module.ModuleMemorySize,
        moduleCopyPointer: copyPointer,
        version: gameVersion,
        cacheFile: cacheFile
    );
    
    AddressResolver.Register();
    resolver.Resolve();
}
finally {
    Marshal.FreeHGlobal(copyPointer);
}

Error Handling

Common Issues

Unresolved addresses:
resolver.Resolve();

// Check if address was resolved
var address = ActionManager.Addresses.Instance;
if (address.Value == 0) {
    throw new Exception($"Failed to resolve {address.Name}");
}
Missing .text section:
try {
    resolver.Setup();
}
catch (Exception ex) {
    Console.WriteLine($"Setup failed: {ex.Message}");
    // Fallback or error handling
}
Cache corruption:
var cacheFile = new FileInfo("addresses.json");
if (cacheFile.Exists && !IsValidCache(cacheFile)) {
    cacheFile.Delete();
}
resolver.Setup(cacheFile: cacheFile);

Performance Optimization

Minimize Signatures

Use unique, short signatures:
// Good: Short and unique
[MemberFunction("E8 ?? ?? ?? ?? 48 8B C8")]

// Bad: Too long, slower to scan
[MemberFunction("E8 ?? ?? ?? ?? 48 8B C8 48 85 C0 74 ?? 48 8B 10 48 8B C8 FF 52 ?? 48 8B C8")]

Cache Everything

Always use cache files in production:
var cacheFile = new FileInfo(Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
    "YourApp",
    $"addresses_{gameVersion}.json"
));
resolver.Setup(version: gameVersion, cacheFile: cacheFile);

Reduce Address Count

Only register addresses you actually use:
// Don't register unused addresses
if (enableFeatureX) {
    resolver.RegisterAddress(FeatureX.Addresses.SomeFunction);
}

Advanced Usage

Custom Address Registration

// Create custom address
var customAddress = new Address(
    name: "CustomFunction",
    signature: "48 89 5C 24 ?? 57 48 83 EC ??",
    relativeFollowOffsets: Array.Empty<ushort>(),
    bytes: ParseSignatureToBytes("48 89 5C 24 ?? 57 48 83 EC ??"),
    mask: ParseSignatureToMask("48 89 5C 24 ?? 57 48 83 EC ??"),
    value: 0
);

resolver.RegisterAddress(customAddress);
resolver.Resolve();

// Use resolved address
var funcPtr = (delegate* unmanaged<void>)customAddress.Value;
funcPtr();

Partial Resolution

// Resolve only critical addresses first
var criticalAddresses = new[] {
    ActionManager.Addresses.Instance,
    CharacterManager.Addresses.Instance
};

foreach (var addr in criticalAddresses) {
    resolver.RegisterAddress(addr);
}
resolver.Resolve();

// Later, resolve remaining addresses
AddressResolver.Register();
resolver.Resolve();

See Also

Build docs developers (and LLMs) love