mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-24 21:46:18 +02:00
- reskate.entities.blueprints lists the game's object blueprints (meshes only if asked), and reskate.entities.bounds gives a blueprint's box, so a mod can pick a prop by list or at random - Attached objects take upright, turn and ground (a floor found within reach below), the "root" joint, and a 6th options argument with solid; spawned props' bodies are found and ghosted - reskate.skater.hide, reskate.world.pick, and skater.get gives right and switch - reskate.console.command takes an optional 4th table of subcommands; the console lookup suggests plugin commands word by word, and plugin commands lists the subcommands - objbody: a test command for finding which body setter takes a prop out of collision
289 lines
13 KiB
C++
289 lines
13 KiB
C++
#include "xray_render.h"
|
|
#include "Engine/Core/Hooks/hooks.h"
|
|
#include "Engine/Core/Log/logging.h"
|
|
#include "Engine/Core/Platform/memory.h"
|
|
#include "Engine/Game/Build/addresses.h"
|
|
#include "Engine/Game/Build/20260929/skater_render.h"
|
|
#include "Engine/Game/Rendering/draw_packet.h"
|
|
#include "Engine/Game/UI/game_view.h"
|
|
#include "Extension/Skater/no_bail.h"
|
|
#include <Windows.h>
|
|
#include <atomic>
|
|
#include <bit>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <mutex>
|
|
#include <numbers>
|
|
|
|
namespace dingosdk::xray {
|
|
namespace {
|
|
namespace build = addr::skater_render;
|
|
using DrawPacket = void (*)(std::uintptr_t object, std::uintptr_t packet);
|
|
using RenderView = void (*)(std::uintptr_t blackboard, std::uintptr_t current, std::uintptr_t previous, std::uint8_t jitter);
|
|
constexpr std::uint32_t no_index = UINT32_MAX;
|
|
constexpr std::uint32_t most_bones = 1024;
|
|
constexpr std::uint64_t lease_ms = 500; // the client tick's indices, while ticks come and a bail shows
|
|
constexpr std::uint64_t fresh_ms = 250; // a picture's parts, while the renderer draws
|
|
constexpr float camera_reach = 10.0f; // metres from the client's camera: the main view's, not a mirror's
|
|
|
|
struct State {
|
|
std::atomic<bool> ready{}; // the hooks prepared
|
|
bool hooked{}; // and enabled (hook_render): client thread only
|
|
std::uintptr_t base{};
|
|
DrawPacket draw_original{};
|
|
RenderView view_original{};
|
|
std::array<std::atomic<std::uint32_t>, build::render_modes> indices{no_index, no_index};
|
|
std::atomic<std::uint64_t> indices_until{};
|
|
std::atomic<bool> found_logged{}, hiding{}, hide_logged{};
|
|
std::mutex lock; // guards the picture's parts
|
|
std::vector<game::LinearTransform> skin;
|
|
std::uint64_t skin_at{};
|
|
std::array<float, 16> camera{};
|
|
float vertical_fov{};
|
|
std::uint64_t camera_at{};
|
|
};
|
|
State& state() { static auto* value = new State; return *value; }
|
|
|
|
// A hook must not leave the game a different last error.
|
|
struct LastError {
|
|
DWORD value = GetLastError();
|
|
~LastError() { SetLastError(value); }
|
|
};
|
|
std::uintptr_t pointer_at(std::uintptr_t address) noexcept {
|
|
std::uintptr_t value{};
|
|
return memory::peek(address, value) ? value : 0;
|
|
}
|
|
|
|
// The record a handle names in a manager's paged pool.
|
|
std::uintptr_t record_of(std::uintptr_t manager, const build::RenderManager& kind, std::uint32_t handle) noexcept {
|
|
if (handle < build::first_handle || handle > 1'000'000) return 0;
|
|
const std::uint64_t slot = std::uint64_t{handle - build::first_handle} + 16;
|
|
const auto high = std::bit_width(slot) - 1; // 4 and up
|
|
const auto page = pointer_at(manager + build::manager_pool_offset + 8 + 8 * (high - 4));
|
|
return page ? page + static_cast<std::uintptr_t>(slot - (std::uint64_t{1} << high)) * kind.record_size : 0;
|
|
}
|
|
// The render index of the entity's render object for one mode, through the managers' handles.
|
|
std::uint32_t render_index(std::uintptr_t base, std::uintptr_t entity, std::size_t mode) noexcept {
|
|
std::uint32_t handle{};
|
|
if (!memory::peek(entity + build::entity_render_handles_offset + mode * build::entity_render_handle_stride, handle))
|
|
return no_index;
|
|
for (const auto& kind : build::handle_chain) {
|
|
const auto manager = pointer_at(base + kind.global);
|
|
if (!manager || pointer_at(manager) != base + kind.vtable) return no_index;
|
|
const auto record = record_of(manager, kind, handle);
|
|
if (!record || !memory::peek(record + kind.handle_offset, handle)) return no_index;
|
|
}
|
|
return handle >= build::first_handle ? handle - build::first_handle : no_index;
|
|
}
|
|
|
|
// One of the local skater's render objects (riding or walking), while the client tick follows them.
|
|
bool local_object(State& s, std::uintptr_t object) noexcept {
|
|
std::uint32_t index{};
|
|
return GetTickCount64() <= s.indices_until.load(std::memory_order_acquire) && pointer_at(object) == s.base + build::render_object_vtable &&
|
|
memory::peek(object + build::render_index_offset, index) &&
|
|
(index == s.indices[0].load(std::memory_order_acquire) || index == s.indices[1].load(std::memory_order_acquire));
|
|
}
|
|
bool clear_byte(std::uintptr_t address) noexcept {
|
|
__try {
|
|
*reinterpret_cast<volatile std::uint8_t*>(address) = 0;
|
|
return true;
|
|
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
|
return false;
|
|
}
|
|
}
|
|
// Render threads, after each skinned draw packet: the local skater's visible one, as skinning
|
|
// matrices in the world.
|
|
void observe_packet(std::uintptr_t object, std::uintptr_t packet) {
|
|
auto& s = state();
|
|
std::uint32_t index{}, count{}, locked{};
|
|
std::uint8_t visible{}, relative{};
|
|
if (GetTickCount64() > s.indices_until.load(std::memory_order_acquire) ||
|
|
pointer_at(object) != s.base + build::render_object_vtable || !memory::peek(object + build::render_index_offset, index) ||
|
|
(index != s.indices[0].load(std::memory_order_acquire) && index != s.indices[1].load(std::memory_order_acquire)))
|
|
return;
|
|
if (!memory::peek(packet + build::packet_visible_offset, visible) || !visible ||
|
|
!memory::peek(packet + build::packet_bone_count_offset, count) || !count || count > most_bones ||
|
|
!memory::peek(packet + build::packet_relative_offset, relative) ||
|
|
!memory::peek(object + build::render_object_lock_offset, locked) || locked)
|
|
return;
|
|
const auto bones = pointer_at(packet + build::packet_bones_offset), root_at = pointer_at(packet + build::packet_root_offset);
|
|
std::vector<draw_packet::PackedBone> packed(count);
|
|
std::array<float, 4> root{};
|
|
game::LinearTransform actor{};
|
|
if (!bones || !memory::peek_bytes(bones, packed.data(), packed.size() * sizeof(draw_packet::PackedBone)) ||
|
|
(root_at && !memory::peek(root_at, root)) || !memory::peek(packet + build::packet_actor_offset, actor))
|
|
return;
|
|
const auto placed = draw_packet::placement(root, relative != 0, actor);
|
|
std::vector<game::LinearTransform> skin(count);
|
|
for (std::size_t bone = 0; bone < count; ++bone)
|
|
if (!draw_packet::skin(packed[bone], placed, skin[bone])) return;
|
|
{
|
|
std::lock_guard guard(s.lock);
|
|
s.skin = std::move(skin);
|
|
s.skin_at = GetTickCount64();
|
|
}
|
|
if (!s.found_logged.exchange(true))
|
|
logging::log(logging::Level::info, logging::Channel::graphics,
|
|
"X-ray: the renderer draws the local skater (render object {}, {} bones).", index, count);
|
|
}
|
|
void draw_packet_hook(std::uintptr_t object, std::uintptr_t packet) {
|
|
auto& s = state();
|
|
s.draw_original(object, packet);
|
|
LastError error;
|
|
try {
|
|
// Hidden on this screen (reskate.skater.hide): the packet goes out not visible.
|
|
if (s.hiding.load(std::memory_order_acquire) && packet && local_object(s, object)) {
|
|
if (clear_byte(packet + build::packet_visible_offset) && !s.hide_logged.exchange(true))
|
|
logging::log(logging::Level::info, logging::Channel::graphics, "Skater hide: the local skater's draw packets go out not visible.");
|
|
return;
|
|
}
|
|
observe_packet(object, packet);
|
|
} catch (...) { /* A lost picture is never worth the renderer. */ }
|
|
}
|
|
|
|
// The camera of the main picture: a sound world matrix, close to the client's own camera.
|
|
void observe_view(std::uintptr_t view) {
|
|
auto& s = state();
|
|
if (GetTickCount64() > s.indices_until.load(std::memory_order_acquire)) return;
|
|
std::uint32_t kind{};
|
|
std::array<std::byte, build::view_size> data{};
|
|
if (!memory::peek(view - build::view_kind_back, kind) || kind != build::main_view_kind ||
|
|
!memory::peek_bytes(view, data.data(), data.size()))
|
|
return;
|
|
std::array<float, 16> camera{};
|
|
float radians{};
|
|
std::memcpy(camera.data(), data.data() + build::view_camera_offset, sizeof(camera));
|
|
std::memcpy(&radians, data.data() + build::view_fov_offset, sizeof(radians));
|
|
for (std::size_t row = 0; row < 4; ++row) {
|
|
for (std::size_t axis = 0; axis < 3; ++axis)
|
|
if (!std::isfinite(camera[row * 4 + axis]) || std::abs(camera[row * 4 + axis]) > 1e7f) return;
|
|
camera[row * 4 + 3] = row == 3 ? 1.0f : 0.0f;
|
|
}
|
|
for (std::size_t row = 0; row < 3; ++row)
|
|
for (std::size_t other = row; other < 3; ++other) {
|
|
float dot{};
|
|
for (std::size_t axis = 0; axis < 3; ++axis) dot += camera[row * 4 + axis] * camera[other * 4 + axis];
|
|
if (std::abs(dot - (row == other ? 1.0f : 0.0f)) > 0.05f) return;
|
|
}
|
|
const float fov = radians * 180.0f / std::numbers::pi_v<float>;
|
|
const auto client = latest_game_view();
|
|
if (!std::isfinite(fov) || fov <= 1 || fov >= 175 || !client) return;
|
|
float distance{};
|
|
for (std::size_t axis = 0; axis < 3; ++axis) {
|
|
const float delta = camera[12 + axis] - client->world[12 + axis];
|
|
distance += delta * delta;
|
|
}
|
|
if (!(distance <= camera_reach * camera_reach)) return;
|
|
std::lock_guard guard(s.lock);
|
|
s.camera = camera;
|
|
s.vertical_fov = fov;
|
|
s.camera_at = GetTickCount64();
|
|
}
|
|
void render_view_hook(std::uintptr_t blackboard, std::uintptr_t current, std::uintptr_t previous, std::uint8_t jitter) {
|
|
auto& s = state();
|
|
s.view_original(blackboard, current, previous, jitter);
|
|
LastError error;
|
|
try {
|
|
observe_view(current);
|
|
} catch (...) { /* A lost camera is never worth the renderer. */ }
|
|
}
|
|
std::array<void*, 2> render_targets(std::uintptr_t base) noexcept {
|
|
return {reinterpret_cast<void*>(base + build::draw_packet_contract.rva),
|
|
reinterpret_cast<void*>(base + build::render_view_contract.rva)};
|
|
}
|
|
}
|
|
|
|
bool start_render(std::uintptr_t base) noexcept {
|
|
auto& s = state();
|
|
if (s.ready.load(std::memory_order_acquire)) return s.base == base;
|
|
for (const auto& contract : build::contracts) {
|
|
std::array<unsigned char, 32> actual{};
|
|
if (!memory::peek(base + contract.rva, actual) || actual != contract.bytes) {
|
|
logging::log(logging::Level::warning, logging::Channel::graphics,
|
|
"The skater x-ray cannot see where the skater is drawn: the native contract at 0x{:x} did not match.", contract.rva);
|
|
return false;
|
|
}
|
|
}
|
|
s.base = base;
|
|
const auto targets = render_targets(base);
|
|
const std::array replacements{reinterpret_cast<void*>(&draw_packet_hook), reinterpret_cast<void*>(&render_view_hook)};
|
|
std::array<void*, 2> originals{};
|
|
auto status = HookOk;
|
|
std::size_t prepared{};
|
|
for (; prepared < targets.size(); ++prepared) {
|
|
status = hook_prepare(targets[prepared], replacements[prepared], &originals[prepared]);
|
|
if (status != HookOk) break;
|
|
if (!originals[prepared]) {
|
|
++prepared;
|
|
status = HookUnsupportedFunction;
|
|
break;
|
|
}
|
|
}
|
|
if (status == HookOk) {
|
|
// Both relays are published before either target is enabled (hook_render).
|
|
s.draw_original = reinterpret_cast<DrawPacket>(originals[0]);
|
|
s.view_original = reinterpret_cast<RenderView>(originals[1]);
|
|
s.ready.store(true, std::memory_order_release);
|
|
return true;
|
|
}
|
|
logging::log(logging::Level::warning, logging::Channel::graphics,
|
|
"The skater x-ray cannot see where the skater is drawn: hooking the renderer failed (status {}).", static_cast<LONG>(status));
|
|
while (prepared) (void)hook_remove(targets[--prepared]);
|
|
return false;
|
|
}
|
|
|
|
bool hook_render(bool on) noexcept {
|
|
auto& s = state();
|
|
if (!s.ready.load(std::memory_order_acquire)) return !on; // nothing to hook
|
|
if (s.hooked == on) return true;
|
|
const auto targets = render_targets(s.base);
|
|
auto status = HookOk;
|
|
std::size_t changed{};
|
|
for (; changed < targets.size(); ++changed) {
|
|
status = on ? hook_enable(targets[changed]) : hook_disable(targets[changed]);
|
|
if (status != HookOk) break;
|
|
}
|
|
if (status == HookOk) {
|
|
s.hooked = on;
|
|
return true;
|
|
}
|
|
logging::log(logging::Level::warning, logging::Channel::graphics, "The skater x-ray could not {} its renderer hooks (status {}).",
|
|
on ? "enable" : "disable", static_cast<LONG>(status));
|
|
// Both or neither.
|
|
while (changed) {
|
|
const auto target = targets[--changed];
|
|
(void)(on ? hook_disable(target) : hook_enable(target));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void follow_render(bool wanted) noexcept {
|
|
auto& s = state();
|
|
if (!s.ready.load(std::memory_order_acquire)) return;
|
|
NoBailSkater skater;
|
|
if (!wanted || !no_bail_skater(skater)) {
|
|
s.indices_until.store(0, std::memory_order_release);
|
|
return;
|
|
}
|
|
for (std::size_t mode = 0; mode < build::render_modes; ++mode)
|
|
s.indices[mode].store(render_index(s.base, skater.entity, mode), std::memory_order_release);
|
|
s.indices_until.store(GetTickCount64() + lease_ms, std::memory_order_release);
|
|
}
|
|
|
|
void hide_skater(bool hidden) noexcept { state().hiding.store(hidden, std::memory_order_release); }
|
|
bool latest_picture(Picture& picture) noexcept {
|
|
auto& s = state();
|
|
const auto now = GetTickCount64();
|
|
std::lock_guard guard(s.lock);
|
|
if (!s.skin_at || !s.camera_at || now - s.skin_at > fresh_ms || now - s.camera_at > fresh_ms) return false;
|
|
try {
|
|
picture.skin = s.skin;
|
|
} catch (...) {
|
|
return false;
|
|
}
|
|
picture.camera = s.camera;
|
|
picture.vertical_fov = s.vertical_fov;
|
|
return true;
|
|
}
|
|
}
|