Files
ReSkate/Extension/World/load_error_guard.cpp
T
AyeZeeBB 89a97caed7 Plugins: an item grid's own list and marks, items worn on top, keys kept from the game, the menu's category icons
General APIs from pull request 269 (the player's own item tabs and outfit layers), whose tabs,
panels and prompts are now a plugin's (contrib/plugins/item-tabs, kept on this machine):
- reskate.menu.grid_show(keys): a plugin's list in the item grid that has the focus, drawn from
  every grid of the screen; reskate.menu.grid_mark(keys): the game's check on tiles. menu.grid()
  also says whether a list shows, whether it is empty, and where the grid's own prompts end.
- reskate.outfit.on_top(items) and on_top_state(): items worn on top of the outfit in the slots it
  leaves empty, the item given the slot's category, a slot tried and left if the game reports a
  failed load (ReSkate.layer_slots.txt remembers which load where); paid store items refused.
  Part of the shared outfit service, with reskate.outfit.set.
- reskate.input.game_keys(keys): presses the game does not see, by any way it reads the keyboard;
  "Delete" is a key name.
- d:game_image draws the customization menu's 17 category icons ("cas_tops", ...).
dingosdk_plugin_tests skips the test of a contrib plugin that is not on this machine.
2026-10-21 19:18:01 -05:00

120 lines
5.3 KiB
C++

#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 <Windows.h>
#include <array>
#include <atomic>
#include <string>
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<bool> installed{};
std::atomic<std::uint64_t> empties{}, faults{}, told{}, described_loads{};
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<std::uintptr_t*>(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<char, 1024> 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<std::uintptr_t>(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) {
described_loads.fetch_add(1, std::memory_order_relaxed);
tell(reinterpret_cast<std::uintptr_t>(text), emptied, whole);
}
}
} // namespace
bool install(std::uintptr_t base) noexcept {
if (installed.exchange(true)) return original != nullptr;
if (!base) return false;
std::array<unsigned char, 32> 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<std::uintptr_t>(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<void*>(base + le::describe.rva);
if (hook_prepare(target, reinterpret_cast<void*>(&guarded), reinterpret_cast<void**>(&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); }
std::uint64_t described() noexcept { return described_loads.load(std::memory_order_relaxed); }
} // namespace dingosdk::load_error_guard