#include "loading_screen.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/engine.h" #include "Engine/Game/Build/20260929/level_loading.h" #include "Engine/Game/Build/20260929/loading_screen.h" #include "Engine/Game/UI/loading_screen_selection.h" #include #include #include #include namespace dingosdk::loading_screen { namespace { using Address = std::uintptr_t; using Select = Address (*)(Address, bool); struct State { Address base{}; Select original{}; std::atomic active{}; std::mutex mutex; Pending pending; std::vector live; // Infos built for live screens, by level. They copy an authored info, so // the engine sees the same object layout; they are never freed. std::deque bundles; std::vector> built; }; State& state() { static auto* value = new State; return *value; } template T read(Address at) { T result{}; return memory::read(at, result) ? result : T{}; } std::string name(Address at) { std::array bytes{}; if (!at || !memory::read(at, bytes)) return {}; const auto end = std::find(bytes.begin(), bytes.end(), '\0'); return end == bytes.end() ? std::string{} : std::string(bytes.begin(), end); } Address checked_info(Address info, Address base) { info &= ~Address{4}; return read
(info + 8) == base + addr::loading_screen::info_type ? info : 0; } // RimeLoadScreenInfo: BundleName (char*) at +0x20, WidgetAssetGuid after it. constexpr Address info_bundle = 0x20; constexpr Address info_widget_default = 0x28; constexpr std::size_t info_copy = 0x100; // Where WidgetAssetGuid sits: found in an authored row whose widget guid is // known (a mod enabled at launch reports its rows too), else the default. Address widget_offset(State &s, Address overrides, unsigned count) { for (unsigned i = 0; i < count; ++i) { const auto entry = overrides + i * 48; const auto info = checked_info(read
(entry + 32), s.base); if (!info) continue; const auto level = name(read
(entry + 24)); for (const auto &live : s.live) { if (!same_map(live.level, level)) continue; std::array bytes{}; if (!memory::read(info, bytes)) continue; for (Address at = info_bundle + 8; at + 16 <= bytes.size(); at += 4) if (std::memcmp(bytes.data() + at, live.widget.data(), 16) == 0) return at; } } return info_widget_default; } // An info for a live screen: a copy of the config's first authored info with // this map's bundle and widget. Address live_info(State &s, const std::string &destination, Address overrides, unsigned count) { const LiveScreen *wanted{}; for (const auto &live : s.live) if (same_map(live.level, destination)) wanted = &live; if (!wanted) return 0; for (const auto &[level, info] : s.built) if (same_map(level, destination)) return info; Address donor{}; for (unsigned i = 0; i < count && !donor; ++i) donor = checked_info(read
(overrides + i * 48 + 32), s.base); if (!donor) return 0; std::array bytes{}; if (!memory::read(donor, bytes)) return 0; auto *copy = static_cast(VirtualAlloc(nullptr, info_copy, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)); if (!copy) return 0; std::memcpy(copy, bytes.data(), bytes.size()); s.bundles.push_back(wanted->bundle); const auto bundle = reinterpret_cast
(s.bundles.back().c_str()); const auto widget = widget_offset(s, overrides, count); std::memcpy(copy + info_bundle, &bundle, 8); std::memcpy(copy + widget, wanted->widget.data(), 16); const auto info = reinterpret_cast
(copy); s.built.emplace_back(wanted->level, info); logging::log(logging::Level::info, logging::Channel::level, "Loading screen for {} built in memory (widget guid at +{:#x})", wanted->level, widget); return info; } Address select(Address screen, bool boot) { auto& s = state(); const auto original = s.original(screen, boot); struct LastError { DWORD value{GetLastError()}; ~LastError() { SetLastError(value); } } last_error; try { std::string destination; { std::lock_guard lock(s.mutex); destination = s.pending.take(screen, boot, GetTickCount64()); } logging::log(logging::Level::info, logging::Channel::level, "Loading screen chosen (boot {}) for {}", boot, destination.empty() ? std::string("the game's own choice") : destination); if (destination.empty() || read
(screen) != s.base + addr::loading_screen::screen_vtable) return original; const auto config = read
(screen + 0x40); if (read
(config + 8) != s.base + addr::loading_screen::config_type) return original; const auto overrides = read
(config + 0x20); const auto count = read(overrides - 4) & 0x7fffffff; if (!overrides || count > 128) return original; for (unsigned i = 0; i < count; ++i) { const auto entry = overrides + i * 48; if (!same_map(name(read
(entry + 24)), destination)) continue; const auto custom = read
(entry); unsigned custom_count{}; if (custom && !memory::read(custom - 4, custom_count)) return original; custom_count &= 0x7fffffff; std::array conditions{}; if (!memory::read(entry + 8, conditions) || !unconditional(conditions, custom_count)) return original; if (const auto info = checked_info(read
(entry + 32), s.base)) return info; return original; } // A map enabled since launch has no row in the configuration the game read. { std::lock_guard lock(s.mutex); if (const auto info = live_info(s, destination, overrides, count)) return info; } // Custom maps have no authored override; don't reuse the previous map. if (const auto fallback = checked_info(read
(config + 0x30), s.base)) return fallback; } catch (...) { } return original; } } bool start(Address base) noexcept { try { auto& s = state(); if (s.active) return s.base == base; const auto& contract = addr::level_loading::loading_screen_select_contract; std::array bytes{}; if (!base || !memory::read(base + contract.rva, bytes) || bytes != contract.bytes) return false; s.base = base; auto* target = reinterpret_cast(base + contract.rva); void* original{}; if (hook_prepare(target, reinterpret_cast(&select), &original) != HookOk) return false; s.original = reinterpret_cast