// ue4 reflection stuff #include "expresso_recovered.h" #include namespace expresso { // verbatim from RVA 0x15A6A0: // lea rax, [rcx - 0x10001] // mov rdx, 0x7FFFFFFEFFFD // cmp rax, rdx ; unsigned // ja fail // mov rax, 0xFFFF000000000000 // test rax, rcx // jne fail bool IsCanonicalUserPointer(uint64_t addr) { if (static_cast(addr - 0x10001ULL) > 0x7FFFFFFEFFFDULL) return false; if (addr & 0xFFFF000000000000ULL) return false; return true; } uint64_t SafeReadPtr(const IMemory& mem, uint64_t addr) { uint64_t v = 0; if (!IsCanonicalUserPointer(addr)) return 0; if (!mem.Read(addr, &v, sizeof(v))) return 0; return v; } bool SafeReadBool(const IMemory& mem, uint64_t addr) { uint8_t v = 0; if (!IsCanonicalUserPointer(addr)) return false; if (!mem.Read(addr, &v, sizeof(v))) return false; return v != 0; } int32_t SafeReadInt32(const IMemory& mem, uint64_t addr) { int32_t v = 0; if (!IsCanonicalUserPointer(addr)) return 0; if (!mem.Read(addr, &v, sizeof(v))) return 0; return v; } // movss xmm0, [rcx]; 0.0f on failure float SafeReadFloat(const IMemory& mem, uint64_t addr) { float v = 0.0f; if (!IsCanonicalUserPointer(addr)) return 0.0f; if (!mem.Read(addr, &v, sizeof(v))) return 0.0f; return v; } bool SafeWriteInt32(IMemory& mem, uint64_t addr, int32_t v) { if (!IsCanonicalUserPointer(addr)) return false; return mem.Write(addr, &v, sizeof(v)); } // literal `mov byte [rcx], 1` bool SafeSetTrue(IMemory& mem, uint64_t addr) { const uint8_t one = 1; if (!IsCanonicalUserPointer(addr)) return false; return mem.Write(addr, &one, sizeof(one)); } bool SafeWriteFloat(IMemory& mem, uint64_t addr, float v) { if (!IsCanonicalUserPointer(addr)) return false; return mem.Write(addr, &v, sizeof(v)); } bool SafeWritePtr(IMemory& mem, uint64_t addr, uint64_t v) { if (!IsCanonicalUserPointer(addr)) return false; return mem.Write(addr, &v, sizeof(v)); } // RVA 0x15AB00 FName index to string through the chunked pool // // 0x15AB18 lea rcx, [rdx + 0x314A7D0] ; &GNames // 0x15AB25 call SafeReadPtr ; pool // 0x15AB63 shr edi, 0x0E ; chunk index // 0x15AB66 and esi, 0x3FFF ; index within chunk // 0x15AB6C shl edi, 3 ; * sizeof(void*) // 0x15AB74 call SafeReadPtr ; chunk // 0x15AB94 lea ecx, [rsi*8] // 0x15ABA6 call SafeReadPtr ; FNameEntry* // // then decodes the entry into a 0xB0-byte stack buffer bool GetNameString(const IMemory& mem, uint64_t game_base, int32_t name_index, char* out, size_t out_size) { if (out == nullptr || out_size == 0) return false; out[0] = '\0'; if (name_index <= 0 || static_cast(name_index) > limits::kMaxNameIndex) { return false; } const uint64_t pool = SafeReadPtr(mem, game_base + game_rva::kGNames); if (!IsCanonicalUserPointer(pool)) return false; const uint32_t idx = static_cast(name_index); const uint32_t chunk_idx = idx >> fname::kChunkShift; const uint32_t entry_idx = idx & fname::kChunkMask; const uint64_t chunk = SafeReadPtr(mem, pool + chunk_idx * 8u); if (!IsCanonicalUserPointer(chunk)) return false; const uint64_t entry = SafeReadPtr(mem, chunk + entry_idx * 8u); if (!IsCanonicalUserPointer(entry)) return false; // might look different idk, geometry not confirmed only layout const int32_t index_field = SafeReadInt32(mem, entry + 0x00); const bool is_wide = (index_field & 1) != 0; const uint64_t text = entry + 0x10; size_t n = 0; if (is_wide) { for (; n + 1 < out_size; ++n) { uint16_t wc = 0; if (!mem.Read(text + n * 2, &wc, sizeof(wc))) break; if (wc == 0) break; out[n] = (wc < 0x80) ? static_cast(wc) : '?'; } } else { for (; n + 1 < out_size; ++n) { char c = 0; if (!mem.Read(text + n, &c, sizeof(c))) break; if (c == '\0') break; out[n] = c; } } out[n] = '\0'; return n != 0; } // RVA 0x15C210, resolve a UFunction by name on an object's class chain uint64_t FindFunctionByName(const IMemory& mem, uint64_t game_base, uint64_t object, const char* name) { if (name == nullptr) return 0; if (!IsCanonicalUserPointer(object)) return 0; uint64_t cls = SafeReadPtr(mem, object + uobject::kClassPrivate); for (uint32_t depth = 0; depth < limits::kMaxClassDepth; ++depth) { if (!IsCanonicalUserPointer(cls)) return 0; uint64_t field = SafeReadPtr(mem, cls + uobject::kChildren); for (uint32_t i = 0; i < limits::kMaxFieldsPerCls; ++i) { if (!IsCanonicalUserPointer(field)) break; const int32_t name_index = SafeReadInt32(mem, field + uobject::kNameIndex); // 0x15C2DF: lea ecx,[rax-1] / cmp ecx,0x1FFFFE / ja skip if (static_cast(name_index - 1) <= limits::kMaxNameIndex - 1) { char buf[0xB0]; if (GetNameString(mem, game_base, name_index, buf, sizeof(buf)) && std::strcmp(buf, name) == 0) { return field; // 0x15C387: mov rax, rdi } } field = SafeReadPtr(mem, field + uobject::kFieldNext); } cls = SafeReadPtr(mem, cls + uobject::kSuperStruct); } return 0; } // RVA 0x15C130 invocation primitive. // // 0x15C19E lea rcx, [rbx + 0x88] ; &fn->FunctionFlags // 0x15C1A5 call SafeReadInt32 ; saved // 0x15C1B3 or edx, r12d ; saved | orFlags // 0x15C1BD call SafeWriteInt32 // 0x15C1CB call r15 ; ProcessEvent(obj, fn, params) // 0x15C1D8 call SafeWriteInt32 ; restore saved bool CallUFunction(IMemory& mem, uint64_t object, uint64_t function, void* params, uint32_t or_flags, ProcessEventFn invoke, void* user) { if (invoke == nullptr) return false; // 0x15C15F if (!IsCanonicalUserPointer(object)) return false; // 0x15C168 if (!IsCanonicalUserPointer(function)) return false; // 0x15C18D const uint64_t flags_addr = function + uobject::kFunctionFlags; const int32_t saved = SafeReadInt32(mem, flags_addr); SafeWriteInt32(mem, flags_addr, static_cast(static_cast(saved) | or_flags)); invoke(object, function, params, user); SafeWriteInt32(mem, flags_addr, saved); return true; } // RVA 0x15AE60 cached local PC uint64_t GetLocalPlayerController(const IMemory& mem, uint64_t game_base, LocalPlayerCache& cache, uint64_t now_ms) { const uint64_t world = SafeReadPtr(mem, game_base + game_rva::kGWorld); if (!IsCanonicalUserPointer(world)) return 0; // 0x15AEBD // 0x15AF00: reuse the cache only while world is unchanged and the entry // is younger than 2000 ms if (IsCanonicalUserPointer(cache.controller) && cache.world == world && now_ms - cache.timestamp < limits::kPcCacheMs) { if (IsCanonicalUserPointer(SafeReadPtr(mem, cache.controller))) return cache.controller; } const uint64_t game_instance = SafeReadPtr(mem, world + world::kOwningGameInstance); if (!IsCanonicalUserPointer(game_instance)) return 0; const uint64_t local_players = SafeReadPtr(mem, game_instance + world::kLocalPlayers); if (!IsCanonicalUserPointer(local_players)) return 0; // Fast path (0x15AF7F): treat local_players as the array data pointer and // take element 0 directly { const uint64_t player = SafeReadPtr(mem, local_players); if (IsCanonicalUserPointer(player)) { const uint64_t pc = SafeReadPtr(mem, player + world::kPlayerController); if (IsCanonicalUserPointer(pc) && IsCanonicalUserPointer(SafeReadPtr(mem, pc))) { cache.controller = pc; // 0x15AFD8 cache.world = world; // 0x15AFDF cache.timestamp = now_ms; // 0x15AFE6 return pc; } } } // Slow path (0x15B004): treat local_players as &TArray{Data, Num} and walk const uint64_t data = SafeReadPtr(mem, local_players); const int32_t count = SafeReadInt32(mem, local_players + 8); if (!IsCanonicalUserPointer(data)) return 0; if (count <= 0 || static_cast(count - 1) >= limits::kMaxLocalPlayers) { // 0x15B03E return 0; } for (int32_t i = 0; i < count; ++i) { const uint64_t player = SafeReadPtr(mem, data + static_cast(i) * 8); if (!IsCanonicalUserPointer(player)) continue; const uint64_t pc = SafeReadPtr(mem, player + world::kPlayerController); if (!IsCanonicalUserPointer(pc)) continue; if (!IsCanonicalUserPointer(SafeReadPtr(mem, pc))) continue; cache.controller = pc; cache.world = world; cache.timestamp = now_ms; return pc; } return 0; } // ClassPrivate -> NamePrivate -> string // // lea rcx, [obj + 0x10] ; SafeReadPtr -> UClass* // lea rcx, [cls + 0x18] ; SafeReadInt32 -> FName::ComparisonIndex // GetNameString(&buf, gameBase, idx) bool GetObjectClassName(const IMemory& mem, uint64_t game_base, uint64_t object, char* out, size_t out_size) { if (out == nullptr || out_size == 0) return false; out[0] = '\0'; if (!IsCanonicalUserPointer(object)) return false; const uint64_t cls = SafeReadPtr(mem, object + uobject::kClassPrivate); if (!IsCanonicalUserPointer(cls)) return false; const int32_t idx = SafeReadInt32(mem, cls + uobject::kNameIndex); if (idx == 0) return false; // 0x15B8AA if (static_cast(idx) >= limits::kMaxClassNameIdx) // 0x15B8B2 return false; return GetNameString(mem, game_base, idx, out, out_size); } } // namespace expresso