diff --git a/Engine/Game/Build/20260929/native_cosmetics.h b/Engine/Game/Build/20260929/native_cosmetics.h index 2511213..e56c2af 100644 --- a/Engine/Game/Build/20260929/native_cosmetics.h +++ b/Engine/Game/Build/20260929/native_cosmetics.h @@ -11,6 +11,12 @@ inline constexpr std::uintptr_t recipe_copy = 0x5230b0; inline constexpr std::array 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 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 diff --git a/Engine/Game/Build/20260929/startup_interventions.h b/Engine/Game/Build/20260929/startup_interventions.h index 11f7164..78a79b1 100644 --- a/Engine/Game/Build/20260929/startup_interventions.h +++ b/Engine/Game/Build/20260929/startup_interventions.h @@ -3,15 +3,23 @@ #include 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 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 frame_instruction{0x40, 0x88, 0x32}; // MOV [RDX], SIL +inline constexpr std::array 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 exit_instruction{ 0xff, 0x25, 0x7f, 0x43, 0x5e, 0xfd}; } diff --git a/Extension/Boot/startup_interventions.cpp b/Extension/Boot/startup_interventions.cpp index 5686681..0453d6a 100644 --- a/Extension/Boot/startup_interventions.cpp +++ b/Extension/Boot/startup_interventions.cpp @@ -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 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 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 bool read_local(std::uintptr_t address, T& output) noexcept { SIZE_T count{}; return address && ReadProcessMemory(GetCurrentProcess(), - reinterpret_cast(address), &output, sizeof(output), &count) && - count == sizeof(output); + reinterpret_cast(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(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(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(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(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(memory.BaseAddress) + memory.RegionSize; + !VirtualQuery(reinterpret_cast(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(memory.BaseAddress); return memory.State == MEM_COMMIT && memory.Type == MEM_IMAGE && reinterpret_cast(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(address), 1, PAGE_EXECUTE_READWRITE, - &old_protection)) return false; + if (!VirtualProtect(reinterpret_cast(address), 1, PAGE_EXECUTE_READWRITE, &old_protection)) return false; SIZE_T count{}; - const auto wrote = WriteProcessMemory(GetCurrentProcess(), reinterpret_cast(address), - &opcode, sizeof(opcode), &count) && count == sizeof(opcode); - const auto flushed = wrote && FlushInstructionCache( - GetCurrentProcess(), reinterpret_cast(address), 1) != FALSE; + const auto wrote = WriteProcessMemory(GetCurrentProcess(), reinterpret_cast(address), &opcode, 1, &count) && count == 1; + const auto flushed = wrote && FlushInstructionCache(GetCurrentProcess(), reinterpret_cast(address), 1) != FALSE; DWORD ignored{}; - const auto protected_again = VirtualProtect( - reinterpret_cast(address), 1, old_protection, &ignored) != FALSE; + const auto protected_again = VirtualProtect(reinterpret_cast(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(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( - 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 patched{}; - std::uint64_t state{}; - if (!read_local(address, patched) || patched[0] != breakpoint_opcode || - registers.rbp > std::numeric_limits::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 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 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(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 patched{}; - std::uint64_t return_address{}; - if (!read_local(address, patched) || patched[0] != breakpoint_opcode || - registers.rsp > std::numeric_limits::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 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& 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& instruction, - std::uint64_t state) noexcept { +bool StartupInterventionMachine::capture_frame(std::uintptr_t base, const StartupInterventionRegisters& registers, + const std::array& 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::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::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& 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::max() && - registers.rdx == 0x32 && return_matches && - instruction == startup_exit_instruction && - registers.rsp <= std::numeric_limits::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& 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::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& frame, const std::array& 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& 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(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(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 frame{}; + std::array frame{}; std::array 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 diff --git a/Extension/Boot/startup_interventions.h b/Extension/Boot/startup_interventions.h index aa36416..02639b8 100644 --- a/Extension/Boot/startup_interventions.h +++ b/Extension/Boot/startup_interventions.h @@ -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& 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& instruction, - std::uint64_t protected_state) noexcept; + bool capture_frame(std::uintptr_t image_base, const StartupInterventionRegisters& registers, + const std::array& instruction, + const StartupFrameMetadata& metadata, std::uint32_t thread_id) noexcept; bool return_from_exit(std::uintptr_t image_base, StartupInterventionRegisters& registers, const std::array& 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 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& exit_instruction) noexcept; +bool startup_intervention_fingerprints_match(std::uintptr_t image_base, std::uint32_t image_size, + const std::array& frame, const std::array& 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 diff --git a/Extension/Customization/developer_hoodie.cpp b/Extension/Customization/developer_hoodie.cpp new file mode 100644 index 0000000..0cb4dc8 --- /dev/null +++ b/Extension/Customization/developer_hoodie.cpp @@ -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 +#include + +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 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(binding.node + 0x10), color.data(), sizeof(color)); + *reinterpret_cast(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(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 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 diff --git a/Extension/Customization/developer_hoodie.h b/Extension/Customization/developer_hoodie.h new file mode 100644 index 0000000..3842763 --- /dev/null +++ b/Extension/Customization/developer_hoodie.h @@ -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 diff --git a/Extension/Customization/developer_hoodie_material.h b/Extension/Customization/developer_hoodie_material.h new file mode 100644 index 0000000..1c3d2b2 --- /dev/null +++ b/Extension/Customization/developer_hoodie_material.h @@ -0,0 +1,181 @@ +#pragma once +#include "Extension/Multiplayer/developer_identity.h" +#include "Extension/Multiplayer/Remote/native_cosmetics_layout.h" +#include +#include +#include +#include + +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 color_keys{ + 0x8c46cd354b18a9e2ULL, 0x1567b5742f4ec0b2ULL, 0x1564a942cb33c53bULL, + 0xf526305270d94cb3ULL, 0x99879bf202d1960aULL, 0x998450425dbd9044ULL}; +using Color = std::array; +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 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(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(milliseconds % 8000) * (6.f / 8000.f); + const auto sector = static_cast(hue); + constexpr float high = .8f, low = .025f; + const auto rising = low + (high - low) * (hue - static_cast(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 Materials materials(Read &read, std::uintptr_t base, std::uintptr_t entity) { + using namespace multiplayer; + CosmeticMemory 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(out.component, 0x10a) || m.template get(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(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(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(item, 0xb0) & 2)) { out.eligible = false; continue; } + const auto buckets = m.ptr(item, 0x1f0); + const auto size = m.template get(item, 0x1f8), total = m.template get(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(node) != hash) continue; + std::array 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(material, 0x270), count = m.template get(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(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(parameter, 2) == sizeof(Color) && + m.template get(m.ptr(type)) == 0x885eff59U && + m.template get(m.ptr(type), 6) == sizeof(Color) && m.template get(parameter, 6) == 1 && + (m.template get(parameter, 0x28) & 1) && + m.template get(type, 8) == m.template get(parameter, 4), + "Hoodie color parameter type differs."); + const auto color = m.template get(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 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 +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 diff --git a/Extension/Multiplayer/Hud/custom_nametags.h b/Extension/Multiplayer/Hud/custom_nametags.h index 8d6d168..16bc9b8 100644 --- a/Extension/Multiplayer/Hud/custom_nametags.h +++ b/Extension/Multiplayer/Hud/custom_nametags.h @@ -1,5 +1,6 @@ #pragma once #include "Extension/UI/Overlay/overlay.h" +#include "Extension/Multiplayer/developer_identity.h" #include #include #include @@ -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 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 players, diff --git a/Extension/Multiplayer/Session/session.cpp b/Extension/Multiplayer/Session/session.cpp index a383882..3a85b71 100644 --- a/Extension/Multiplayer/Session/session.cpp +++ b/Extension/Multiplayer/Session/session.cpp @@ -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(); diff --git a/Extension/Multiplayer/Session/session_internal.h b/Extension/Multiplayer/Session/session_internal.h index 2a8540b..0ac4d32 100644 --- a/Extension/Multiplayer/Session/session_internal.h +++ b/Extension/Multiplayer/Session/session_internal.h @@ -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{}; diff --git a/Extension/Multiplayer/developer_identity.h b/Extension/Multiplayer/developer_identity.h new file mode 100644 index 0000000..9e2ca9d --- /dev/null +++ b/Extension/Multiplayer/developer_identity.h @@ -0,0 +1,16 @@ +#pragma once +#include +#include + +namespace dingosdk::multiplayer { +// Shared by developer tags and cosmetics. Steam authenticates these identities. +inline constexpr std::array 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 diff --git a/Launcher/gui.cpp b/Launcher/gui.cpp index b11eeb0..57ea50d 100644 --- a/Launcher/gui.cpp +++ b/Launcher/gui.cpp @@ -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(parameter); + DWORD owner{}; + GetWindowThreadProcessId(window, &owner); + if (owner != target.process || !IsWindowVisible(window)) return TRUE; + std::array name{}; + GetClassNameW(window, name.data(), static_cast(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(&search)); + return search.found; +} + + } // namespace } // namespace detail @@ -258,6 +277,9 @@ int run(const launcher_app::Session& session, const std::vector& 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& 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& 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 (...) {} diff --git a/Launcher/gui.h b/Launcher/gui.h index 6cb961d..9e0c07c 100644 --- a/Launcher/gui.h +++ b/Launcher/gui.h @@ -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& arguments); diff --git a/Launcher/gui_internal.h b/Launcher/gui_internal.h index 6c71b5a..1908ff0 100644 --- a/Launcher/gui_internal.h +++ b/Launcher/gui_internal.h @@ -122,6 +122,7 @@ struct Settings { int console_key{static_cast(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(); diff --git a/Launcher/gui_launcher.cpp b/Launcher/gui_launcher.cpp index 7f50ad1..127fc1f 100644 --- a/Launcher/gui_launcher.cpp +++ b/Launcher/gui_launcher.cpp @@ -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(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(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."); } diff --git a/Launcher/gui_settings.cpp b/Launcher/gui_settings.cpp index a0291ca..5654415 100644 --- a/Launcher/gui_settings.cpp +++ b/Launcher/gui_settings.cpp @@ -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.", diff --git a/Runtime/client_tick.cpp b/Runtime/client_tick.cpp index 67704c4..af780d6 100644 --- a/Runtime/client_tick.cpp +++ b/Runtime/client_tick.cpp @@ -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 diff --git a/cmake/Runtime.cmake b/cmake/Runtime.cmake index c6f8f6b..190c429 100644 --- a/cmake/Runtime.cmake +++ b/cmake/Runtime.cmake @@ -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