This commit is contained in:
zeex64 committed 2026-10-03 01:26:18 -05:00
commit 62b99171db
18 files changed
+608 -261

No files matched your search

@@ -11,6 +11,12 @@ inline constexpr std::uintptr_t recipe_copy = 0x5230b0;
inline constexpr std::array<unsigned char, 24> recipe_copy_prefix{
0x48,0x89,0x5c,0x24,0x08,0x57,0x48,0x83,0xec,0x20,0x48,0x8b,0x41,0x08,0x48,0x8b,
0xf9,0x48,0x8b,0x98,0x90,0x00,0x00,0x00};
// Publishes this AppearanceItem's per-instance named material map to its render meshes.
// (item, map_index), with map_index 0 for addMaterial("Top_mat").
inline constexpr std::uintptr_t publish_materials = 0x1331420;
inline constexpr std::array<unsigned char, 24> publish_materials_prefix{
0x48,0x89,0x5c,0x24,0x08,0x89,0x54,0x24,0x10,0x55,0x56,0x57,0x41,0x54,0x41,0x55,
0x41,0x56,0x41,0x57,0x48,0x83,0xec,0x20};
// Finds a named entry in a loaded table by the FNV-1a hash of its name:
// (out, name, table, flag, context) -> out, the fifth passed on the stack.
// The skater loader's preset lookups reach it through a Lua binding that
@@ -3,15 +3,23 @@
#include <cstdint>
namespace dingosdk::game::build::v20260929::startup_interventions {
// Interpreter frame clear (REP STOSB) that overlaps the protected frame state.
inline constexpr std::uintptr_t frame = 0x0880d7d5;
inline constexpr std::array<std::uint8_t, 2> frame_instruction{0xf3, 0xaa};
// Handler and data expected in R9/R14 at the frame clear.
inline constexpr std::uintptr_t frame_handler = 0x0880d710;
inline constexpr std::uintptr_t frame_data = 0x086772fc;
// Exit thunk (JMP [rip+...]) and the return address it is expected to be called from.
// Build 25414733 clears the saved native frame through the VM's byte store,
// not the 20260908 REP STOSB handler. Capture before its first zero byte.
inline constexpr std::uintptr_t frame = 0x08ae2785;
inline constexpr std::array<std::uint8_t, 3> frame_instruction{0x40, 0x88, 0x32}; // MOV [RDX], SIL
inline constexpr std::array<std::uint8_t, 8> frame_fingerprint{
0x40, 0x88, 0x32, 0x5e, 0x66, 0x8b, 0x41, 0x08};
inline constexpr std::uintptr_t frame_data = 0x08b7bf3e; // R11: clear bytecode
inline constexpr std::uintptr_t frame_cursor = 0x08b7bf72; // [RCX + 0xd8]
inline constexpr std::uintptr_t frame_size = 0xc0;
inline constexpr std::uintptr_t frame_stack_offset = 0xb70;
inline constexpr std::uintptr_t frame_vm_offset = 0xb40;
inline constexpr std::uintptr_t frame_saved_registers_offset = 0x1b0;
inline constexpr std::uintptr_t frame_cursor_offset = 0xd8;
inline constexpr std::uint64_t frame_count = 0xdaf400c0;
inline constexpr std::uintptr_t exit_stack_offset = 0x128;
inline constexpr std::uintptr_t exit_thunk = 0x08a06ff3;
inline constexpr std::uintptr_t exit_return = 0x087755f2;
inline constexpr std::uintptr_t exit_return = 0x087fe7fb;
inline constexpr std::array<std::uint8_t, 6> exit_instruction{
0xff, 0x25, 0x7f, 0x43, 0x5e, 0xfd};
}
+180 -198
View File
@@ -9,195 +9,201 @@
namespace dingosdk {
namespace {
// Re-observed on the supported image: the interpreter frame is based on RBP,
// and its 0x630-byte clear overlaps that frame. These are not Season 4 offsets.
constexpr std::uint64_t protected_state = 0x343cfc26;
constexpr std::uint32_t direction_flag = 0x400;
namespace pins = addr::startup_interventions;
constexpr std::uint8_t breakpoint_opcode = 0xcc;
StartupInterventionMachine machine;
std::atomic<std::uintptr_t> installed_base{};
// The frame site sits in protector code that is regenerated for every game
// build; when the table has no verified site for this build only the exit
// intervention is armed.
std::atomic_bool frame_armed{};
PVOID handler_handle{};
SRWLOCK handler_lock = SRWLOCK_INIT;
std::array<std::uint8_t, startup_frame_size> saved_frame{};
std::uintptr_t saved_frame_address{};
struct HandlerGuard {
DWORD last_error{GetLastError()};
HandlerGuard() noexcept { AcquireSRWLockExclusive(&handler_lock); }
~HandlerGuard() { ReleaseSRWLockExclusive(&handler_lock); SetLastError(last_error); }
};
template<class T>
bool read_local(std::uintptr_t address, T& output) noexcept {
SIZE_T count{};
return address && ReadProcessMemory(GetCurrentProcess(),
reinterpret_cast<const void*>(address), &output, sizeof(output), &count) &&
count == sizeof(output);
reinterpret_cast<const void*>(address), &output, sizeof(output), &count) && count == sizeof(output);
}
bool executable_image_range(std::uintptr_t image_base, std::uintptr_t address,
std::size_t size) noexcept {
bool writable_range(std::uintptr_t address, std::size_t size) noexcept {
MEMORY_BASIC_INFORMATION memory{};
if (!address || !size || !VirtualQuery(reinterpret_cast<const void*>(address), &memory, sizeof(memory)) ||
memory.State != MEM_COMMIT || (memory.Protect & (PAGE_GUARD | PAGE_NOACCESS))) return false;
switch (memory.Protect & 0xff) {
case PAGE_READWRITE: case PAGE_WRITECOPY: case PAGE_EXECUTE_READWRITE: case PAGE_EXECUTE_WRITECOPY: break;
default: return false;
}
const auto region = reinterpret_cast<std::uintptr_t>(memory.BaseAddress);
return address >= region && address - region < memory.RegionSize && size <= memory.RegionSize - (address - region);
}
bool write_local(std::uintptr_t address, const void* data, std::size_t size) noexcept {
SIZE_T count{};
return writable_range(address, size) && WriteProcessMemory(GetCurrentProcess(),
reinterpret_cast<void*>(address), data, size, &count) && count == size;
}
bool executable_image_range(std::uintptr_t image_base, std::uintptr_t address, std::size_t size) noexcept {
MEMORY_BASIC_INFORMATION memory{};
if (!image_base || !address || !size ||
!VirtualQuery(reinterpret_cast<const void*>(address), &memory, sizeof(memory))) return false;
const auto executable = [](DWORD protection) {
if (protection & (PAGE_GUARD | PAGE_NOACCESS)) return false;
switch (protection & 0xff) {
case PAGE_EXECUTE:
case PAGE_EXECUTE_READ:
case PAGE_EXECUTE_READWRITE:
case PAGE_EXECUTE_WRITECOPY:
return true;
default:
return false;
}
};
const auto end = reinterpret_cast<std::uintptr_t>(memory.BaseAddress) + memory.RegionSize;
!VirtualQuery(reinterpret_cast<const void*>(address), &memory, sizeof(memory)) ||
(memory.Protect & (PAGE_GUARD | PAGE_NOACCESS))) return false;
switch (memory.Protect & 0xff) {
case PAGE_EXECUTE: case PAGE_EXECUTE_READ: case PAGE_EXECUTE_READWRITE: case PAGE_EXECUTE_WRITECOPY: break;
default: return false;
}
const auto region = reinterpret_cast<std::uintptr_t>(memory.BaseAddress);
return memory.State == MEM_COMMIT && memory.Type == MEM_IMAGE &&
reinterpret_cast<std::uintptr_t>(memory.AllocationBase) == image_base &&
executable(memory.Protect) && address <= end && size <= end - address;
address >= region && address - region < memory.RegionSize && size <= memory.RegionSize - (address - region);
}
bool write_opcode(std::uintptr_t address, std::uint8_t opcode) noexcept {
DWORD old_protection{};
if (!VirtualProtect(reinterpret_cast<void*>(address), 1, PAGE_EXECUTE_READWRITE,
&old_protection)) return false;
if (!VirtualProtect(reinterpret_cast<void*>(address), 1, PAGE_EXECUTE_READWRITE, &old_protection)) return false;
SIZE_T count{};
const auto wrote = WriteProcessMemory(GetCurrentProcess(), reinterpret_cast<void*>(address),
&opcode, sizeof(opcode), &count) && count == sizeof(opcode);
const auto flushed = wrote && FlushInstructionCache(
GetCurrentProcess(), reinterpret_cast<void*>(address), 1) != FALSE;
const auto wrote = WriteProcessMemory(GetCurrentProcess(), reinterpret_cast<void*>(address), &opcode, 1, &count) && count == 1;
const auto flushed = wrote && FlushInstructionCache(GetCurrentProcess(), reinterpret_cast<void*>(address), 1) != FALSE;
DWORD ignored{};
const auto protected_again = VirtualProtect(
reinterpret_cast<void*>(address), 1, old_protection, &ignored) != FALSE;
const auto protected_again = VirtualProtect(reinterpret_cast<void*>(address), 1, old_protection, &ignored) != FALSE;
return wrote && flushed && protected_again;
}
StartupInterventionRegisters capture_registers(const CONTEXT& context,
std::uintptr_t exception_address) noexcept {
return {exception_address, context.Rsp, context.Rax, context.Rbx, context.Rcx,
context.Rdx, context.Rbp, context.Rsi, context.Rdi, context.R8,
context.R9, context.R14, context.EFlags};
StartupInterventionRegisters capture_registers(const CONTEXT& c, std::uintptr_t address) noexcept {
return {address, c.Rsp, c.Rax, c.Rbx, c.Rcx, c.Rdx, c.Rbp, c.Rsi, c.Rdi,
c.R8, c.R9, c.R10, c.R11, c.R14, c.R15, c.EFlags};
}
void apply_registers(CONTEXT& context, const StartupInterventionRegisters& registers) noexcept {
context.Rip = registers.rip;
context.Rsp = registers.rsp;
context.Rax = registers.rax;
context.Rcx = registers.rcx;
context.Rdi = registers.rdi;
bool frame_shape_matches(std::uintptr_t base, const StartupInterventionRegisters& r) noexcept {
return base == supported_build::preferred_image_base && r.rip == base + startup_frame_rva &&
r.rbp >= pins::frame_stack_offset && r.rsp == r.rbp - pins::frame_stack_offset &&
r.rcx == r.rbp - pins::frame_vm_offset && r.rdx == r.rbp - startup_frame_size &&
r.r15 == r.rbp - pins::frame_saved_registers_offset &&
r.r11 == base + startup_frame_data_rva && r.r9 == pins::frame_count && r.r10 == r.r9 &&
(r.rsi & 0xff) == 0;
}
LONG CALLBACK intervention_handler(EXCEPTION_POINTERS* pointers) {
if (!pointers || !pointers->ExceptionRecord || !pointers->ContextRecord ||
pointers->ExceptionRecord->ExceptionCode != EXCEPTION_BREAKPOINT)
pointers->ExceptionRecord->ExceptionCode != EXCEPTION_BREAKPOINT) return EXCEPTION_CONTINUE_SEARCH;
const auto base = installed_base.load(std::memory_order_acquire);
const auto address = reinterpret_cast<std::uintptr_t>(pointers->ExceptionRecord->ExceptionAddress);
if (!base || (address != base + startup_frame_rva && address != base + startup_exit_rva))
return EXCEPTION_CONTINUE_SEARCH;
const auto image_base = installed_base.load(std::memory_order_acquire);
if (!image_base) return EXCEPTION_CONTINUE_SEARCH;
const auto address = reinterpret_cast<std::uintptr_t>(
pointers->ExceptionRecord->ExceptionAddress);
auto registers = capture_registers(*pointers->ContextRecord, address);
HandlerGuard guard;
auto& context = *pointers->ContextRecord;
auto registers = capture_registers(context, address);
const auto thread = GetCurrentThreadId();
if (address == image_base + startup_frame_rva) {
std::array<std::uint8_t, 2> patched{};
std::uint64_t state{};
if (!read_local(address, patched) || patched[0] != breakpoint_opcode ||
registers.rbp > std::numeric_limits<std::uintptr_t>::max() - startup_frame_state_offset ||
!read_local(registers.rbp + startup_frame_state_offset, state)) {
machine.fail();
return EXCEPTION_CONTINUE_SEARCH;
if (address == base + startup_frame_rva) {
std::array<std::uint8_t, 3> instruction{};
if (!read_local(address, instruction) ||
(instruction[0] != breakpoint_opcode && instruction[0] != startup_frame_instruction[0])) return EXCEPTION_CONTINUE_SEARCH;
const bool patched = instruction[0] == breakpoint_opcode;
instruction[0] = startup_frame_instruction[0];
if (instruction != startup_frame_instruction) { machine.fail(); return EXCEPTION_CONTINUE_SEARCH; }
// Another thread may have fetched INT3 before capture restored the site.
if (!patched) { context.Rip = address; return EXCEPTION_CONTINUE_EXECUTION; }
if (machine.phase() == StartupInterventionPhase::armed && frame_shape_matches(base, registers)) {
StartupFrameMetadata metadata{};
if (!read_local(registers.rcx + startup_frame_cursor_offset, metadata.cursor)) {
machine.fail(); return EXCEPTION_CONTINUE_SEARCH;
}
if (startup_frame_clear_matches(base, registers, instruction, metadata)) {
const auto frame_address = registers.rbp - startup_frame_size;
if (!read_local(frame_address, saved_frame) ||
!machine.capture_frame(base, registers, instruction, metadata, thread) ||
!write_opcode(address, startup_frame_instruction[0])) {
machine.fail(); return EXCEPTION_CONTINUE_SEARCH;
}
saved_frame_address = frame_address;
// Let the original clear run; restore this exact frame only if the
// guarded exit arrives on the same thread and native stack frame.
context.Rip = address;
logging::log(logging::Level::info, logging::Channel::runtime,
"Startup frame captured: {} bytes, site {:#x}, bytecode {:#x}",
saved_frame.size(), startup_frame_rva, startup_frame_data_rva);
return EXCEPTION_CONTINUE_EXECUTION;
}
}
const std::array<std::uint8_t, 2> original{startup_frame_instruction[0], patched[1]};
if (!machine.preserve_frame(image_base, registers, original, state) ||
!write_opcode(address, startup_frame_instruction[0])) {
// This VM opcode also performs ordinary byte stores before the target
// clear. Reproduce MOV [RDX], SIL without changing flags or registers.
const auto byte = static_cast<std::uint8_t>(registers.rsi);
if (!write_local(registers.rdx, &byte, sizeof(byte))) {
machine.fail();
return EXCEPTION_CONTINUE_SEARCH;
logging::write(logging::Level::error, logging::Channel::runtime,
"Startup byte-store intervention could not replay an ordinary store");
if (!write_opcode(address, startup_frame_instruction[0])) return EXCEPTION_CONTINUE_SEARCH;
context.Rip = address;
return EXCEPTION_CONTINUE_EXECUTION;
}
apply_registers(*pointers->ContextRecord, registers);
context.Rip = address + startup_frame_instruction.size();
return EXCEPTION_CONTINUE_EXECUTION;
}
if (address == image_base + startup_exit_rva) {
std::array<std::uint8_t, 6> patched{};
std::uint64_t return_address{};
if (!read_local(address, patched) || patched[0] != breakpoint_opcode ||
registers.rsp > std::numeric_limits<std::uintptr_t>::max() - sizeof(return_address) ||
!read_local(registers.rsp, return_address)) {
machine.fail();
return EXCEPTION_CONTINUE_SEARCH;
}
patched[0] = startup_exit_instruction[0];
logging::log(logging::Level::info, logging::Channel::runtime,
"Startup exit intervention hit: return {:#x}, rcx {:#x}, rdx {:#x}, frame site {}",
return_address >= image_base ? return_address - image_base : return_address, registers.rcx,
registers.rdx, frame_armed.load() ? "armed" : "not armed");
if (!machine.return_from_exit(image_base, registers, patched, return_address) ||
!write_opcode(address, startup_exit_instruction[0])) {
machine.fail();
return EXCEPTION_CONTINUE_SEARCH;
}
apply_registers(*pointers->ContextRecord, registers);
return EXCEPTION_CONTINUE_EXECUTION;
std::array<std::uint8_t, 6> instruction{};
std::uint64_t return_address{};
if (!read_local(address, instruction) || instruction[0] != breakpoint_opcode ||
!read_local(registers.rsp, return_address)) { machine.fail(); return EXCEPTION_CONTINUE_SEARCH; }
instruction[0] = startup_exit_instruction[0];
logging::log(logging::Level::info, logging::Channel::runtime,
"Startup exit intervention hit: return {:#x}, rcx {:#x}, rdx {:#x}, frame captured {}",
return_address >= base ? return_address - base : return_address, registers.rcx, registers.rdx,
machine.phase() == StartupInterventionPhase::frame_captured);
if (!machine.return_from_exit(base, registers, instruction, return_address, thread) ||
!write_local(saved_frame_address, saved_frame.data(), saved_frame.size()) ||
!write_opcode(address, startup_exit_instruction[0])) {
machine.fail();
logging::write(logging::Level::error, logging::Channel::runtime, "Startup exit/frame restoration guard failed");
return EXCEPTION_CONTINUE_SEARCH;
}
return EXCEPTION_CONTINUE_SEARCH;
context.Rax = registers.rax;
context.Rip = registers.rip;
context.Rsp = registers.rsp;
logging::log(logging::Level::info, logging::Channel::runtime,
"Startup frame restored: {} bytes; exit intervention completed", saved_frame.size());
return EXCEPTION_CONTINUE_EXECUTION;
}
} // namespace
bool startup_frame_clear_matches(std::uintptr_t base, const StartupInterventionRegisters& registers,
const std::array<std::uint8_t, 3>& instruction, const StartupFrameMetadata& metadata) noexcept {
return frame_shape_matches(base, registers) && instruction == startup_frame_instruction &&
metadata.cursor == base + startup_frame_cursor_rva;
}
bool StartupInterventionMachine::arm() noexcept {
auto expected = StartupInterventionPhase::uninitialized;
return phase_.compare_exchange_strong(expected, StartupInterventionPhase::armed,
std::memory_order_acq_rel);
return phase_.compare_exchange_strong(expected, StartupInterventionPhase::armed, std::memory_order_acq_rel);
}
bool StartupInterventionMachine::preserve_frame(
std::uintptr_t image_base, StartupInterventionRegisters& registers,
const std::array<std::uint8_t, 2>& instruction,
std::uint64_t state) noexcept {
bool StartupInterventionMachine::capture_frame(std::uintptr_t base, const StartupInterventionRegisters& registers,
const std::array<std::uint8_t, 3>& instruction, const StartupFrameMetadata& metadata, std::uint32_t thread) noexcept {
auto expected = StartupInterventionPhase::armed;
if (!phase_.compare_exchange_strong(expected, StartupInterventionPhase::handling_frame,
std::memory_order_acq_rel)) return false;
const auto matches = image_base == supported_build::preferred_image_base &&
registers.rip == image_base + startup_frame_rva && registers.rsp == registers.rbp &&
registers.rcx == 0x630 &&
registers.rdi <= std::numeric_limits<std::uint64_t>::max() - 0x210 &&
registers.rdi + 0x210 == registers.rsp && registers.r8 == registers.rdi &&
registers.r9 == image_base + startup_frame_handler_rva &&
registers.r14 == image_base + startup_frame_data_rva &&
(registers.rax & 0xff) == 0 && !(registers.eflags & direction_flag) &&
instruction == startup_frame_instruction && state == protected_state &&
registers.rdi <= std::numeric_limits<std::uint64_t>::max() - registers.rcx;
if (!matches) {
phase_.store(StartupInterventionPhase::failed, std::memory_order_release);
return false;
}
registers.rdi += registers.rcx;
registers.rcx = 0;
registers.rip += instruction.size();
phase_.store(StartupInterventionPhase::frame_preserved, std::memory_order_release);
if (!phase_.compare_exchange_strong(expected, StartupInterventionPhase::handling_frame, std::memory_order_acq_rel)) return false;
if (!thread || !startup_frame_clear_matches(base, registers, instruction, metadata)) { fail(); return false; }
frame_base_ = registers.rbp;
frame_thread_ = thread;
phase_.store(StartupInterventionPhase::frame_captured, std::memory_order_release);
return true;
}
bool StartupInterventionMachine::return_from_exit(
std::uintptr_t image_base, StartupInterventionRegisters& registers,
const std::array<std::uint8_t, 6>& instruction,
std::uint64_t return_address) noexcept {
const bool frame = frame_armed.load(std::memory_order_acquire);
auto expected = frame ? StartupInterventionPhase::frame_preserved : StartupInterventionPhase::armed;
if (!phase_.compare_exchange_strong(expected, StartupInterventionPhase::handling_exit,
std::memory_order_acq_rel)) return false;
// With the frame site verified the exact protector return address is pinned
// too; without it, TerminateProcess(current process, 0x32) from this build's
// protector thunk back into the image is the guard.
const bool return_matches = return_address == image_base + startup_exit_return_rva ||
(!frame && return_address >= image_base && return_address - image_base < startup_expected_image_size);
const auto matches = image_base == supported_build::preferred_image_base &&
registers.rip == image_base + startup_exit_rva &&
registers.rcx == std::numeric_limits<std::uint64_t>::max() &&
registers.rdx == 0x32 && return_matches &&
instruction == startup_exit_instruction &&
registers.rsp <= std::numeric_limits<std::uint64_t>::max() - sizeof(return_address);
if (!matches) {
phase_.store(StartupInterventionPhase::failed, std::memory_order_release);
return false;
}
bool StartupInterventionMachine::return_from_exit(std::uintptr_t base, StartupInterventionRegisters& registers,
const std::array<std::uint8_t, 6>& instruction, std::uint64_t return_address, std::uint32_t thread) noexcept {
auto expected = StartupInterventionPhase::frame_captured;
if (!phase_.compare_exchange_strong(expected, StartupInterventionPhase::handling_exit, std::memory_order_acq_rel)) return false;
const bool matches = base == supported_build::preferred_image_base && registers.rip == base + startup_exit_rva &&
registers.rcx == std::numeric_limits<std::uint64_t>::max() && registers.rdx == 0x32 &&
instruction == startup_exit_instruction && return_address == base + startup_exit_return_rva &&
thread == frame_thread_ && registers.rbp == frame_base_ &&
frame_base_ >= pins::exit_stack_offset && registers.rsp == frame_base_ - pins::exit_stack_offset;
if (!matches) { fail(); return false; }
registers.rax = FALSE;
registers.rip = return_address;
registers.rsp += sizeof(return_address);
@@ -205,84 +211,60 @@ bool StartupInterventionMachine::return_from_exit(
return true;
}
void StartupInterventionMachine::fail() noexcept {
phase_.store(StartupInterventionPhase::failed, std::memory_order_release);
void StartupInterventionMachine::fail() noexcept { phase_.store(StartupInterventionPhase::failed, std::memory_order_release); }
StartupInterventionPhase StartupInterventionMachine::phase() const noexcept { return phase_.load(std::memory_order_acquire); }
bool startup_intervention_fingerprints_match(std::uintptr_t base, std::uint32_t size,
const std::array<std::uint8_t, 8>& frame, const std::array<std::uint8_t, 6>& exit) noexcept {
return base == supported_build::preferred_image_base && size == startup_expected_image_size &&
frame == startup_frame_fingerprint && exit == startup_exit_instruction;
}
StartupInterventionPhase StartupInterventionMachine::phase() const noexcept {
return phase_.load(std::memory_order_acquire);
}
bool startup_intervention_fingerprints_match(
std::uintptr_t image_base, std::uint32_t image_size,
const std::array<std::uint8_t, 6>& exit_instruction) noexcept {
return image_base == supported_build::preferred_image_base &&
image_size == startup_expected_image_size &&
exit_instruction == startup_exit_instruction;
}
bool start_startup_interventions(std::uintptr_t image_base, std::string& error) noexcept {
bool start_startup_interventions(std::uintptr_t base, std::string& error) noexcept {
error.clear();
try {
if (handler_handle || installed_base.load())
throw std::runtime_error("Startup interventions are already installed");
if (handler_handle || installed_base.load()) throw std::runtime_error("Startup interventions are already installed");
IMAGE_DOS_HEADER dos{};
if (!read_local(image_base, dos) || dos.e_magic != IMAGE_DOS_SIGNATURE ||
dos.e_lfanew <= 0 || dos.e_lfanew > 0x100000)
if (!read_local(base, dos) || dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew <= 0 || dos.e_lfanew > 0x100000)
throw std::runtime_error("Startup intervention image has an invalid DOS header");
IMAGE_NT_HEADERS64 nt{};
if (!read_local(image_base + static_cast<std::uintptr_t>(dos.e_lfanew), nt) ||
nt.Signature != IMAGE_NT_SIGNATURE ||
nt.FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64 ||
nt.OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC)
if (!read_local(base + static_cast<std::uintptr_t>(dos.e_lfanew), nt) || nt.Signature != IMAGE_NT_SIGNATURE ||
nt.FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64 || nt.OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC)
throw std::runtime_error("Startup intervention image has an invalid PE header");
const auto frame_address = image_base + startup_frame_rva;
const auto exit_address = image_base + startup_exit_rva;
if (exit_address < image_base ||
!executable_image_range(image_base, exit_address, startup_exit_instruction.size()))
const auto frame_address = base + startup_frame_rva;
const auto exit_address = base + startup_exit_rva;
if (!executable_image_range(base, frame_address, startup_frame_fingerprint.size()) ||
!executable_image_range(base, exit_address, startup_exit_instruction.size()))
throw std::runtime_error("Startup intervention sites are outside executable image pages");
std::array<std::uint8_t, 2> frame{};
std::array<std::uint8_t, 8> frame{};
std::array<std::uint8_t, 6> exit{};
if (!read_local(exit_address, exit) ||
!startup_intervention_fingerprints_match(image_base,
nt.OptionalHeader.SizeOfImage, exit))
if (!read_local(frame_address, frame) || !read_local(exit_address, exit) ||
!startup_intervention_fingerprints_match(base, nt.OptionalHeader.SizeOfImage, frame, exit))
throw std::runtime_error("Startup intervention image fingerprint does not match");
const bool arm_frame = frame_address >= image_base &&
executable_image_range(image_base, frame_address, startup_frame_instruction.size()) &&
read_local(frame_address, frame) && frame == startup_frame_instruction;
frame_armed.store(arm_frame, std::memory_order_release);
installed_base.store(image_base, std::memory_order_release);
installed_base.store(base, std::memory_order_release);
handler_handle = AddVectoredExceptionHandler(1, intervention_handler);
if (!handler_handle) {
installed_base.store(0, std::memory_order_release);
throw std::runtime_error("Cannot install startup intervention exception handler");
if (!handler_handle) { installed_base.store(0); throw std::runtime_error("Cannot install startup intervention exception handler"); }
bool frame_attempted = false;
if (!write_opcode(exit_address, breakpoint_opcode))
error = "Cannot arm the guarded startup-exit intervention";
if (error.empty()) {
frame_attempted = true;
if (!write_opcode(frame_address, breakpoint_opcode))
error = "Cannot arm the guarded frame-capture intervention";
}
bool frame_patched = false;
bool exit_patched = false;
if (write_opcode(exit_address, breakpoint_opcode)) exit_patched = true;
else error = "Cannot arm the guarded startup-exit intervention";
if (error.empty() && arm_frame) {
if (write_opcode(frame_address, breakpoint_opcode)) frame_patched = true;
else error = "Cannot arm the guarded frame-preservation intervention";
}
if (error.empty() && !machine.arm())
error = "Cannot arm the startup intervention state machine";
if (error.empty() && !machine.arm()) error = "Cannot arm the startup intervention state machine";
if (!error.empty()) {
if (frame_patched) write_opcode(frame_address, startup_frame_instruction[0]);
if (exit_patched) write_opcode(exit_address, startup_exit_instruction[0]);
if (frame_attempted) write_opcode(frame_address, startup_frame_instruction[0]);
write_opcode(exit_address, startup_exit_instruction[0]);
RemoveVectoredExceptionHandler(handler_handle);
handler_handle = nullptr;
installed_base.store(0, std::memory_order_release);
installed_base.store(0);
machine.fail();
return false;
}
logging::log(logging::Level::info, logging::Channel::runtime,
"Startup interventions armed: frame {:#x}, exit {:#x}", startup_frame_rva, startup_exit_rva);
return true;
} catch (const std::exception& exception) {
error = exception.what();
machine.fail();
return false;
}
} catch (const std::exception& exception) { error = exception.what(); machine.fail(); return false; }
}
} // namespace dingosdk
+29 -36
View File
@@ -1,6 +1,5 @@
#pragma once
#include "Engine/Game/Build/image_identity.h"
#include "Engine/Game/Build/supported_build.h"
#include "Engine/Game/Build/addresses.h"
#include "Engine/Game/Build/20260929/startup_interventions.h"
@@ -14,67 +13,61 @@ namespace dingosdk {
inline constexpr std::uint32_t startup_expected_image_size = supported_build::game_image_size;
inline constexpr std::uintptr_t startup_frame_rva = addr::startup_interventions::frame;
inline constexpr std::uintptr_t startup_frame_handler_rva = addr::startup_interventions::frame_handler;
inline constexpr std::uintptr_t startup_frame_data_rva = addr::startup_interventions::frame_data;
inline constexpr std::uintptr_t startup_frame_state_offset = 0x70;
inline constexpr std::uintptr_t startup_frame_cursor_rva = addr::startup_interventions::frame_cursor;
inline constexpr std::uintptr_t startup_frame_size = addr::startup_interventions::frame_size;
inline constexpr std::uintptr_t startup_frame_cursor_offset = addr::startup_interventions::frame_cursor_offset;
inline constexpr std::uintptr_t startup_exit_rva = addr::startup_interventions::exit_thunk;
inline constexpr std::uintptr_t startup_exit_return_rva = addr::startup_interventions::exit_return;
inline constexpr auto startup_frame_instruction = addr::startup_interventions::frame_instruction;
inline constexpr auto startup_frame_fingerprint = addr::startup_interventions::frame_fingerprint;
inline constexpr auto startup_exit_instruction = addr::startup_interventions::exit_instruction;
enum class StartupInterventionPhase : std::uint32_t {
uninitialized,
armed,
handling_frame,
frame_preserved,
handling_exit,
complete,
failed,
uninitialized, armed, handling_frame, frame_captured, handling_exit, complete, failed,
};
struct StartupInterventionRegisters {
std::uint64_t rip{};
std::uint64_t rsp{};
std::uint64_t rax{};
std::uint64_t rbx{};
std::uint64_t rcx{};
std::uint64_t rdx{};
std::uint64_t rbp{};
std::uint64_t rsi{};
std::uint64_t rdi{};
std::uint64_t r8{};
std::uint64_t r9{};
std::uint64_t r14{};
std::uint64_t rip{}, rsp{}, rax{}, rbx{}, rcx{}, rdx{}, rbp{}, rsi{}, rdi{};
std::uint64_t r8{}, r9{}, r10{}, r11{}, r14{}, r15{};
std::uint32_t eflags{};
};
// Pure guarded state machine shared by the runtime exception handler and its
// focused tests. A matching transition is accepted once; a guard mismatch at
// an armed site permanently fails the sequence.
struct StartupFrameMetadata {
std::uint64_t cursor{};
};
bool startup_frame_clear_matches(std::uintptr_t image_base,
const StartupInterventionRegisters& registers,
const std::array<std::uint8_t, 3>& instruction,
const StartupFrameMetadata& metadata) noexcept;
// One-shot capture/exit sequence. The runtime serializes transitions and owns
// the frame bytes; ordinary VM byte stores do not advance this state machine.
class StartupInterventionMachine {
public:
bool arm() noexcept;
bool preserve_frame(std::uintptr_t image_base, StartupInterventionRegisters& registers,
const std::array<std::uint8_t, 2>& instruction,
std::uint64_t protected_state) noexcept;
bool capture_frame(std::uintptr_t image_base, const StartupInterventionRegisters& registers,
const std::array<std::uint8_t, 3>& instruction,
const StartupFrameMetadata& metadata, std::uint32_t thread_id) noexcept;
bool return_from_exit(std::uintptr_t image_base, StartupInterventionRegisters& registers,
const std::array<std::uint8_t, 6>& instruction,
std::uint64_t return_address) noexcept;
std::uint64_t return_address, std::uint32_t thread_id) noexcept;
void fail() noexcept;
StartupInterventionPhase phase() const noexcept;
private:
std::atomic<StartupInterventionPhase> phase_{StartupInterventionPhase::uninitialized};
std::uint64_t frame_base_{};
std::uint32_t frame_thread_{};
};
// The frame site is checked separately: it is optional (see start_startup_interventions).
bool startup_intervention_fingerprints_match(
std::uintptr_t image_base, std::uint32_t image_size,
const std::array<std::uint8_t, 6>& exit_instruction) noexcept;
bool startup_intervention_fingerprints_match(std::uintptr_t image_base, std::uint32_t image_size,
const std::array<std::uint8_t, 8>& frame, const std::array<std::uint8_t, 6>& exit) noexcept;
// Installs two process-wide, one-shot breakpoint interventions for the exact
// inspected Skate image. The caller keeps all existing game threads suspended
// during installation; future worker threads are covered by the code sites.
// Installs both verified sites while the caller holds existing game threads
// suspended. Capture retires the byte-store breakpoint; exit restores the
// captured native frame and retires the exit breakpoint before returning FALSE.
bool start_startup_interventions(std::uintptr_t image_base, std::string& error) noexcept;
} // namespace dingosdk
@@ -0,0 +1,87 @@
#include "developer_hoodie.h"
#include "Engine/Core/Platform/memory.h"
#include "Engine/Core/Log/logging.h"
#include "Extension/Multiplayer/Steam/steam_social.h"
#include "Extension/Multiplayer/Remote/native_skater.h"
#include <Windows.h>
#include <cstring>
namespace dingosdk {
namespace {
// Report a changed failure/activation at most once per actor per five seconds.
// Cosmetic streaming is transient; no per-frame log output or cached writes.
void report_hoodie(std::uintptr_t entity, std::uint64_t steam_id, std::string_view message,
logging::Level level) noexcept {
struct Report { std::uintptr_t entity{}; std::uint64_t at{}; std::uint32_t message{}; };
static std::array<Report, 33> reports{};
const auto now = GetTickCount64();
const auto hash = developer_hoodie_detail::material_hash(message);
auto *entry = &reports.front();
for (auto &candidate : reports) {
if (candidate.entity == entity) { entry = &candidate; break; }
if (candidate.at < entry->at) entry = &candidate;
}
if (entry->entity == entity && (entry->message == hash || now - entry->at < 5000)) return;
*entry = {entity, now, hash};
logging::log(level, logging::Channel::customization, "Developer hoodie (Steam {}, actor {:#x}): {}",
steam_id, entity, message);
}
// NativeValue is owned by this material. Preserve its padding, type, priority,
// flags and key, and set the same dirty byte as the native addEsVector setter.
bool write_color(const developer_hoodie_detail::Binding &binding, const developer_hoodie_detail::Color &color) noexcept {
__try {
std::memcpy(reinterpret_cast<void *>(binding.node + 0x10), color.data(), sizeof(color));
*reinterpret_cast<std::uint8_t *>(binding.material + 0x2f6) = 1;
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
}
bool publish_materials(std::uintptr_t base, std::uintptr_t item) noexcept {
__try {
reinterpret_cast<void (*)(std::uintptr_t, int)>(base + addr::native_cosmetics::publish_materials)(item, 0);
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
}
} // namespace
void update_developer_hoodie(std::uintptr_t base, std::uintptr_t entity, std::uint64_t steam_id,
std::uint64_t generation, DeveloperHoodieState &state) noexcept {
const bool developer = multiplayer::reskate_developer(steam_id);
if (!entity) { state = {}; return; }
if (!developer && !state.count) return;
try {
auto read = [](std::uintptr_t address, void *out, std::size_t size) { return memory::peek_bytes(address, out, size); };
std::array<unsigned char, addr::native_cosmetics::publish_materials_prefix.size()> prefix{};
if (!read(base + addr::native_cosmetics::publish_materials, prefix.data(), prefix.size()) ||
prefix != addr::native_cosmetics::publish_materials_prefix) {
report_hoodie(entity, steam_id, "material publisher differs from the supported build", logging::Level::warning);
state = {}; return;
}
const auto live = developer_hoodie_detail::materials(read, base, entity);
bool published = true;
auto publish = [base, &published](std::uintptr_t item) { published = publish_materials(base, item); };
developer_hoodie_detail::animate(state, live, entity, generation, developer, GetTickCount64(), write_color, publish);
if (!published) report_hoodie(entity, steam_id, "native material publication failed", logging::Level::warning);
else if (state.count) report_hoodie(entity, steam_id, "RGB active on the owned hoodie material", logging::Level::info);
else if (developer && live.eligible)
report_hoodie(entity, steam_id, "waiting for the assigned top slot's color parameters", logging::Level::warning);
} catch (const std::exception &error) {
report_hoodie(entity, steam_id, error.what(), logging::Level::warning);
} catch (...) {
// Streaming/replacement may invalidate a controller. Never recover by
// writing cached addresses, or let an optional cosmetic stop the session.
}
}
void tick_local_developer_hoodie(std::uintptr_t base, std::uintptr_t client, bool ready) noexcept {
static DeveloperHoodieState state;
// Pausing/loading can temporarily make the local actor unavailable without
// destroying it. Keep originals until a fresh ownership walk succeeds.
if (!ready) return;
try {
const auto social = multiplayer::steam_social_snapshot();
const auto id = social ? social->local.id : 0;
if (!multiplayer::reskate_developer(id) && !state.count) return;
const auto local = multiplayer::capture_local(base, client, false);
if (local.ready) update_developer_hoodie(base, local.entity, id, local.context, state);
else report_hoodie(client, id, local.detail, logging::Level::warning);
} catch (...) {}
}
} // namespace dingosdk
@@ -0,0 +1,10 @@
#pragma once
#include "developer_hoodie_material.h"
namespace dingosdk {
// Verified client update thread only. Remote identity comes from the Steam roster,
// generation from the current native actor; neither is a client cosmetic parameter.
void update_developer_hoodie(std::uintptr_t base, std::uintptr_t entity, std::uint64_t steam_id,
std::uint64_t generation, DeveloperHoodieState &state) noexcept;
void tick_local_developer_hoodie(std::uintptr_t base, std::uintptr_t client, bool ready) noexcept;
} // namespace dingosdk
@@ -0,0 +1,181 @@
#pragma once
#include "Extension/Multiplayer/developer_identity.h"
#include "Extension/Multiplayer/Remote/native_cosmetics_layout.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
namespace dingosdk {
namespace developer_hoodie_detail {
inline constexpr std::string_view asset = "Own_TopShirt_Gen_HoodieRelaxed_00001";
// The compiled appearance preset names controllers by their assigned CAS slot.
// "TopItem" is a Lua construction name, not the live controller's name.
inline constexpr std::string_view top_slot = "Characters/MainCharacters/Generic/CAS/Common/Slots/CAS_Top_Slot";
// Serialized ParamDbKey identities from this hoodie's AppearanceShaderExpressionPreset.
// Only the six fabric color regions: textures, stitches and other clothing stay intact.
inline constexpr std::array<std::uint64_t, 6> color_keys{
0x8c46cd354b18a9e2ULL, 0x1567b5742f4ec0b2ULL, 0x1564a942cb33c53bULL,
0xf526305270d94cb3ULL, 0x99879bf202d1960aULL, 0x998450425dbd9044ULL};
using Color = std::array<float, 3>;
struct Binding {
std::uintptr_t material{}, node{}, type{};
std::uint64_t key{};
Color color{};
bool operator==(const Binding &) const = default;
};
struct SavedColor {
Binding binding;
Color original{}, last{};
};
inline constexpr std::size_t max_bindings = color_keys.size() * 4;
struct Materials {
std::uintptr_t component{}, appearance{}, item{};
std::array<Binding, max_bindings> bindings{};
std::size_t count{};
bool eligible{};
};
inline std::uint32_t material_hash(std::string_view name) noexcept {
std::uint32_t hash = 5381;
for (auto c : name) hash = hash * 33 ^ static_cast<unsigned char>(c >= 'A' && c <= 'Z' ? c + 32 : c);
return hash;
}
inline Color rainbow(std::uint64_t milliseconds) noexcept {
// One continuous hue cycle every eight seconds, in the material's existing gamut.
const auto hue = static_cast<float>(milliseconds % 8000) * (6.f / 8000.f);
const auto sector = static_cast<unsigned>(hue);
constexpr float high = .8f, low = .025f;
const auto rising = low + (high - low) * (hue - static_cast<float>(sector));
const auto falling = high + low - rising;
switch (sector) {
case 0: return {high, rising, low};
case 1: return {falling, high, low};
case 2: return {low, high, rising};
case 3: return {low, falling, high};
case 4: return {rising, low, high};
default: return {high, low, falling};
}
}
// All discovery is bounded and follows the actor's own component/controller/material
// backlinks. Never edit the shared AppearanceShaderExpressionPreset or recipe.
template<class Read> Materials materials(Read &read, std::uintptr_t base, std::uintptr_t entity) {
using namespace multiplayer;
CosmeticMemory<Read &> m{read, base};
Materials out;
m.check(m.ptr(entity) == base + addr::engine::skater_entity_vtable, "Hoodie actor type differs.");
out.component = m.component(entity);
const bool pending = m.template get<std::uint8_t>(out.component, 0x10a) || m.template get<std::uint8_t>(out.component, 0x10d);
const auto recipe = m.ptr(out.component, 0x158);
const auto items = m.count(recipe, sizeof(NativeCosmeticItem), max_cosmetic_slots);
const auto wanted = cosmetic_asset_hash(asset);
for (std::size_t i = 0; i < items; ++i) {
const auto item = m.template get<NativeCosmeticItem>(recipe, i * sizeof(NativeCosmeticItem));
if (item.hash == wanted) {
// Native recipes may store only the hash (an empty CString).
const auto name = m.text(reinterpret_cast<std::uintptr_t>(item.asset));
out.eligible = !pending && (name.empty() || name == asset);
}
}
out.appearance = read_native_component(read, entity, base + addr::engine::skater_appearance_vtable);
const auto begin = m.ptr(out.appearance, 0x90), end = m.ptr(out.appearance, 0x98), capacity = m.ptr(out.appearance, 0xa0);
m.check(begin <= end && end <= capacity && (end - begin) % 8 == 0 && (capacity - begin) % 8 == 0 &&
(capacity - begin) / 8 <= 64 && (begin || !capacity), "Hoodie controller array exceeds bounds.");
for (auto entry = begin; entry < end; entry += 8) {
const auto item = m.ptr(entry);
if (!item || m.ptr(item, 0x48) != out.appearance + 0x40 || m.text(m.ptr(item, 0x30)) != top_slot) continue;
m.check(!out.item, "Hoodie has multiple top controllers.");
out.item = item;
if (!(m.template get<std::uint32_t>(item, 0xb0) & 2)) { out.eligible = false; continue; }
const auto buckets = m.ptr(item, 0x1f0);
const auto size = m.template get<std::uint32_t>(item, 0x1f8), total = m.template get<std::uint32_t>(item, 0x1fc);
if (!total) continue;
m.check(size && size <= 256 && total <= 128, "Hoodie material map exceeds bounds.");
const auto sentinel = m.ptr(buckets, size * 8ULL);
const auto hash = material_hash("Top_mat");
auto node = m.ptr(buckets, (hash % size) * 8ULL);
for (std::uint32_t visited = 0; node && node != sentinel && visited < total; ++visited, node = m.ptr(node, 0x28)) {
if (m.template get<std::uint32_t>(node) != hash) continue;
std::array<std::uintptr_t, 4> seen{};
for (std::size_t variant = 0; variant < seen.size(); ++variant) {
const auto material = m.ptr(node, 8 + variant * 8);
if (!material || std::find(seen.begin(), seen.begin() + variant, material) != seen.begin() + variant) continue;
seen[variant] = material;
const auto params = m.ptr(material, 0x268);
const auto n = m.template get<std::uint32_t>(material, 0x270), count = m.template get<std::uint32_t>(material, 0x274);
if (!count) continue;
m.check(n && n <= 1024 && count <= 512, "Hoodie parameter map exceeds bounds.");
const auto stop = m.ptr(params, n * 8ULL);
for (const auto key : color_keys) {
auto parameter = m.ptr(params, (key % n) * 8ULL);
for (std::uint32_t v = 0; parameter && parameter != stop && v < count; ++v, parameter = m.ptr(parameter, 0x40)) {
if (m.template get<std::uint64_t>(parameter, 8) != key) continue;
// The EBX boxed ColorRgb has a serialized gamut word; the
// loaded native ColorRgb is three floats (12 bytes). Validate
// the actual type descriptor, not the serialized asset size.
const auto type = m.ptr(parameter, 0x20);
m.check(type >= base && type < base + 0x09144000 &&
m.template get<std::uint16_t>(parameter, 2) == sizeof(Color) &&
m.template get<std::uint32_t>(m.ptr(type)) == 0x885eff59U &&
m.template get<std::uint16_t>(m.ptr(type), 6) == sizeof(Color) && m.template get<std::uint16_t>(parameter, 6) == 1 &&
(m.template get<std::uint32_t>(parameter, 0x28) & 1) &&
m.template get<std::uint16_t>(type, 8) == m.template get<std::uint16_t>(parameter, 4),
"Hoodie color parameter type differs.");
const auto color = m.template get<Color>(parameter, 0x10);
m.check(std::all_of(color.begin(), color.end(), [](float c) { return std::isfinite(c); }) &&
out.count < out.bindings.size(), "Hoodie color exceeds bounds.");
out.bindings[out.count++] = {material, parameter, type, key, color};
break;
}
}
}
break;
}
}
return out;
}
inline bool same_binding(const Binding &a, const Binding &b) noexcept {
return a.material == b.material && a.node == b.node && a.type == b.type && a.key == b.key;
}
} // namespace developer_hoodie_detail
struct DeveloperHoodieState {
std::uintptr_t entity{}, component{}, appearance{}, item{};
std::uint64_t generation{};
std::array<developer_hoodie_detail::SavedColor, developer_hoodie_detail::max_bindings> colors{};
std::size_t count{};
};
namespace developer_hoodie_detail {
// Writes are only to freshly rediscovered bindings. Restoration additionally
// requires the current color to still be ours; a new native preset takes precedence.
template<class Write, class Publish>
void animate(DeveloperHoodieState &state, const Materials &live, std::uintptr_t entity, std::uint64_t generation,
bool developer, std::uint64_t milliseconds, Write &write, Publish &publish) {
if (state.entity != entity || state.generation != generation || state.component != live.component ||
state.appearance != live.appearance || state.item != live.item) state = {};
const bool enabled = developer && live.eligible;
const auto color = rainbow(milliseconds);
DeveloperHoodieState next;
if (enabled) {
next.entity = entity; next.generation = generation; next.component = live.component;
next.appearance = live.appearance; next.item = live.item;
}
bool changed{};
for (std::size_t i = 0; i < live.count; ++i) {
const auto &binding = live.bindings[i];
const SavedColor *saved{};
for (std::size_t j = 0; j < state.count; ++j)
if (same_binding(binding, state.colors[j].binding)) { saved = &state.colors[j]; break; }
if (enabled) {
const auto original = saved && binding.color == saved->last ? saved->original : binding.color;
if (binding.color == color || write(binding, color)) {
next.colors[next.count++] = {binding, original, color};
changed = changed || binding.color != color;
}
} else if (saved && binding.color == saved->last && binding.color != saved->original) {
changed = write(binding, saved->original) || changed;
}
}
state = next;
if (changed) publish(live.item);
}
} // namespace developer_hoodie_detail
} // namespace dingosdk
+2 -10
View File
@@ -1,5 +1,6 @@
#pragma once
#include "Extension/UI/Overlay/overlay.h"
#include "Extension/Multiplayer/developer_identity.h"
#include <array>
#include <cstdint>
#include <optional>
@@ -22,16 +23,7 @@ inline constexpr std::uint32_t nametag_white = 0xffffffffU, nametag_developer =
nametag_host = 0xffffc85eU, // blue
nametag_friend = 0xff8ae07bU; // green
inline constexpr std::uint32_t nametag_developer_start = 0xffff206eU; // #6E20FF, IM_COL32 layout
// ReSkate's developers, by SteamID64 (Steam authenticates it; it cannot be claimed).
inline constexpr std::array<std::uint64_t, 2> reskate_developers{
76561198084159190ULL, // zee_x64
76561198255588397ULL, // reglitched
};
inline bool reskate_developer(std::uint64_t id) noexcept {
for (const auto developer : reskate_developers)
if (developer == id) return true;
return false;
}
// Client thread, once per rendered frame: everyone to label and the local skater's position
// (for distances). Also reads whether the game is hiding its own nametags right now.
void publish_custom_nametags(std::uintptr_t base, std::vector<NametagPlayer> players,
@@ -444,6 +444,7 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui
// the spectate position are still refreshed (sound events are released one per frame).
if (was_visible && local.ready && !p.render_failed && p.far_interval && now < p.next_far_sample &&
!(p.far_for_view && !out_of_view(p, view))) {
update_developer_hoodie(s.base, remote_skater_entity(), p.member.id, remote_skater_generation(), p.developer_hoodie);
present_audio(p);
update_party_position(&p.render_pose);
if (labels) label(p);
@@ -518,6 +519,8 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui
sampled ? "Waiting for the player's cosmetic recipe." : "Waiting for player poses.";
}
}
update_developer_hoodie(s.base, p.visible ? remote_skater_entity() : 0, p.member.id,
remote_skater_generation(), p.developer_hoodie);
if (!p.visible) {
stop_remote_audio();
p.presented_audio.reset();
@@ -1,6 +1,7 @@
#pragma once
#include "Engine/Game/Multiplayer/chat_rate.h"
#include "session.h"
#include "Extension/Customization/developer_hoodie.h"
#include "Extension/Multiplayer/Remote/native_skater.h"
#include "Extension/Multiplayer/Voice/voice_chat.h"
#include "Extension/Multiplayer/Steam/steam_transport.h"
@@ -96,6 +97,7 @@ struct Peer {
// Bumped per accepted outfit; render compares it with the one its actor wears
// (0 after a spawn) to spread native recipe applies over frames.
std::uint64_t cosmetic_revision{}, applied_cosmetics{};
DeveloperHoodieState developer_hoodie;
AudioBuffer audio;
std::uint32_t voice_sequence{};
bool received_voice{};
@@ -0,0 +1,16 @@
#pragma once
#include <array>
#include <cstdint>
namespace dingosdk::multiplayer {
// Shared by developer tags and cosmetics. Steam authenticates these identities.
inline constexpr std::array<std::uint64_t, 2> reskate_developers{
76561198084159190ULL, // zee_x64
76561198255588397ULL, // reglitched
};
inline bool reskate_developer(std::uint64_t id) noexcept {
for (const auto developer : reskate_developers)
if (developer == id) return true;
return false;
}
} // namespace dingosdk::multiplayer
+52 -6
View File
@@ -173,6 +173,25 @@ void apply_style() {
}
// True once `process` shows its startup splash (ReSkateStartupWindow) or, if the
// splash is disabled, the game's own window.
bool game_window_shown(DWORD process) {
struct Search { DWORD process; bool found; } search{process, false};
EnumWindows([](HWND window, LPARAM parameter) -> BOOL {
auto& target = *reinterpret_cast<Search*>(parameter);
DWORD owner{};
GetWindowThreadProcessId(window, &owner);
if (owner != target.process || !IsWindowVisible(window)) return TRUE;
std::array<wchar_t, 64> name{};
GetClassNameW(window, name.data(), static_cast<int>(name.size()));
const std::wstring_view kind(name.data());
if (kind == L"ReSkateStartupWindow" || kind == L"Skate") { target.found = true; return FALSE; }
return TRUE;
}, reinterpret_cast<LPARAM>(&search));
return search.found;
}
} // namespace
} // namespace detail
@@ -258,6 +277,9 @@ int run(const launcher_app::Session& session, const std::vector<std::wstring>& a
auto& ui = *ui_storage;
ModsPanel mods_panel;
bool running = true;
HANDLE game{};
DWORD game_id{};
bool hidden{};
while (running) {
MSG message;
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
@@ -270,12 +292,35 @@ int run(const launcher_app::Session& session, const std::vector<std::wstring>& a
launcher.restart();
break;
}
// The launcher is done the moment injection succeeded and Skate is running
// on its own, so close immediately instead of staying beside the game. This
// is the earliest safe point: a launch that fails never sets launched(), so
// it still gets to report itself in the window.
if (launcher.launched()) break;
if (IsIconic(window)) { Sleep(50); continue; }
// A successful launch closes immediately unless the user keeps the
// launcher alive. In that mode it sleeps hidden while the game runs.
if (launcher.launched() && !launcher.settings().keep_open_after_launch) break;
if (!game && launcher.game()) {
game_id = launcher.game();
game = OpenProcess(SYNCHRONIZE, FALSE, game_id);
}
if (game) {
if (!hidden && game_window_shown(game_id)) {
ShowWindow(window, SW_HIDE);
hidden = true;
}
if (hidden && !launcher.busy() && launcher.snapshot().phase == Phase::failed) {
ShowWindow(window, SW_SHOWNORMAL);
SetForegroundWindow(window);
hidden = false;
}
if (WaitForSingleObject(game, 0) == WAIT_OBJECT_0) {
CloseHandle(game);
game = nullptr;
launcher.game_exited(hidden);
if (hidden) {
ShowWindow(window, SW_SHOWNORMAL);
SetForegroundWindow(window);
hidden = false;
}
}
}
if (hidden || IsIconic(window)) { Sleep(hidden ? 100 : 50); continue; }
ImGui_ImplDX12_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
@@ -284,6 +329,7 @@ int run(const launcher_app::Session& session, const std::vector<std::wstring>& a
renderer.render();
}
ShowWindow(window, SW_HIDE);
if (game) CloseHandle(game);
launcher.cancel();
// Closing the window with Settings still open must not lose changes.
try { launcher.save(); } catch (...) {}
+2 -1
View File
@@ -7,7 +7,8 @@
namespace dingosdk::launcher_gui {
// Runs the windowed launcher until the user closes it or Skate starts.
// Runs the launcher until it is closed. The saved setting can retain it hidden
// while Skate runs and show it again after the game exits.
// `arguments` are the launcher's own command-line arguments, kept for a
// restart after a self-update.
int run(const launcher_app::Session& session, const std::vector<std::wstring>& arguments);
+4 -2
View File
@@ -122,6 +122,7 @@ struct Settings {
int console_key{static_cast<int>(launcher::default_console_key)};
int log_level{2};
std::string arguments;
bool keep_open_after_launch{};
// Off: never replace ReSkate.dll or the launcher (keeps a test build someone handed out).
bool updates{true};
// Off: a crash uploads nothing (RESKATE_CRASH_REPORTING=0 for the launcher and the game).
@@ -159,9 +160,10 @@ public:
bool restart_requested() const { return restart_; }
// Process id of the Skate this launcher started, once it exists.
DWORD game() const { return game_; }
// Injection finished and the game is running on its own; the launcher closes
// as soon as this is true.
// Injection finished and the game is running on its own.
bool launched() const { return launched_; }
// Resets the launch state after the retained game process exits.
void game_exited(bool seen);
Settings& settings() { return settings_; }
const launcher_app::Session& session() const { return session_; }
void save();
+12
View File
@@ -56,6 +56,8 @@ Settings load_settings(const fs::path& path) {
for (std::size_t index = 0; index < log_levels.size(); ++index)
if (level == log_levels[index]) settings.log_level = static_cast<int>(index);
settings.arguments = json.value("arguments", std::string());
// Keep the original settings key so existing launcher preferences survive.
settings.keep_open_after_launch = !json.value("close_on_launch", !settings.keep_open_after_launch);
settings.updates = json.value("updates", settings.updates);
settings.crash_reports = json.value("crash_reports", settings.crash_reports);
settings.steam_username = json.value("steam_username", std::string());
@@ -79,6 +81,7 @@ void save_settings(const fs::path& path, const Settings& settings) {
json["console_key"] = settings.console_key;
json["log_level"] = log_levels[static_cast<std::size_t>(settings.log_level)];
json["arguments"] = settings.arguments;
json["close_on_launch"] = !settings.keep_open_after_launch;
json["updates"] = settings.updates;
json["crash_reports"] = settings.crash_reports;
json["steam_username"] = settings.steam_username;
@@ -131,6 +134,14 @@ State Launcher::snapshot() {
return state_;
}
void Launcher::game_exited(bool seen) {
game_ = 0;
launched_ = false;
if (busy_ || snapshot().phase == Phase::failed) return;
if (seen) check();
else fail("Skate closed while it was starting.");
}
void Launcher::save() {
save_settings(settings_path(session_), settings_);
// The game inherits the launcher's environment, so this reaches the next launch; the
@@ -390,6 +401,7 @@ void Launcher::run_play() {
launched_ = true;
set(Phase::launching, "Skate is starting", options.offline || !launcher_app::steam_signed_in() ?
"Offline mode: playing without Steam as Unknown Player; multiplayer is hidden." :
settings_.keep_open_after_launch ? "The launcher hides while you play and returns when Skate closes." :
"The launcher closes now; Skate keeps starting on its own.");
}
+3
View File
@@ -183,6 +183,9 @@ void settings_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui
break;
default:
section_caption(fonts, "LAUNCHER");
setting_check("Keep the launcher open after launch",
"Hide while you play and return when Skate closes. Turn off to close after a successful launch.",
settings.keep_open_after_launch);
setting_check("Install ReSkate updates",
"Turn off to keep the ReSkate.dll and launcher you have, such as a test build someone gave you. "
"Skate's own Steam updates are not affected.",
+2
View File
@@ -1,4 +1,5 @@
#include "runtime_internal.h"
#include "Extension/Customization/developer_hoodie.h"
#include "Extension/Assets/live_mods.h"
#include "Engine/Core/Log/logging.h"
#include "Engine/Core/Profiling/profiler.h"
@@ -983,6 +984,7 @@ void tick(std::uintptr_t client, std::uintptr_t update) {
DINGO_PROFILE_ZONE("tick/multiplayer");
dingosdk::multiplayer::tick(r.base,client,multiplayer_ready,r.multiplayer_map,load_multiplayer_map);
}
dingosdk::tick_local_developer_hoodie(r.base, client, multiplayer_ready);
// The session spawns and places skaters and can teleport: check the camera again.
tick_state.camera_issue.reset();
// The spectate camera follows a moving skater, so it runs every client tick, after the
+1
View File
@@ -91,6 +91,7 @@ add_library(dingosdk_runtime SHARED
Extension/World/level_loading.cpp
Extension/World/loading_screen.cpp
Extension/Customization/preset_lookup_guard.cpp
Extension/Customization/developer_hoodie.cpp
Extension/Skater/skater_model.cpp
Extension/Skater/skater_observer.cpp
Extension/Skater/client_source_spawn.cpp