From 2a71ff1519150ed5e09f5c1e56953b4603940331 Mon Sep 17 00:00:00 2001 From: ApfelTeeSaft <91074565+ApfelTeeSaft@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:11:11 +0200 Subject: [PATCH] LarpForestGreen --- engine_gameplay.cpp | 176 ++++++++++++++++++++++++++ engine_localplayer.cpp | 154 +++++++++++++++++++++++ expresso_recovered.h | 217 ++++++++++++++++++++++++++++++++ ue4_reflection.cpp | 278 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 825 insertions(+) create mode 100644 engine_gameplay.cpp create mode 100644 engine_localplayer.cpp create mode 100644 expresso_recovered.h create mode 100644 ue4_reflection.cpp diff --git a/engine_gameplay.cpp b/engine_gameplay.cpp new file mode 100644 index 0000000..0c99586 --- /dev/null +++ b/engine_gameplay.cpp @@ -0,0 +1,176 @@ +// i'm so fucking tired dude +#include "expresso_recovered.h" + +#include + +namespace expresso { + +// SERVER_ funcs used by this POS +const char* const kRecoveredServerRpcs[] = { + "SERVER_OnBlock", // 0x59A3D8 + "SERVER_Attack", // 0x59A3E8 + "SERVER_Dodge", // 0x59A3F8 + "SERVER_SetCombatStance", // 0x59A408 + "SERVER_FillAbilities", // 0x59A420 + "SERVER_ActivateRage", // 0x59A440 + "SERVER_SetShift", // 0x59A458 + "SERVER_SetCharacterDisabled", // 0x59A480 + "SERVER_LockFearTo", // 0x59A4A0 + "SERVER_SetSprintingWithButtonState", // 0x59A4B8 + "SERVER_DeadFriendlySpotted", // 0x59A4E0 + "SERVER_UseCurrentSmallItem", // 0x59A568 + "SERVER_UseCurrentLargeItem", // 0x59A588 + "SERVER_DestroyDoor", // 0x59A660 + "SERVER_DeactivateLights", // 0x59A6A8 + "SERVER_NotifyMinigameFail", // 0x59A748 + "SERVER_NotifyKillerOfMinigameFail", // 0x59A768 + "SERVER_OpenDoor", // 0x59A798 + "SERVER_ForceStopInteractAnim", // 0x59A7A8 + "SERVER_FinishInteractAnim", // 0x59A7C8 + "SERVER_CancelInteractAnim", // 0x59A7E8 + "SERVER_OnMorph", // 0x59B3C0 +}; +const size_t kRecoveredServerRpcCount = + sizeof(kRecoveredServerRpcs) / sizeof(kRecoveredServerRpcs[0]); + +// RVA 0x160B80 shape every SERVER_* call site shares +// +// 0x160BB5 cmp qword [g_ProcessEvent], 0 ; resolve once +// 0x160BBF lea rax, [rbx + 0x7A8010] ; gameBase + kProcessEvent +// 0x160BD4 lea r8, "SERVER_SetShift" +// 0x160BE1 call FindFunctionByName +// 0x160BE6 +// 0x160BF7 mov r9d, 0x40400 ; FUNC_Native | FUNC_Private +// 0x160BFD mov byte [rsp+0x20], 1 ; params.bEnable = true +// 0x160C0D call CallUFunction +bool InvokeServerRpc(IMemory& mem, uint64_t game_base, uint64_t target_object, + const ServerRpcCall& call, + ProcessEventFn invoke, void* user) { + if (call.function_name == nullptr) return false; + if (!IsCanonicalUserPointer(target_object)) return false; + + const uint64_t fn = + FindFunctionByName(mem, game_base, target_object, call.function_name); + if (!IsCanonicalUserPointer(fn)) return false; // 0x160BF0 / 0x160BF5 + + unsigned char frame[256] = {0}; + const size_t n = (call.params_size < sizeof(frame)) ? call.params_size + : sizeof(frame); + if (call.params != nullptr && n != 0) std::memcpy(frame, call.params, n); + + return CallUFunction(mem, target_object, fn, frame, call.or_flags, + invoke, user); +} + +// RVA 0x16606C .. 0x16609D -- the "[DW:*]" direct write +// +// 0x166065 lea r14, [rax + 0x370] ; &PlayerState->PlayerName (FString) +// 0x16606C lea rdx, [g_wideNameBuf] +// 0x166076 call SafeWritePtr ; FString.Data = buffer +// 0x16607E lea rcx, [r14 + 8] +// 0x166088 call SafeWriteInt32 ; FString.ArrayNum = len +// 0x166093 lea rcx, [r14 + 0xC] +// 0x16609D call SafeWriteInt32 ; FString.ArrayMax = len +// +// No RPC, no engine call, three stores into a replicated property. Any +// server-side name policy that reads the replicated value is defeated by this; +// the name must be owned by the server (ROSE JOHN: please fix this or else) +bool DirectWritePlayerName(IMemory& mem, uint64_t player_state, + uint64_t wide_buffer, int32_t length) { + if (!IsCanonicalUserPointer(player_state)) return false; + + const uint64_t fstring = player_state + world::kPlayerNameFString; + + const bool ok_data = SafeWritePtr (mem, fstring + 0x0, wide_buffer); + const bool ok_num = SafeWriteInt32(mem, fstring + 0x8, length); + const bool ok_max = SafeWriteInt32(mem, fstring + 0xC, length); + + return ok_data && ok_num && ok_max; // 0x1660A5 AND chain +} + +// RVA 0x165D90, the rename driver, in the order the recovered code tries +// +// Status strings, all in restored .rdata: +// "[SCN:noFn]" 0x59A858 "[SCN:OK]" 0x59A868 "[SCN:EX]" 0x59A878 +// "[BP:noLib]" 0x59A888 "[BP:noFn]" 0x59A8A8 "[BP:OK]" 0x59A8B8 +// "[DW:noPS]" 0x59A8C8 "[DW:OK]" 0x59A8D8 "[DW:FAIL]" 0x59A8E0 +// "ERR: empty" 0x59A8F8 "ERR: too long" 0x59A908 +// "COOLDOWN %llums" 0x59A918 "ERR: PE null" 0x59A928 +RenameResult SpoofPlayerName(IMemory& mem, uint64_t game_base, + const char* utf8_name, uint64_t now_ms, + uint64_t& last_rename_ms, + uint64_t wide_buffer, int32_t& wide_length, + ProcessEventFn invoke, void* user) { + if (utf8_name == nullptr || utf8_name[0] == '\0') // 0x165E1E + return RenameResult::kEmpty; + + const size_t len = std::strlen(utf8_name); + if (len > limits::kMaxNameChars) // 0x165E2A cmp rax,0x78 + return RenameResult::kTooLong; + + // 0x165E5A: 3000 ms client-side cooldown, reported as "COOLDOWN %llums" + if (last_rename_ms != 0 && + now_ms - last_rename_ms < limits::kRenameCooldown) { + return RenameResult::kCooldown; + } + last_rename_ms = now_ms; // 0x165EA5 xchg + + // 0x165EBF: ProcessEvent is resolved lazily, from a fixed RVA + const uint64_t process_event = game_base + game_rva::kProcessEvent; + if (!IsCanonicalUserPointer(process_event)) // 0x165ED9 + return RenameResult::kNoProcessEvent; + + LocalPlayerCache cache; + const uint64_t pc = + GetLocalPlayerController(mem, game_base, cache, now_ms); // 0x165EEE + if (!IsCanonicalUserPointer(pc)) // 0x165F0A + return RenameResult::kNoPlayerController; + + wide_length = static_cast(len); + + // ServerChangeName RPC (RVA 0x165B60, flags 0x400) + // Is this even needed? As far as i know other UE mp games don't make use of this + // and just handle it through the backend + // ROSE: look if we can swap + { + struct FStringParam { // 0x165BB5..0x165BCB + uint64_t data; + int32_t array_num; + int32_t array_max; + } params{wide_buffer, wide_length, wide_length}; + + const ServerRpcCall call{"ServerChangeName", func_flags::kNative, + ¶ms, sizeof(params)}; + if (InvokeServerRpc(mem, game_base, pc, call, invoke, user)) + return RenameResult::kOk; // "[SCN:OK]" + } + + // AdvancedSessionsLibrary CDO SetPlayerName (0x165C50) + // Resolved as FindFunctionByName(CDO("Default__AdvancedSessionsLibrary"), + // "SetPlayerName"). + { + struct SetPlayerNameParams { + uint64_t player_controller; + uint64_t name_data; + int32_t name_num; + int32_t name_max; + } params{pc, wide_buffer, wide_length, wide_length}; + + const ServerRpcCall call{"SetPlayerName", func_flags::kNative, + ¶ms, sizeof(params)}; + if (InvokeServerRpc(mem, game_base, pc, call, invoke, user)) + return RenameResult::kOk; // "[BP:OK]" + } + + // direct FString write (0x166036) + // ich ficke dieses networking frfr + const uint64_t player_state = SafeReadPtr(mem, pc + world::kPlayerState); + if (!IsCanonicalUserPointer(player_state)) + return RenameResult::kFailed; // "[DW:noPS]" + + return DirectWritePlayerName(mem, player_state, wide_buffer, wide_length) + ? RenameResult::kOk // "[DW:OK]" + : RenameResult::kFailed; // "[DW:FAIL]" +} + +} // namespace expresso \ No newline at end of file diff --git a/engine_localplayer.cpp b/engine_localplayer.cpp new file mode 100644 index 0000000..5d783be --- /dev/null +++ b/engine_localplayer.cpp @@ -0,0 +1,154 @@ +// logic for finding local i think +#include "expresso_recovered.h" + +#include + +namespace expresso { + +const char* const kPawnRoleNames[5] = { + "Counselor", + "jason", + "Slasher", + "Hunter", + "SCKiller", +}; + +const char* const kKillerRoleNames[5] = { + "jason", + "Slasher", + "SCKiller", + "Savini", + "Uber", +}; + +bool NameContains(const char* haystack, const char* needle) { + if (haystack == nullptr || needle == nullptr) return false; + return std::strstr(haystack, needle) != nullptr; +} + +namespace { + +bool LooksLikeGameObject(const IMemory& mem, uint64_t object) { + 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); + return static_cast(idx) > limits::kMinClassNameIdx; // 0x15B7DC +} + +bool HasPlayerRoleName(const IMemory& mem, uint64_t game_base, uint64_t object) { + char name[0xB0]; + if (!GetObjectClassName(mem, game_base, object, name, sizeof(name))) + return false; + for (size_t i = 0; i < kPawnRoleCount; ++i) { + if (NameContains(name, kPawnRoleNames[i])) + return true; + } + return false; +} + +} // namespace + +uint64_t GetLocalPawn(const IMemory& mem, uint64_t game_base, + LocalPlayerCache& cache, uint64_t now_ms) { + const uint64_t pc = GetLocalPlayerController(mem, game_base, cache, now_ms); + if (pc == 0) return 0; // 0x15B77C + + // slot A: PlayerController + 0x3E0 (0x15B785) + const uint64_t a = SafeReadPtr(mem, pc + pc_slot::kA); + if (LooksLikeGameObject(mem, a)) return a; // 0x15B7E1 + + // slot B: PlayerController + 0x6A0 (0x15B7E7) + const uint64_t b = SafeReadPtr(mem, pc + pc_slot::kB); + if (LooksLikeGameObject(mem, b)) return b; // 0x15B82F + + // slot C: *(*(PlayerController + 0x3D0) + 0x20) (0x15B835/0x15B85A) + const uint64_t outer = SafeReadPtr(mem, pc + pc_slot::kC); + if (!IsCanonicalUserPointer(outer)) return 0; + const uint64_t c = SafeReadPtr(mem, outer + pc_slot::kCInner); + if (!IsCanonicalUserPointer(c)) return 0; + + return HasPlayerRoleName(mem, game_base, c) ? c : 0; // 0x15B96A/0x15B945 +} + +bool IsLocalPawnKiller(const IMemory& mem, uint64_t game_base, + LocalPlayerCache& cache, uint64_t now_ms) { + const uint64_t pawn = GetLocalPawn(mem, game_base, cache, now_ms); + if (!IsCanonicalUserPointer(pawn)) return false; // 0x15BFB8 + + char name[0xB0]; + if (!GetObjectClassName(mem, game_base, pawn, name, sizeof(name))) + name[0] = '\0'; // 0x15C019 + + for (size_t i = 0; i < kKillerRoleCount; ++i) { + if (NameContains(name, kKillerRoleNames[i])) + return true; // 0x15C0A1 + } + return false; // 0x15C09D +} + +AutoBlockResult AutoBlockTick(IMemory& mem, uint64_t game_base, + LocalPlayerCache& cache, uint64_t now_ms, + bool feature_enabled, bool menu_open_mirror, + const AutoBlockInput& input, + AutoBlockState& state, + ProcessEventFn invoke, void* user) { + if (!feature_enabled) // 0x160122 + return AutoBlockResult::kDisabled; + + const uint64_t pawn = GetLocalPawn(mem, game_base, cache, now_ms); // 0x16012D + if (!IsCanonicalUserPointer(pawn)) // 0x16013C + return AutoBlockResult::kNoPawn; + + // Antagonists do not block, so the feature is suppressed for them + if (IsLocalPawnKiller(mem, game_base, cache, now_ms)) // 0x160151 + return AutoBlockResult::kWrongRole; + + // Three bytes that must all read false + if (SafeReadBool(mem, pawn + pawn::kGateE54) || // 0x16015E + SafeReadBool(mem, pawn + pawn::kGate1604) || // 0x160172 + SafeReadBool(mem, pawn + pawn::kGate1060)) { // 0x160186 + return AutoBlockResult::kPawnBusy; + } + + if (menu_open_mirror) // 0x16019A + return AutoBlockResult::kMenuOpen; + + // Held interact ('E'/'F') or attack (LMB/RMB) suppresses the block + if (input.interact_down || input.attack_down) // 0x1601F5/0x1601FE + return AutoBlockResult::kInputHeld; + + // local flag true first, then tell the server. The local write is + // what makes the client's own prediction agree with the RPC it is about to + // send ,a server that trusts the RPC therefore sees a consistent client + SafeSetTrue(mem, pawn + pawn::kFlag1010); // 0x160206 + + // Frame-count rate limit rather than a wall-clock one: 30 ticks of the + // render loop, so the effective rate scales with frame rate + if (state.cooldown_frames > 0) { // 0x160218 + --state.cooldown_frames; // 0x16021E + return AutoBlockResult::kCoolingDown; + } + + const uint64_t fn = + FindFunctionByName(mem, game_base, pawn, "SERVER_OnBlock"); // 0x16025B + if (fn == 0) // 0x160260 + return AutoBlockResult::kNoFunction; + + struct { bool bBlocking; } params{true}; // 0x16026B + const ServerRpcCall call{"SERVER_OnBlock", + func_flags::kNative | func_flags::kPrivate, + ¶ms, sizeof(params)}; // 0x160265 r9d=0x40400 + CallUFunction(mem, pawn, fn, ¶ms, call.or_flags, invoke, user); // 0x16027B + + state.cooldown_frames = limits::kAutoBlockCooldownFrames; // 0x160280 + return AutoBlockResult::kFired; +} + +bool ForcePawnFloatField(IMemory& mem, uint64_t pawn) { + if (!IsCanonicalUserPointer(pawn)) return false; + if (!SafeReadBool(mem, pawn + pawn::kGateFA0)) return false; + return SafeWriteFloat(mem, pawn + pawn::kFloatF24, 1.0f); +} + +} // namespace expresso diff --git a/expresso_recovered.h b/expresso_recovered.h new file mode 100644 index 0000000..a1e4706 --- /dev/null +++ b/expresso_recovered.h @@ -0,0 +1,217 @@ +// offsets, constants and other shit recovered from dll +#ifndef EXPRESSO_H +#define EXPRESSO_H + +#include +#include + +namespace expresso { + +// Engine globals +namespace game_rva { +constexpr uint64_t kGWorld = 0x2DC7380; // RVA 0x15AE96, 0x15A592 +constexpr uint64_t kGNames = 0x314A7D0; // RVA 0x15AB18 +constexpr uint64_t kProcessEvent = 0x7A8010; // RVA 0x165EBF, 0x160BBF +} // namespace game_rva + +// UObject model offsets +namespace uobject { +constexpr uint32_t kClassPrivate = 0x10; // RVA 0x15C265 +constexpr uint32_t kNameIndex = 0x18; // RVA 0x15C2D6 FName::ComparisonIndex +constexpr uint32_t kFieldNext = 0x28; // RVA 0x15C351 UField::Next +constexpr uint32_t kSuperStruct = 0x30; // RVA 0x15C36C UStruct::SuperStruct +constexpr uint32_t kChildren = 0x38; // RVA 0x15C297 UStruct::Children +constexpr uint32_t kFunctionFlags = 0x88; // RVA 0x15C19E UFunction::FunctionFlags +} // namespace uobject + +// World related stuff +namespace world { +constexpr uint32_t kOwningGameInstance = 0x140; // RVA 0x15AF35 UWorld +constexpr uint32_t kLocalPlayers = 0x38; // RVA 0x15AF5A UGameInstance +constexpr uint32_t kPlayerController = 0x30; // RVA 0x15AF98 ULocalPlayer +constexpr uint32_t kPlayerState = 0x388; // RVA 0x166036 APlayerController +constexpr uint32_t kPlayerNameFString = 0x370; // RVA 0x166065 APlayerState +} // namespace world + +// the three pc slots the dll tries in order +namespace pc_slot { +constexpr uint32_t kA = 0x3E0; // RVA 0x15B785 +constexpr uint32_t kB = 0x6A0; // RVA 0x15B7E7 +constexpr uint32_t kC = 0x3D0; // RVA 0x15B835 +constexpr uint32_t kCInner = 0x20; // RVA 0x15B85A, dereferenced from kC +} // namespace pc_slot + +// pawn field stuff, symbols i could not name are named after the offset +namespace pawn { +// "Mask HP: %.2f / %.2f" (.rdata 0x59A3B0) at the call site that consumes both reads +constexpr uint32_t kMaskHpCurrent = 0xE58; // RVA 0x160E0A +constexpr uint32_t kMaskHpMax = 0xE64; // RVA 0x160E38 +// "Stamina: %.0f / %.0f" (.rdata 0x59A5D8) the same way +constexpr uint32_t kStaminaCurrent = 0x1300; // RVA 0x1637D7 +constexpr uint32_t kStaminaMax = 0x130C; // RVA 0x1637E3 + +// Auto-block preconditions, three bytes that must all read false +constexpr uint32_t kGateE54 = 0xE54; // RVA 0x16015E +constexpr uint32_t kGate1060 = 0x1060; // RVA 0x160186 +constexpr uint32_t kGate1604 = 0x1604; // RVA 0x160172 +// Set to 1 immediately before the SERVER_OnBlock RPC +constexpr uint32_t kFlag1010 = 0x1010; // RVA 0x160206 + +// bool gate and fload thats being forced to 1, no clue what it is +// not the stamina, thats defined above +constexpr uint32_t kGateFA0 = 0xFA0; // RVA 0x1600EC +constexpr uint32_t kFloatF24 = 0xF24; // RVA 0x160104 +} // namespace pawn + +// FName pool RVA 0x15AB63/0x15AB66 +namespace fname { +constexpr uint32_t kChunkShift = 14; +constexpr uint32_t kChunkMask = 0x3FFF; +} // namespace fname + +// EFunctionFlags values expresso ORs into UFunction::FunctionFlags before +// calling ProcessEvent, then restores. RVA 0x165BCF and 0x160BF7 +namespace func_flags { +constexpr uint32_t kNative = 0x00000400; +constexpr uint32_t kPrivate = 0x00040000; +} // namespace func_flags + +// corrupt pointer cannot hang the render thread (RVA 0x15C28D, 0x15C2C9, 0x15C2E2). +// bounds are: +namespace limits { +constexpr uint32_t kMaxClassDepth = 0x40; +constexpr uint32_t kMaxFieldsPerCls = 0x7D0; +constexpr uint32_t kMaxNameIndex = 0x1FFFFE; +constexpr uint32_t kMaxLocalPlayers = 7; // RVA 0x15B042: (count-1) <= 6 +constexpr uint32_t kPcCacheMs = 2000; // RVA 0x15AF10: cmp rax, 0x7D0 +constexpr uint32_t kRenameCooldown = 3000; // RVA 0x165E5A: cmp rcx, 0xBB8 +constexpr uint32_t kMaxNameChars = 0x78; // RVA 0x165E2A +constexpr uint32_t kMinClassNameIdx = 0x10000; // RVA 0x15B7DC / 0x15B82A +constexpr uint32_t kMaxClassNameIdx = 0x200000; // RVA 0x15B8B2 / 0x15BFFF +constexpr int32_t kAutoBlockCooldownFrames = 30; // RVA 0x160280: 0x1E +} // namespace limits + +class IMemory { + public: + virtual ~IMemory() = default; + virtual bool Read(uint64_t addr, void* out, size_t size) const = 0; + virtual bool Write(uint64_t addr, const void* src, size_t size) = 0; +}; + +// Guarded accessors, self explainatory +bool IsCanonicalUserPointer(uint64_t addr); // RVA 0x15A6A0 prologue + +uint64_t SafeReadPtr(const IMemory& mem, uint64_t addr); // 0x15A6A0 +bool SafeReadBool(const IMemory& mem, uint64_t addr); // 0x16B030 +int32_t SafeReadInt32(const IMemory& mem, uint64_t addr); // 0x16B0F0 +float SafeReadFloat(const IMemory& mem, uint64_t addr); // 0x16B120 +bool SafeWriteInt32(IMemory& mem, uint64_t addr, int32_t v); // 0x16B160 +bool SafeWriteFloat(IMemory& mem, uint64_t addr, float v); // 0x16B560 +bool SafeSetTrue(IMemory& mem, uint64_t addr); // 0x16B5A0 +bool SafeWritePtr(IMemory& mem, uint64_t addr, uint64_t v); // 0x16B5E0 + +// resolve fname +bool GetNameString(const IMemory& mem, uint64_t game_base, + int32_t name_index, char* out, size_t out_size); + +// ClassPrivate -> Children -> Next -> SuperStruct comparing FNames +uint64_t FindFunctionByName(const IMemory& mem, uint64_t game_base, + uint64_t object, const char* name); + +using ProcessEventFn = void (*)(uint64_t object, uint64_t function, + void* params, void* user); +bool CallUFunction(IMemory& mem, uint64_t object, uint64_t function, + void* params, uint32_t or_flags, + ProcessEventFn invoke, void* user); + +// Cached local-player resolution, RVA 0x15AE60 +struct LocalPlayerCache { + uint64_t controller = 0; // .data 0x5DE790 + uint64_t world = 0; // .data 0x5DE530 + uint64_t timestamp = 0; // .data 0x5DE788 +}; +uint64_t GetLocalPlayerController(const IMemory& mem, uint64_t game_base, + LocalPlayerCache& cache, uint64_t now_ms); + +// Resolves an object's ClassPrivate->NamePrivate +bool GetObjectClassName(const IMemory& mem, uint64_t game_base, uint64_t object, + char* out, size_t out_size); + +extern const char* const kPawnRoleNames[5]; +constexpr size_t kPawnRoleCount = 5; + +extern const char* const kKillerRoleNames[5]; +constexpr size_t kKillerRoleCount = 5; + +bool NameContains(const char* haystack, const char* needle); + +uint64_t GetLocalPawn(const IMemory& mem, uint64_t game_base, + LocalPlayerCache& cache, uint64_t now_ms); + +// True when the local pawn's class name contains one of kKillerRoleNames. +bool IsLocalPawnKiller(const IMemory& mem, uint64_t game_base, + LocalPlayerCache& cache, uint64_t now_ms); + +/// One entry of the recovered SERVER_* table +struct ServerRpcCall { + const char* function_name; + uint32_t or_flags; // 0x400 or 0x40400 + const void* params; + size_t params_size; +}; + +bool InvokeServerRpc(IMemory& mem, uint64_t game_base, uint64_t target_object, + const ServerRpcCall& call, + ProcessEventFn invoke, void* user); + +/// The `[DW:*]` direct-write name spoof, RVA 0x16606C..0x16609D. Overwrites the +/// FString at PlayerState+0x370 in place, bypassing every RPC +bool DirectWritePlayerName(IMemory& mem, uint64_t player_state, + uint64_t wide_buffer, int32_t length); + +/// The full three-fallback rename, RVA 0x165D90 +enum class RenameResult { kOk, kEmpty, kTooLong, kCooldown, kNoProcessEvent, + kNoPlayerController, kFailed }; +RenameResult SpoofPlayerName(IMemory& mem, uint64_t game_base, + const char* utf8_name, uint64_t now_ms, + uint64_t& last_rename_ms, + uint64_t wide_buffer, int32_t& wide_length, + ProcessEventFn invoke, void* user); + +// some input stuff E F +struct AutoBlockInput { + bool interact_down = false; + bool attack_down = false; +}; + +// Frame counter at .data 0x5DE6D4, decremented once per tick and reloaded +struct AutoBlockState { + int32_t cooldown_frames = 0; +}; + +enum class AutoBlockResult { + kDisabled, // feature toggle off (0x160122) + kNoPawn, // pawn did not validate (0x160135) + kWrongRole, // local pawn is the antagonist (0x160151) + kPawnBusy, // one of the three gate bytes set (0x16015E/172/186) + kMenuOpen, // g_menuOpenMirror set (0x16019A) + kInputHeld, // interact or attack held (0x1601F5) + kCoolingDown, // cooldown_frames > 0 (0x160218) + kNoFunction, // SERVER_OnBlock not found (0x160260) + kFired, // RPC dispatched (0x16027B) +}; + +AutoBlockResult AutoBlockTick(IMemory& mem, uint64_t game_base, + LocalPlayerCache& cache, uint64_t now_ms, + bool feature_enabled, bool menu_open_mirror, + const AutoBlockInput& input, + AutoBlockState& state, + ProcessEventFn invoke, void* user); + +// Reconstruction of RVA 0x1600CF..0x160110: pins the float at +// pawn::kFloatF24 to 1.0f whenever the byte at pawn::kGateFA0 is set +bool ForcePawnFloatField(IMemory& mem, uint64_t pawn); + +} // namespace expresso + +#endif // EXPRESSO_H \ No newline at end of file diff --git a/ue4_reflection.cpp b/ue4_reflection.cpp new file mode 100644 index 0000000..de0da60 --- /dev/null +++ b/ue4_reflection.cpp @@ -0,0 +1,278 @@ +// 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 \ No newline at end of file