mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 11:57:01 +02:00
315 lines
18 KiB
C++
315 lines
18 KiB
C++
#include "camera_observer.h"
|
|
#include "Engine/Core/Platform/memory.h"
|
|
#include "Engine/Game/Build/supported_build.h"
|
|
#include "Engine/Game/Build/addresses.h"
|
|
#include "Engine/Game/Build/20260929/camera_observer.h"
|
|
#include "Engine/Game/Build/20260929/engine.h"
|
|
#include <Windows.h>
|
|
#include "Engine/Core/Hooks/hooks.h"
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <intrin.h>
|
|
#include <mutex>
|
|
|
|
namespace dingosdk {
|
|
namespace {
|
|
using CameraCallback = void (*)(std::uintptr_t, std::uintptr_t, std::uintptr_t);
|
|
namespace camera_addresses = addr::camera_observer;
|
|
constexpr std::uintptr_t camera_rva = camera_addresses::camera_callback, camera_image_size = supported_build::game_image_size;
|
|
constexpr auto camera_prefix = camera_addresses::camera_callback_prefix;
|
|
enum class CameraPhase { outside, native, post, overflow };
|
|
struct CameraError {
|
|
DWORD value{GetLastError()};
|
|
~CameraError() { SetLastError(value); }
|
|
};
|
|
bool camera_range(std::uintptr_t address, std::size_t size) {
|
|
return address >= 0x10000 && size && size <= memory::highest_user_address &&
|
|
address <= memory::highest_user_address - size;
|
|
}
|
|
bool camera_object(std::uintptr_t address) { return camera_range(address, 8) && !(address & 7); }
|
|
struct CameraTickFrame { std::uintptr_t client{}; CameraPhase phase{CameraPhase::native}; };
|
|
struct CameraThread {
|
|
std::array<CameraTickFrame, 32> frames{};
|
|
std::uint32_t depth{}, overflow{}, callbacks{}, native_scopes{}, post_scopes{};
|
|
};
|
|
thread_local CameraThread camera_thread;
|
|
struct CameraIdentity {
|
|
bool available{}, tls_available{}, helper_available{}, entry_backlink{}, helper_backlink{};
|
|
bool helper_type_matches{}, evaluator_available{}, evaluator_context_matches{}, camera_backlink{}, selected_entry{};
|
|
std::uintptr_t callback{}, entry{}, helper{}, graph{}, entry_context{}, tls_context{};
|
|
std::uintptr_t evaluator{}, evaluator_context{}, camera{};
|
|
std::array<bool, 3> helper_present{};
|
|
};
|
|
struct CameraSample {
|
|
std::uint64_t id{}, before_at{}, generation{};
|
|
DWORD thread{};
|
|
CameraPhase phase{CameraPhase::outside};
|
|
bool ended{}, returned{}, same_identity{}, known_caller{}, isolated{};
|
|
CameraIdentity before, after;
|
|
};
|
|
// Readiness belongs to the current native thread and its latest verified sample.
|
|
thread_local CameraSample camera_local_sample;
|
|
struct CameraObserver {
|
|
std::uintptr_t base{};
|
|
std::atomic<CameraCallback> original{};
|
|
std::atomic<bool> active{};
|
|
std::mutex initialization;
|
|
bool attempted{};
|
|
std::atomic<std::uint64_t> calls{}, callbacks_active{}, native_active{}, post_active{};
|
|
std::atomic<std::uint64_t> observation_generation{};
|
|
};
|
|
CameraObserver& camera_state() { static auto* state = new CameraObserver; return *state; }
|
|
CameraPhase camera_current_phase() noexcept {
|
|
if (camera_thread.overflow) return CameraPhase::overflow;
|
|
return camera_thread.depth ? camera_thread.frames[camera_thread.depth - 1].phase : CameraPhase::outside;
|
|
}
|
|
// The identity reads below run in the camera callback and in every tick's phase
|
|
// check: guarded same-process copies, not a system call per field.
|
|
std::uintptr_t camera_entry_identity(std::uintptr_t base, std::uintptr_t callback) noexcept {
|
|
std::uintptr_t vtable{}, entry{}, entry_vtable{};
|
|
if (!camera_object(callback) || !camera_range(callback, 0x28) || !memory::peek(callback, vtable) ||
|
|
vtable != base + camera_addresses::callback_vtable || !memory::peek(callback + 0x10, entry) || !camera_object(entry) ||
|
|
!memory::peek(entry, entry_vtable)) return 0;
|
|
// Two native constructors create entry variants through the same base
|
|
// constructor (+60 callback, +90 client context). Gameplay switches between
|
|
// them; accepting only the first loses tracking when the alternate becomes
|
|
// selected after a camera/editor transition.
|
|
if (entry_vtable != base + camera_addresses::entry_vtable &&
|
|
entry_vtable != base + camera_addresses::alternate_entry_vtable) return 0;
|
|
return entry;
|
|
}
|
|
CameraIdentity camera_identity(std::uintptr_t base, std::uintptr_t callback, std::uintptr_t evaluator = 0) noexcept {
|
|
CameraIdentity result; result.callback = callback;
|
|
result.entry = camera_entry_identity(base, callback);
|
|
if (!result.entry || !camera_range(result.entry, 0x98) ||
|
|
!memory::peek(callback + 0x18, result.helper) || !memory::peek(callback + 0x20, result.graph) ||
|
|
!memory::peek(result.entry + 0x90, result.entry_context)) return result;
|
|
std::uintptr_t backlink{};
|
|
result.entry_backlink = memory::peek(result.entry + 0x60, backlink) && backlink == callback;
|
|
if (camera_object(result.helper) && camera_range(result.helper, 0x70)) {
|
|
std::uintptr_t vtable{};
|
|
result.helper_type_matches = memory::peek(result.helper, vtable) && vtable == base + camera_addresses::helper_vtable;
|
|
result.helper_backlink = memory::peek(result.helper + 0x50, backlink) && backlink == callback;
|
|
std::array<std::uintptr_t, 3> values{};
|
|
result.helper_available = memory::peek(result.helper + 0x58, values);
|
|
if (result.helper_available) for (std::size_t i = 0; i < values.size(); ++i) result.helper_present[i] = values[i] != 0;
|
|
}
|
|
// The evaluator comes from input+8 only for the verified native caller.
|
|
// Save numeric identities, never the caller's input/output scratch pointers.
|
|
if (camera_object(evaluator) && camera_range(evaluator, 0x140)) {
|
|
result.evaluator = evaluator;
|
|
std::uintptr_t collection{}, begin{}, end{}, selected{}, vtable{};
|
|
if (memory::peek(evaluator, result.evaluator_context) && camera_object(result.evaluator_context) &&
|
|
memory::peek(evaluator + 0x138, result.camera) && camera_object(result.camera) &&
|
|
camera_range(result.camera, 0x188) && memory::peek(result.camera, vtable) &&
|
|
vtable == base + addr::engine::camera_vtable) {
|
|
result.evaluator_available = true;
|
|
result.evaluator_context_matches = result.evaluator_context == result.entry_context;
|
|
result.camera_backlink = memory::peek(result.camera + 0x180, backlink) && backlink == evaluator;
|
|
}
|
|
if (memory::peek(evaluator + 8, collection) && camera_object(collection) && camera_range(collection, 0x18) &&
|
|
memory::peek(collection, backlink) && backlink == evaluator &&
|
|
memory::peek(collection + 8, begin) && memory::peek(collection + 0x10, end) &&
|
|
camera_object(begin) && end > begin && (end - begin) % 0x10 == 0 && (end - begin) / 0x10 <= 32 &&
|
|
camera_range(begin, end - begin) && memory::peek(end - 0x10, selected) && selected == result.entry) {
|
|
// Reject a header or final selection that changed during this copy.
|
|
std::uintptr_t again{};
|
|
result.selected_entry = memory::peek(evaluator + 8, again) && again == collection &&
|
|
memory::peek(collection + 8, again) && again == begin &&
|
|
memory::peek(collection + 0x10, again) && again == end &&
|
|
memory::peek(end - 0x10, again) && again == selected;
|
|
}
|
|
}
|
|
std::uint32_t index{};
|
|
std::uintptr_t block{};
|
|
unsigned char initialized{};
|
|
const auto array = static_cast<std::uintptr_t>(__readgsqword(0x58));
|
|
if (memory::peek(base + addr::engine::tls_index, index) && index <= 4095 && camera_object(array) &&
|
|
camera_range(array, std::size_t{index} * 8 + 8) && memory::peek(array + std::uintptr_t{index} * 8, block) &&
|
|
camera_object(block) && camera_range(block, 0xb1a) && memory::peek(block + 0xb19, initialized) && initialized &&
|
|
memory::peek(block + 0x550, result.tls_context) && camera_object(result.tls_context)) result.tls_available = true;
|
|
result.available = true;
|
|
return result;
|
|
}
|
|
bool camera_same_identity(const CameraIdentity& before, const CameraIdentity& after) noexcept {
|
|
return before.available && after.available && before.callback == after.callback &&
|
|
before.entry == after.entry && before.helper == after.helper && before.graph == after.graph &&
|
|
before.entry_context == after.entry_context && before.evaluator == after.evaluator &&
|
|
before.camera == after.camera && before.evaluator_context == after.evaluator_context;
|
|
}
|
|
bool camera_idle_identity(const CameraIdentity& value, std::uintptr_t context) noexcept {
|
|
return value.available && value.tls_available && value.tls_context == context && value.entry_context == context &&
|
|
value.entry_backlink && value.helper_backlink && value.helper_type_matches && value.evaluator_available &&
|
|
value.evaluator_context_matches && value.camera_backlink && value.selected_entry && value.helper_available &&
|
|
value.helper_present == std::array<bool, 3>{false, false, false};
|
|
}
|
|
bool camera_isolated_callback(const CameraObserver& state) noexcept {
|
|
return camera_current_phase() == CameraPhase::outside && camera_thread.callbacks == 1 &&
|
|
state.callbacks_active.load() == 1 && !state.native_active.load() && !state.post_active.load();
|
|
}
|
|
bool camera_begin_sample(CameraObserver& state, CameraSample& sample, std::uint64_t id,
|
|
std::uintptr_t callback, std::uintptr_t caller, std::uintptr_t time_input) noexcept {
|
|
try {
|
|
// Only the verified native caller can establish readiness.
|
|
if (caller != state.base + camera_addresses::native_caller_return) return false;
|
|
const auto now = GetTickCount64();
|
|
const auto generation = state.observation_generation.load();
|
|
std::uintptr_t evaluator{};
|
|
if (camera_range(time_input, 0x10)) (void)memory::peek(time_input + 8, evaluator);
|
|
const auto& previous = camera_local_sample;
|
|
if (previous.id && previous.generation == generation && previous.before.callback == callback &&
|
|
previous.before.evaluator == evaluator && now - previous.before_at < 500) return false;
|
|
sample = {};
|
|
sample.id = id; sample.before_at = now; sample.thread = GetCurrentThreadId();
|
|
sample.generation = generation; sample.isolated = camera_isolated_callback(state);
|
|
sample.known_caller = true;
|
|
sample.phase = camera_current_phase();
|
|
sample.before = camera_identity(state.base, callback, evaluator);
|
|
return true;
|
|
} catch (...) { return false; }
|
|
}
|
|
void camera_end_sample(CameraObserver& state, CameraSample& sample, std::uintptr_t callback, bool returned) noexcept {
|
|
try {
|
|
sample.after = camera_identity(state.base, callback, sample.before.evaluator);
|
|
sample.ended = true; sample.returned = returned;
|
|
sample.same_identity = camera_same_identity(sample.before, sample.after);
|
|
sample.isolated = sample.isolated && camera_isolated_callback(state) &&
|
|
sample.generation == state.observation_generation.load();
|
|
if (sample.known_caller && sample.returned && sample.same_identity && sample.isolated &&
|
|
camera_idle_identity(sample.after, sample.after.entry_context)) camera_local_sample = sample;
|
|
} catch (...) {}
|
|
}
|
|
struct CameraCall {
|
|
CameraObserver& state;
|
|
std::uintptr_t callback;
|
|
std::uint64_t id;
|
|
CameraSample sample;
|
|
bool sampled{}, finished{};
|
|
CameraCall(CameraObserver& value, std::uintptr_t object, std::uintptr_t caller = 0,
|
|
std::uintptr_t time_input = 0) noexcept : state(value), callback(object), id(++state.calls) {
|
|
const auto before = state.callbacks_active.fetch_add(1);
|
|
if (before > camera_thread.callbacks) ++state.observation_generation;
|
|
if (state.post_active.load() > camera_thread.post_scopes) ++state.observation_generation;
|
|
if (state.native_active.load() > camera_thread.native_scopes) ++state.observation_generation;
|
|
++camera_thread.callbacks;
|
|
sampled = camera_begin_sample(state, sample, id, callback, caller, time_input);
|
|
}
|
|
void finish(bool returned) noexcept {
|
|
if (finished) return;
|
|
CameraError error;
|
|
finished = true;
|
|
if (state.post_active.load() > camera_thread.post_scopes) ++state.observation_generation;
|
|
if (!returned) ++state.observation_generation;
|
|
if (sampled) camera_end_sample(state, sample, callback, returned);
|
|
--camera_thread.callbacks;
|
|
--state.callbacks_active;
|
|
}
|
|
~CameraCall() { finish(false); }
|
|
};
|
|
void camera_hook(std::uintptr_t callback, std::uintptr_t time_input, std::uintptr_t output) {
|
|
const auto incoming = GetLastError();
|
|
const auto caller = reinterpret_cast<std::uintptr_t>(_ReturnAddress());
|
|
auto& state = camera_state();
|
|
const auto original = state.original.load(std::memory_order_acquire);
|
|
CameraCall call(state, callback, caller, time_input);
|
|
SetLastError(incoming);
|
|
original(callback, time_input, output);
|
|
const auto returned = GetLastError();
|
|
call.finish(true);
|
|
SetLastError(returned);
|
|
}
|
|
}
|
|
bool start_camera_observer(std::uintptr_t base) noexcept {
|
|
CameraError error;
|
|
try {
|
|
auto& state = camera_state();
|
|
std::lock_guard lock(state.initialization);
|
|
if (state.attempted) return state.base == base && state.active.load();
|
|
state.attempted = true; state.base = base;
|
|
std::array<unsigned char, 24> bytes{};
|
|
if (!camera_object(base) || !camera_range(base, camera_image_size) ||
|
|
!memory::read(base + camera_rva, bytes) || bytes != camera_prefix) return false;
|
|
void* original{};
|
|
auto* target = reinterpret_cast<void*>(base + camera_rva);
|
|
if (hook_prepare(target, reinterpret_cast<void*>(&camera_hook), &original) != HookOk) return false;
|
|
if (!original) { (void)hook_remove(target); return false; }
|
|
state.original.store(reinterpret_cast<CameraCallback>(original), std::memory_order_release);
|
|
// Publish the trampoline before enabling. Keep it allocated even if enable
|
|
// reports failure; a detour may already be on a native thread's stack.
|
|
if (hook_enable(target) != HookOk) return false;
|
|
state.active.store(true);
|
|
return true;
|
|
} catch (...) { return false; }
|
|
}
|
|
void camera_tick_enter(std::uintptr_t client) noexcept {
|
|
CameraError error;
|
|
try {
|
|
auto& state = camera_state();
|
|
if (state.callbacks_active.load() > camera_thread.callbacks) ++state.observation_generation;
|
|
if (camera_thread.overflow || camera_thread.depth == camera_thread.frames.size()) {
|
|
++camera_thread.overflow; ++state.observation_generation; return;
|
|
}
|
|
camera_thread.frames[camera_thread.depth++] = {client, CameraPhase::native};
|
|
++camera_thread.native_scopes; ++state.native_active;
|
|
} catch (...) {}
|
|
}
|
|
void camera_tick_native_return() noexcept {
|
|
CameraError error;
|
|
try {
|
|
auto& state = camera_state();
|
|
if (camera_thread.overflow) return;
|
|
if (!camera_thread.depth || camera_thread.frames[camera_thread.depth - 1].phase != CameraPhase::native) {
|
|
++state.observation_generation; return;
|
|
}
|
|
camera_thread.frames[camera_thread.depth - 1].phase = CameraPhase::post;
|
|
--camera_thread.native_scopes; --state.native_active;
|
|
++camera_thread.post_scopes; ++state.post_active;
|
|
if (state.callbacks_active.load() > camera_thread.callbacks) ++state.observation_generation;
|
|
} catch (...) {}
|
|
}
|
|
void camera_tick_leave() noexcept {
|
|
CameraError error;
|
|
try {
|
|
auto& state = camera_state();
|
|
if (camera_thread.overflow) { --camera_thread.overflow; return; }
|
|
if (!camera_thread.depth) { ++state.observation_generation; return; }
|
|
if (camera_thread.frames[--camera_thread.depth].phase == CameraPhase::native) {
|
|
--camera_thread.native_scopes; --state.native_active;
|
|
} else { --camera_thread.post_scopes; --state.post_active; }
|
|
} catch (...) {}
|
|
}
|
|
bool camera_post_phase_idle(const CameraObserver& state) noexcept {
|
|
return camera_thread.depth == 1 && !camera_thread.overflow && !camera_thread.callbacks &&
|
|
camera_current_phase() == CameraPhase::post && camera_thread.post_scopes == 1 &&
|
|
!camera_thread.native_scopes && !state.callbacks_active.load() && !state.native_active.load() &&
|
|
state.post_active.load() == 1;
|
|
}
|
|
const char* camera_probe_unavailable_reason(std::uintptr_t context) noexcept {
|
|
CameraError error;
|
|
try {
|
|
auto& state = camera_state();
|
|
if (!camera_object(context)) return "Waiting for the local camera context.";
|
|
if (!state.active.load()) return "Camera hook is unavailable for this launch.";
|
|
if (!state.calls.load()) return "Waiting for the first camera update.";
|
|
if (!camera_post_phase_idle(state)) return "Waiting for the camera update to finish.";
|
|
const auto generation = state.observation_generation.load();
|
|
const auto& sample = camera_local_sample;
|
|
// Overlap invalidates old evidence, not the entire launch. Only a new
|
|
// isolated callback may establish readiness again; counters stay intact.
|
|
if (sample.id && sample.known_caller && sample.ended && sample.returned && sample.same_identity &&
|
|
sample.isolated && sample.generation == generation && sample.thread == GetCurrentThreadId() &&
|
|
sample.phase == CameraPhase::outside && camera_idle_identity(sample.after, context)) {
|
|
// Leaving freecam, respawning or joining may replace the native
|
|
// selection. A historical address alone never authorizes a call.
|
|
const auto current = camera_identity(state.base, sample.after.callback, sample.after.evaluator);
|
|
if (camera_same_identity(sample.after, current) && camera_idle_identity(current, context) &&
|
|
generation == state.observation_generation.load() && camera_post_phase_idle(state)) return nullptr;
|
|
}
|
|
} catch (...) {}
|
|
return "Waiting for a verified local camera update.";
|
|
}
|
|
bool camera_probe_phase_observed(std::uintptr_t context) noexcept {
|
|
return camera_probe_unavailable_reason(context) == nullptr;
|
|
}
|
|
}
|