Skip to content

Engine Adapters

The EngineAdapter abstraction is what makes QS-Bridge universal — each game engine gets its own adapter implementation while sharing the same bridge core.


Architecture

graph TD
    EA[EngineAdapter — abstract]
    EA --> UE4[UE4Adapter<br/>Production — UE4 4.27, vtable hooking]
    EA --> UE5[UE5Adapter<br/>Planned]
    EA --> UNITY[UnityAdapter<br/>Planned — IL2CPP function hooking]
    EA --> GODOT[GodotAdapter<br/>Planned — GDExtension API]
    EA --> MOCK[MockAdapter<br/>Testing — headless, no game engine]

Abstract Interface

// include/qsbridge/engine_adapter.h (164 lines)

class EngineAdapter {
public:
    virtual ~EngineAdapter() = default;

    // --- Lifecycle ---
    virtual bool init() = 0;
    virtual void shutdown() = 0;
    virtual void tick(float delta_time) = 0;

    // --- Identity ---
    virtual std::string get_engine_name() const = 0;
    virtual std::string get_engine_version() const = 0;

    // --- Hook Management ---
    virtual bool hook_function(const char* function_name, void* callback) = 0;
    virtual bool unhook_function(const char* function_name) = 0;
    virtual bool is_hooked(const char* function_name) const = 0;

    // --- Player Tracking ---
    virtual std::vector<PlayerInfo> get_connected_players() const = 0;
    virtual std::string extract_player_id(void* player_controller) const = 0;
    virtual void* find_player_controller(const std::string& player_id) const = 0;

    // --- Memory Access ---
    virtual bool read_memory(void* base, size_t offset, void* out, size_t size) const = 0;
    virtual bool write_memory(void* base, size_t offset, const void* data, size_t size) = 0;

    // --- Object System ---
    virtual void* find_object_by_name(const char* class_name) const = 0;
    virtual std::string get_object_class_name(void* object) const = 0;
};

UE4 Adapter (Production)

Location: lib/engines/ue4/ (4 source files)

File Purpose Key Functions
entry.cpp LD_PRELOAD constructor, bootstrap __attribute__((constructor))
introspection.cpp UE4 memory scanning, FProperty walks find_property_offset()
vtable_hook.cpp vtable-based function hooking hook_virtual(), unhook_virtual()
fname_cache.cpp FName string resolution cache resolve_fname()

Hooking Method

UE4 uses virtual function tables (vtables) for polymorphism. The adapter replaces vtable entries to intercept function calls:

// Simplified vtable hook
void hook_virtual(void* object, int vtable_index, void* hook_function) {
    void** vtable = *(void***)object;

    // Save original function pointer
    original_functions_[vtable_index] = vtable[vtable_index];

    // Make vtable writable
    mprotect(page_align(vtable), PAGE_SIZE, PROT_READ | PROT_WRITE);

    // Replace with hook
    vtable[vtable_index] = hook_function;

    // Restore protection
    mprotect(page_align(vtable), PAGE_SIZE, PROT_READ);
}

Key Hooks

UE4 Function Hook Purpose
UObject::ProcessEvent Intercept all RPC calls
UWorld::SpawnActor Track actor creation
AActor::Destroyed Track actor destruction
UWorld::Tick Inject bridge tick into engine loop
AGameMode::PostLogin Player connection
AGameMode::Logout Player disconnection

Mock Adapter (Testing)

Location: include/qsbridge/engines/mock_adapter.h

Headless adapter for unit and integration testing. No game engine required.

class MockAdapter : public EngineAdapter {
    std::vector<MockPlayer> mock_players_;

public:
    // Simulate player events
    void add_mock_player(const std::string& id, const std::string& name);
    void remove_mock_player(const std::string& id);
    void simulate_rpc(const std::string& function_name, const void* params);

    // EngineAdapter implementation (all functional, no game engine)
    bool init() override { return true; }
    void tick(float dt) override { /* update mock state */ }
    // ...
};

Tests: 17 assertions in test_mock_adapter.cpp — verifying player tracking, RPC simulation, and adapter lifecycle.

Adding a New Engine Adapter

  1. Create include/qsbridge/engines/my_engine.h and lib/engines/my_engine/
  2. Implement EngineAdapter interface
  3. Add to CMake build as a new static library layer
  4. Register in entry.cpp engine detection logic

Engine Detection

// Detect engine from the loaded process
EngineAdapter* detect_engine() {
    if (is_ue4_process())  return new UE4Adapter();
    if (is_ue5_process())  return new UE5Adapter();
    if (is_unity_process()) return new UnityAdapter();
    if (is_godot_process()) return new GodotAdapter();
    return nullptr;  // Unknown engine
}