mirror of
https://github.com/ApfelTeeSaft/expresso.git
synced 2026-08-26 19:23:27 +00:00
278 lines
9.8 KiB
C++
278 lines
9.8 KiB
C++
// ue4 reflection stuff
|
|
#include "expresso_recovered.h"
|
|
|
|
#include <cstring>
|
|
|
|
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<uint64_t>(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<uint32_t>(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<uint32_t>(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<char>(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<uint32_t>(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<int32_t>(static_cast<uint32_t>(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<uint32_t>(count - 1) >=
|
|
limits::kMaxLocalPlayers) { // 0x15B03E
|
|
return 0;
|
|
}
|
|
|
|
for (int32_t i = 0; i < count; ++i) {
|
|
const uint64_t player = SafeReadPtr(mem, data + static_cast<uint64_t>(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<uint32_t>(idx) >= limits::kMaxClassNameIdx) // 0x15B8B2
|
|
return false;
|
|
|
|
return GetNameString(mem, game_base, idx, out, out_size);
|
|
}
|
|
|
|
} // namespace expresso
|