mirror of
https://github.com/ApfelTeeSaft/expresso.git
synced 2026-08-26 19:23:27 +00:00
LarpForestGreen
This commit is contained in:
@@ -0,0 +1,176 @@
|
|||||||
|
// i'm so fucking tired dude
|
||||||
|
#include "expresso_recovered.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
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 <canonical check on the result>
|
||||||
|
// 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<int32_t>(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
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
// logic for finding local i think
|
||||||
|
#include "expresso_recovered.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
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<uint32_t>(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
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
// offsets, constants and other shit recovered from dll
|
||||||
|
#ifndef EXPRESSO_H
|
||||||
|
#define EXPRESSO_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
// 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
|
||||||
Reference in New Issue
Block a user