diff --git a/Engine/Game/Build/20260929/load_errors.h b/Engine/Game/Build/20260929/load_errors.h new file mode 100644 index 0000000..bcf82f7 --- /dev/null +++ b/Engine/Game/Build/20260929/load_errors.h @@ -0,0 +1,21 @@ +#pragma once +#include "Engine/Game/Build/fingerprint.h" +#include + +// The game's errors of a load made of several (Extension/World/load_error_guard.cpp). +namespace dingosdk::game::build::v20260929::load_errors { +// describe(errors, text): a virtual of the error that holds one error per part of a load. It +// writes "[i]:" and each part's own description (its virtual at + 8), or "no error" for a null +// one, a line each. The parts are an array of pointers (errors + 0x18) and their count +// (errors + 0x20); the text is the game's fixed text writer, whose first field is its buffer +// with flags in the two low bits. +// +// The game calls it when a load fails, to write "load
failed with error: ..." +// (0x1aa75e0), and the array can hold parts that were never set: crash reports of 2026-10-11 +// (one player, six times, 2.0.5 and 2.0.6) all end in the call through part 8 after eight that +// read "no error" - once a null vtable, else garbage that was read or run. +inline constexpr Fingerprint describe{0x2e3ab40, { + 0x48,0x89,0x5c,0x24,0x08,0x48,0x89,0x74,0x24,0x10,0x57,0x48,0x83,0xec,0x20,0x33, + 0xff,0x48,0x8b,0xda,0x48,0x8b,0xf1,0x48,0x39,0x79,0x20,0x76,0x6b,0x0f,0x1f,0x00}}; +inline constexpr std::uintptr_t parts_offset = 0x18, count_offset = 0x20; +} diff --git a/Extension/World/load_error_guard.cpp b/Extension/World/load_error_guard.cpp new file mode 100644 index 0000000..432954f --- /dev/null +++ b/Extension/World/load_error_guard.cpp @@ -0,0 +1,115 @@ +#include "load_error_guard.h" +#include "Engine/Core/Hooks/hooks.h" +#include "Engine/Core/Log/logging.h" +#include "Engine/Core/Platform/memory.h" +#include "Engine/Game/Build/addresses.h" +#include "Engine/Game/Build/20260929/load_errors.h" +#include +#include +#include +#include + +namespace dingosdk::load_error_guard { +namespace { +namespace le = addr::load_errors; +using Describe = void (*)(void* errors, void* text); +Describe original{}; +std::uintptr_t image_begin{}, image_end{}; +std::atomic installed{}; +std::atomic empties{}, faults{}, told{}; +thread_local unsigned depth{}; // a part can be a list of its own: only the outermost is logged +constexpr std::uint64_t most_parts = 4096, most_told = 40; + +bool in_image(std::uintptr_t address) noexcept { return address >= image_begin && address < image_end; } + +// One of the game's own objects: its vtable and the virtual that describes it are in the image. +bool a_part(std::uintptr_t part) noexcept { + std::uintptr_t vtable{}, describe{}; + return memory::read(part, vtable) && in_image(vtable) && !(vtable & 7) && memory::read(vtable + 8, describe) && in_image(describe); +} +// Empties the parts that are not: how many. A list that cannot be read is left alone (the +// handler around the description is what is left for it). +unsigned empty_bad_parts(std::uintptr_t errors) noexcept { + std::uintptr_t parts{}; + std::uint64_t count{}; + if (!memory::read(errors + le::parts_offset, parts) || !memory::read(errors + le::count_offset, count) || !parts || count > most_parts) return 0; + unsigned emptied{}; + for (std::uint64_t i = 0; i < count; ++i) { + std::uintptr_t part{}; + if (!memory::read(parts + i * 8, part)) break; + if (!part || a_part(part)) continue; + __try { + *reinterpret_cast(parts + i * 8) = 0; + ++emptied; + } __except (EXCEPTION_EXECUTE_HANDLER) { + break; + } + } + return emptied; +} +bool describe_guarded(void* errors, void* text) noexcept { + __try { + original(errors, text); + return true; + } __except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { + return false; + } +} +// What the game wrote, for the log: the writer's buffer, on one line. +__declspec(noinline) void tell(std::uintptr_t text, unsigned emptied, bool whole) noexcept { + if (told.fetch_add(1, std::memory_order_relaxed) >= most_told) return; + try { + std::array buffer{}; + std::uintptr_t begin{}; + std::string said; + if (memory::read(text, begin) && (begin &= ~std::uintptr_t{3})) { + if (memory::peek_cstring(begin, buffer.data(), buffer.size()) < 0) buffer.back() = '\0'; + said = buffer.data(); + } + for (auto& c : said) + if (c == '\n' || c == '\r' || c == '\t') c = ' '; + std::string note; + if (emptied) note += " " + std::to_string(emptied) + " part(s) of its error list were never set, where the game would have crashed; they read as no error."; + if (!whole) note += " The description faulted and was cut short."; + logging::log(logging::Level::warning, logging::Channel::runtime, + "The game described the error of a load that failed (content that is missing or broken, often a mod's): {}{}", + said.empty() ? std::string("(nothing readable)") : said, note); + } catch (...) {} +} +void guarded(void* errors, void* text) { + const auto emptied = empty_bad_parts(reinterpret_cast(errors)); + if (emptied) empties.fetch_add(emptied, std::memory_order_relaxed); + ++depth; + const bool whole = describe_guarded(errors, text); + --depth; + if (!whole) faults.fetch_add(1, std::memory_order_relaxed); + if (!depth) tell(reinterpret_cast(text), emptied, whole); +} +} // namespace + +bool install(std::uintptr_t base) noexcept { + if (installed.exchange(true)) return original != nullptr; + if (!base) return false; + std::array head{}; + IMAGE_DOS_HEADER dos{}; + IMAGE_NT_HEADERS64 nt{}; + if (!memory::peek(base + le::describe.rva, head) || head != le::describe.bytes || !memory::peek(base, dos) || dos.e_lfanew <= 0 || + !memory::peek(base + static_cast(dos.e_lfanew), nt) || !nt.OptionalHeader.SizeOfImage) { + logging::log(logging::Level::warning, logging::Channel::runtime, + "Load error guard: this build's load error code differs; it is left as it is."); + return false; + } + image_begin = base; + image_end = base + nt.OptionalHeader.SizeOfImage; + const auto target = reinterpret_cast(base + le::describe.rva); + if (hook_prepare(target, reinterpret_cast(&guarded), reinterpret_cast(&original)) != HookOk || + hook_enable(target) != HookOk) { + original = nullptr; + logging::log(logging::Level::warning, logging::Channel::runtime, "Load error guard: the game's function could not be hooked."); + return false; + } + return true; +} + +std::uint64_t emptied() noexcept { return empties.load(std::memory_order_relaxed); } +} // namespace dingosdk::load_error_guard diff --git a/Extension/World/load_error_guard.h b/Extension/World/load_error_guard.h new file mode 100644 index 0000000..6b30553 --- /dev/null +++ b/Extension/World/load_error_guard.h @@ -0,0 +1,16 @@ +#pragma once +#include + +// A load that failed could crash the game instead: to say why, the game describes each part of +// the load's error in turn, and the list can hold parts that were never set +// (Engine/Game/Build//load_errors.h). The guard looks at the list first and empties every +// part that is not one of the game's own objects, so those read "no error" as an empty part +// does; the description itself runs under an exception handler as well. The failed load is then +// the game's to deal with, as it meant to. What the game wrote is put in the log, so a mod +// whose content does not load can be found. +namespace dingosdk::load_error_guard { +// Checks this build's function and hooks it; once. False when the build differs (no guard). +bool install(std::uintptr_t base) noexcept; +// How many parts were emptied since the game started. +std::uint64_t emptied() noexcept; +} diff --git a/Runtime/runtime_init.cpp b/Runtime/runtime_init.cpp index 1a39da0..8e6e295 100644 --- a/Runtime/runtime_init.cpp +++ b/Runtime/runtime_init.cpp @@ -6,6 +6,7 @@ #include "Engine/Core/Profiling/profiler.h" #include "Extension/Settings/engine_tweaks.h" #include "Extension/Settings/replay_guard.h" +#include "Extension/World/load_error_guard.h" #include "Extension/World/mesh_unload_guard.h" #include "Extension/Console/commands.h" #include "Extension/UI/ui_pointer_skip.h" @@ -317,6 +318,7 @@ extern "C" __declspec(dllexport) BOOL WINAPI DingoSDKDebugInitialize() { (void)dingosdk::engine_tweaks::install(r.base); (void)dingosdk::replay_guard::install(r.base); (void)dingosdk::mesh_unload_guard::install(r.base); + (void)dingosdk::load_error_guard::install(r.base); dingosdk::console::start_memory_trace_from_environment(); const bool fixed_stop_provider = (global_offline_hook && progression_guard_armed) && dingosdk::prepare_fixed_stop_entitlement_provider(r.base); diff --git a/cmake/Runtime.cmake b/cmake/Runtime.cmake index 3942e45..b282683 100644 --- a/cmake/Runtime.cmake +++ b/cmake/Runtime.cmake @@ -70,6 +70,7 @@ add_library(dingosdk_runtime SHARED Extension/Settings/job_spin.cpp Extension/Settings/engine_tweaks.cpp Extension/Settings/replay_guard.cpp + Extension/World/load_error_guard.cpp Extension/World/mesh_unload_guard.cpp Extension/Settings/replay_drafts.cpp Extension/UI/ui_pointer_skip.cpp