Skip to main content

Overview

The HotWheels SDK provides comprehensive memory manipulation utilities for:

Pattern Scanning

Find functions and data in game modules

Netvars

Access network variables and offsets

Address Helpers

Navigate and manipulate memory addresses

Module System

Module Class

The module system provides utilities for working with game DLLs:
namespace modules
{
    class impl
    {
    public:
        impl() = default;
        explicit impl(const char* mod_name)
        {
            module_name = mod_name;
            module_handle = get_module_handle_safe(mod_name);
        };
        
        address pattern_scan(const char* sig);
        
        template<class T>
        T find_interface(const char* interface_name);
        
        constexpr HMODULE get_module_handle_safe(const char* name);
        
        HMODULE module_handle{};
        
    private:
        const char* module_name{};
        
        std::uintptr_t find_pattern(
            std::uint8_t* region_start,
            std::uintptr_t region_size,
            const char* pattern
        );
        
        std::vector<int> pattern_to_bytes(const char* pattern);
    };
}
Source: utils/modules/modules.h:16-43

Global Module Instances

Pre-initialized module instances for common game DLLs:
inline modules::impl g_client_dll = modules::impl(CLIENT_DLL);
inline modules::impl g_engine_dll = modules::impl(ENGINE_DLL);
inline modules::impl g_localize_dll = modules::impl(LOCALIZE_DLL);
inline modules::impl g_shaderapidx9_dll = modules::impl(SHADERAPIDX9_DLL);
inline modules::impl g_vstdlib_dll = modules::impl(VSTDLIB_DLL);
inline modules::impl g_server_dll = modules::impl(SERVER_DLL);
inline modules::impl g_studio_render_dll = modules::impl(STUDIORENDER_DLL);
inline modules::impl g_material_system_dll = modules::impl(MATERIALSYSTEM_DLL);
inline modules::impl g_vgui_dll = modules::impl(VGUI_DLL);
inline modules::impl g_vgui2_dll = modules::impl(VGUI2_DLL);
inline modules::impl g_game_overlay_renderer_dll = modules::impl(GAMEOVERLAYRENDERER_DLL);
inline modules::impl g_physics_dll = modules::impl(PHYSICS_DLL);
inline modules::impl g_tier0_dll = modules::impl(TIER0_DLL);
inline modules::impl g_input_system_dll = modules::impl(INPUTSYSTEM_DLL);
inline modules::impl g_data_cache_dll = modules::impl(DATACACHE_DLL);
inline modules::impl g_steam_api_dll = modules::impl(STEAM_API_DLL);
inline modules::impl g_matchmaking_dll = modules::impl(MATCHMAKING_DLL);
inline modules::impl g_file_system_stdio_dll = modules::impl(FILESYSTEM_DLL);
Source: utils/modules/modules.h:47-64
These global instances are automatically initialized with their respective module handles, making them ready to use immediately.

Pattern Scanning

How Pattern Scanning Works

Pattern scanning searches for byte sequences in memory to locate functions or data:
1

Pattern Conversion

Convert the pattern string to bytes:
std::vector<int> modules::impl::pattern_to_bytes(const char* pattern)
{
    std::vector<int> vec_bytes = {};
    char* start = const_cast<char*>(pattern);
    char* end = start + strlen(pattern);
    
    for (char* current = start; current < end; ++current) {
        if (*current == '?') {
            ++current;
            if (*current == '?')
                ++current;
            vec_bytes.push_back(-1);  // Wildcard
        } else {
            // Convert byte to hex
            vec_bytes.push_back(strtoul(current, &current, 16));
        }
    }
    
    return vec_bytes;
}
Source: utils/modules/modules.cpp:35-55
2

Memory Scanning

Search through the module’s memory:
std::uintptr_t modules::impl::find_pattern(
    std::uint8_t* region_start,
    std::uintptr_t region_size,
    const char* pattern
)
{
    std::vector<int> vec_bytes = pattern_to_bytes(pattern);
    
    for (unsigned long i = 0UL; i < region_size - vec_bytes.size(); ++i) {
        bool byte_found = true;
        
        for (unsigned long s = 0UL; s < vec_bytes.size(); ++s) {
            if (region_start[i + s] != vec_bytes[s] && vec_bytes[s] != -1) {
                byte_found = false;
                break;
            }
        }
        
        if (byte_found)
            return reinterpret_cast<std::uintptr_t>(&region_start[i]);
    }
    
    console::print<console::log_level::FATAL>(
        _("Find pattern failed - [ {} ]"),
        pattern
    );
    return 0U;
}
Source: utils/modules/modules.cpp:16-33
3

Address Retrieval

Return the found address:
address modules::impl::pattern_scan(const char* sig)
{
    if (!module_handle)
        console::print<console::log_level::FATAL>(
            _("Failed to get handle - [ {} ]"),
            module_name
        );
        
    const auto module_address = reinterpret_cast<std::uint8_t*>(module_handle);
    const auto dos_header = reinterpret_cast<PIMAGE_DOS_HEADER>(module_handle);
    const auto nt_headers = reinterpret_cast<PIMAGE_NT_HEADERS>(
        module_address + dos_header->e_lfanew
    );
    const std::uintptr_t offset = find_pattern(
        module_address,
        nt_headers->OptionalHeader.SizeOfImage,
        sig
    );
    
    return offset;
}
Source: utils/modules/modules.cpp:3-14

Pattern Syntax

Patterns use hexadecimal bytes with wildcards:
// Find exact sequence
auto addr = g_client_dll.pattern_scan("55 8B EC 56 8B F1");
Source: hooks/hooks.inl:34-35
Pattern scanning can fail if the game is updated. Always verify patterns after game updates and handle failed scans gracefully.

Address Helper Class

Address Wrapper

The address class provides convenient memory navigation:
struct address {
protected:
    std::uintptr_t m_address{};
    
public:
    constexpr address() : m_address{0} {};
    address(std::uintptr_t ad) : m_address{ad} {};
    address(void* address) : m_address{reinterpret_cast<std::uintptr_t>(address)} {};
    
    operator std::uintptr_t() const { return m_address; }
    operator void*() const { return reinterpret_cast<void*>(m_address); }
    
    template<typename T = address>
    T to() const { return *(T*)(m_address); }
    
    template<typename T = address>
    T as() const { return m_address ? (T)(m_address) : T(); }
    
    template<typename T = address>
    T at(std::ptrdiff_t offset) const { return m_address ? *(T*)(m_address + offset) : T(); }
    
    template<typename T = address>
    T add(std::ptrdiff_t offset) const { return m_address ? (T)(m_address + offset) : T(); }
    
    template<typename T = address>
    T sub(std::ptrdiff_t offset) const { return m_address ? (T)(m_address - offset) : T(); }
    
    template<typename T = address>
    T get(std::uint8_t dereferences = 1) const;
    
    template<typename T = std::uintptr_t>
    void set(const T& value);
    
    template<typename T = address>
    T relative(std::ptrdiff_t offset = 0x1) const;
};
Source: utils/modules/address/address.h:6-95

Address Methods

Convert addresses to different types:
auto addr = g_client_dll.pattern_scan("55 8B EC");

// Cast to pointer
void* ptr = addr.as<void*>();

// Cast to function pointer
using fn_t = void(*)();
auto fn = addr.as<fn_t>();

// Dereference and cast
int value = addr.to<int>();

Network Variables (Netvars)

What are Netvars?

Netvars are network-synchronized variables used by Source Engine to replicate entity data between server and client.

Netvar System

namespace netvar
{
    std::uintptr_t get_offset(sdk::recv_table* table, const char* var);
    std::uintptr_t get_table(const char* table, const char* var);
    std::uintptr_t get_table(sdk::data_map* table, const char* var);
}
Source: utils/netvar/netvar.h:13-21

Finding Offsets

1

Recursive Table Search

Search through receive tables recursively:
std::uintptr_t netvar::get_offset(sdk::recv_table* table, const char* var)
{
    for (std::int32_t i = 0; i < table->props; i++) {
        auto current_prop = table->props_ptr[i];
        
        // Check if this is the property we want
        if (HASH(var) == HASH(current_prop.var_name))
            return current_prop.offset;
            
        // Recursively search sub-tables
        if (auto data_table = current_prop.data_table) {
            std::uintptr_t offset = get_offset(data_table, var);
            
            if (offset)
                return offset + current_prop.offset;
        }
    }
    
    return std::uintptr_t();
}
Source: utils/netvar/netvar.cpp:13-30
2

Find Table by Name

Locate the correct receive table:
std::uintptr_t netvar::get_table(const char* table, const char* var)
{
    auto classes = g_interfaces.client->get_all_classes();
    
    for (auto current_table = classes; current_table; current_table = current_table->next) {
        if (HASH(table) == HASH(current_table->network_name))
            return get_offset(current_table->recv_table, var);
    }
    
    return std::uintptr_t();
}
Source: utils/netvar/netvar.cpp:32-42

Usage Examples

// Get offset for player health
auto health_offset = netvar::get_table("DT_BasePlayer", "m_iHealth");

// Access health
int health = *reinterpret_cast<int*>(
    reinterpret_cast<uintptr_t>(player) + health_offset
);
Netvars are cached on first access. The offset lookup only happens once, then the result is stored in a static variable.

Safe Module Loading

Module Handle Acquisition

The SDK includes a safe module handle getter that waits for the module to load:
constexpr HMODULE modules::impl::get_module_handle_safe(const char* name)
{
    HMODULE return_module{};
    
    while (!return_module) {
        return_module = LI_FN(GetModuleHandleA)(name);
        utils::sleep(1);
    }
    
    return return_module;
}
Source: utils/modules/modules.cpp:56-66
This function will block until the module is loaded. Use it during initialization, not in time-sensitive code.

Best Practices

Always verify patterns are unique and won’t match multiple locations:
// Good - specific and unique
auto addr = g_client_dll.pattern_scan(
    "55 8B EC 83 E4 F8 83 EC 18 56 57 8B F9"
);

// Bad - too short, might match multiple places
auto addr = g_client_dll.pattern_scan("55 8B EC");
Handle failed pattern scans:
auto addr = g_client_dll.pattern_scan("55 8B EC ...");
if (!addr) {
    console::print<console::log_level::FATAL>(
        "Failed to find pattern"
    );
    return false;
}
Cache netvar offsets in static variables:
int& get_health()
{
    static auto offset = netvar::get_table("DT_BasePlayer", "m_iHealth");
    return *reinterpret_cast<int*>(
        reinterpret_cast<uintptr_t>(this) + offset
    );
}
Always validate addresses before use:
auto addr = g_client_dll.pattern_scan("...");
if (addr && addr.as<void*>()) {
    // Safe to use
    auto fn = addr.as<function_t>();
}

Common Patterns

Finding Interfaces

Interfaces can be found using the module’s interface list:
namespace modules
{
    struct interface_node {
    public:
        void* (*get)();
        const char* name;
        interface_node* next;
    };
    
    template<class T>
    T find_interface(const char* interface_name);
}
Source: utils/modules/modules.h:9-29

Memory Reading Pattern

// Find and read a value
auto addr = g_client_dll.pattern_scan("A1 ?? ?? ?? ??");
auto value = addr.add(1).to<int>();  // Skip opcode, read value

Troubleshooting

Symptoms: Pattern scan returns null or crashesSolutions:
  • Verify pattern is correct for your game version
  • Check if pattern is too short (needs to be unique)
  • Use a disassembler to find the correct pattern
  • Add more context bytes to make pattern unique

Next Steps

Hooking

Learn how to use found addresses for hooking

Architecture

Understand how memory utilities fit in the SDK

Build docs developers (and LLMs) love