Merge upstream/main (v1.0.5/v1.0.6): best-GPU launcher, per-archive mod index, UTF-8 paths

- Conflict in launcher_support.h: kept Linux guards, took upstream's
  validated_remote_load_library(note) signature + comment.
- Upstream's path_utf8() helper is portable; merged cleanly into the
  Linux closure. Build + 8/8 portable ctest green.
This commit is contained in:
juan committed 2026-10-03 21:37:35 -03:00
commit 7fe51ecf26
26 files changed
+427 -97

No files matched your search

+82 -4
View File
@@ -1,5 +1,6 @@
#include "launcher_support.h"
#include "launcher_support_internal.h"
#include "path_text.h"
#include <Windows.h>
#include <TlHelp32.h>
@@ -21,6 +22,60 @@ using detail::fail;
using detail::fail_windows;
using detail::Handle;
// What is loaded into the child that Windows did not put there. One of these
// is usually what hooked LoadLibraryW, and naming it beats telling someone to
// go looking for "security software".
std::string foreign_modules(HANDLE process) {
std::array<wchar_t, MAX_PATH> windows{};
const auto length = GetWindowsDirectoryW(windows.data(), static_cast<UINT>(windows.size()));
const std::wstring system_root(windows.data(), length && length < windows.size() ? length : 0);
Handle snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetProcessId(process)));
if (snapshot.get() == INVALID_HANDLE_VALUE || !snapshot.get()) return {};
MODULEENTRY32W entry{};
entry.dwSize = sizeof(entry);
std::wstring game_root;
std::string names;
std::size_t shown = 0;
for (auto more = Module32FirstW(snapshot.get(), &entry); more;
more = Module32NextW(snapshot.get(), &entry)) {
const std::wstring path(entry.szExePath);
// The first module is Skate.exe itself; everything beside it is the
// install, which is not what we are looking for.
if (game_root.empty()) {
game_root = fs::path(path).parent_path().wstring();
continue;
}
const auto under = [&path](const std::wstring& root) {
return !root.empty() && path.size() > root.size() &&
_wcsnicmp(path.c_str(), root.c_str(), root.size()) == 0;
};
if (under(system_root) || under(game_root)) continue;
if (shown == 8) { names += ", and more"; break; }
names += (names.empty() ? "" : ", ") + path_utf8(fs::path(entry.szModule));
++shown;
}
return names;
}
// The length of a jump written over a function's first bytes, or zero when the
// bytes are not one of the forms a hooking engine writes. Anti-virus and
// overlays hook by patching a jump to their own code at the entry and running
// the original afterwards; what they do not do is replace the whole function.
std::size_t inline_jump_length(const unsigned char* bytes, std::size_t size) {
const auto at = [&](std::size_t index) { return index < size ? bytes[index] : 0u; };
if (size >= 5 && (at(0) == 0xE9 || at(0) == 0xE8)) return 5; // jmp/call rel32
if (size >= 6 && at(0) == 0x68 && at(5) == 0xC3) return 6; // push imm32; ret
if (size >= 6 && at(0) == 0xFF && at(1) == 0x25) {
// jmp [rip+rel32]. The common form targets the eight bytes that follow.
const bool next = at(2) == 0 && at(3) == 0 && at(4) == 0 && at(5) == 0;
return next && size >= 14 ? 14 : 6;
}
if (size >= 12 && at(0) == 0x48 && at(1) == 0xB8 && at(10) == 0xFF && at(11) == 0xE0)
return 12; // mov rax, imm64; jmp rax
if (size >= 2 && at(0) == 0xEB) return 2; // short jmp, as hotpatching writes
return 0;
}
bool executable(DWORD protection) {
if (protection & (PAGE_GUARD | PAGE_NOACCESS)) return false;
switch (protection & 0xff) {
@@ -390,7 +445,7 @@ LoaderGate prepare_loader_for_injection(HANDLE process, HANDLE primary_thread,
}
}
std::uintptr_t validated_remote_load_library(HANDLE process) {
std::uintptr_t validated_remote_load_library(HANDLE process, std::string* note) {
const auto kernel32 = GetModuleHandleW(L"kernel32.dll");
if (!kernel32) fail_windows("Cannot locate local kernel32.dll");
const auto symbol = GetProcAddress(kernel32, "LoadLibraryW");
@@ -468,9 +523,32 @@ std::uintptr_t validated_remote_load_library(HANDLE process) {
const auto clean = image_file_bytes(fs::path(std::wstring(provider_path, provider_length)),
static_cast<std::uint32_t>(address - local_base), fingerprint_size, remote_base);
const bool matches_file = clean.size() == actual.size() && std::equal(clean.begin(), clean.end(), actual.begin());
if (!matches_file && actual != local_bytes)
fail("Child LoadLibraryW entry bytes match neither Windows' kernel32 file nor this process. "
"Security software may be hooking new processes; try allowing ReSkateLauncher.exe and Skate.exe.");
// A hook that writes a jump to a per-process trampoline leaves different
// bytes in the launcher and in the child, so neither comparison above can
// match even though nothing is wrong. Accept that shape when the jump is
// the only difference: everything past it is still Windows' own code, and
// the provider image already matched by timestamp, checksum and size.
bool hooked_entry = false;
if (!matches_file && actual != local_bytes && clean.size() == actual.size()) {
const auto jump = inline_jump_length(actual.data(), actual.size());
hooked_entry = jump > 0 && jump < actual.size() &&
std::equal(clean.begin() + static_cast<std::ptrdiff_t>(jump), clean.end(),
actual.begin() + static_cast<std::ptrdiff_t>(jump));
}
if (!matches_file && actual != local_bytes && !hooked_entry) {
const auto foreign = foreign_modules(process);
std::string message = "Skate's LoadLibraryW has been replaced, not just hooked: its entry bytes match "
"neither Windows' kernel32 file nor this process, and what follows them is not "
"Windows' code either.";
if (!foreign.empty()) message += " Loaded into Skate and not part of Windows: " + foreign + ".";
fail(message.c_str());
}
if (hooked_entry && note) {
*note = "Skate's LoadLibraryW is hooked at its entry; the rest of it is Windows' own, so ReSkate is "
"loading through it.";
if (const auto foreign = foreign_modules(process); !foreign.empty())
*note += " Loaded into Skate and not part of Windows: " + foreign + ".";
}
return address;
}
+4 -1
View File
@@ -136,7 +136,10 @@ LoaderGate prepare_loader_for_injection(HANDLE process, HANDLE primary_thread,
// Returns the ordinary LoadLibraryW address for a same-architecture suspended
// child only after verifying the matching KnownDLL mapping and entry bytes.
std::uintptr_t validated_remote_load_library(HANDLE process);
// An entry hooked by anti-virus or an overlay is allowed through when the rest
// of the function is still Windows' own; `note`, when given, is then filled
// with what was found so the caller can log it.
std::uintptr_t validated_remote_load_library(HANDLE process, std::string* note = nullptr);
#else
// Linux: virtual-key names and loader injection are Windows-only.
inline constexpr unsigned default_menu_key = 0x2D;
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <filesystem>
#include <string>
namespace dingosdk {
// A path as narrow text.
//
// std::filesystem::path::string() converts to the process's ANSI code page and
// throws when a character has no mapping there: "No mapping for the Unicode
// character exists in the target multi-byte code page". A folder named in
// Cyrillic or CJK is enough, and so is an invisible one -- a directory called
// U+200E (left-to-right mark) has been seen in the wild. The failure lands far
// from the cause, because the path is usually only being written into a log or
// an error message at the time.
//
// UTF-8 can represent every path Windows allows, so every path that becomes a
// narrow string goes through here instead.
inline std::string path_utf8(const std::filesystem::path& path) {
const auto text = path.u8string();
return {text.begin(), text.end()};
}
} // namespace dingosdk
+12 -1
View File
@@ -21,7 +21,8 @@ int main(int argc, char** argv) {
const std::filesystem::path root = argv[1];
const auto catalog = world_layer_scan::scan(root);
check(catalog.anchors.size() == 6, "one anchor per map");
check(catalog.nodes.size() >= 200 && catalog.layers.size() == catalog.nodes.size(), "one row per layer");
check(catalog.nodes.size() >= 200 && catalog.layers.size() < catalog.nodes.size() &&
catalog.layers.size() + 8 > catalog.nodes.size(), "a row per layer but the essential ones");
for (std::size_t i = 0; i < catalog.nodes.size(); ++i) {
const auto parent = catalog.nodes[i].parent;
check(parent >= -1 && parent < static_cast<int>(i), "parents come first");
@@ -31,6 +32,16 @@ int main(int argc, char** argv) {
};
for (const auto* key : {"bam_tod_1_morning", "bam_tod_7_weathernight", "grom_tod_5_night", "bam_debug_tests"})
check(has(key), key);
// No row for what every map is built out of: a player cannot switch off
// the world and then wonder which row did it.
check(std::none_of(catalog.layers.begin(), catalog.layers.end(), [](const WorldLayer& layer) {
return layer.key.find("coregameassets") != std::string::npos;
}), "CoreGameAssets has no row to switch");
check(std::any_of(catalog.nodes.begin(), catalog.nodes.end(), [](const WorldLayerNode& node) {
std::string bundle = node.bundle;
std::transform(bundle.begin(), bundle.end(), bundle.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return bundle.find("coregameassets") != std::string::npos;
}), "but it keeps its node, so its children are still reachable");
check(std::none_of(catalog.nodes.begin(), catalog.nodes.end(), [](const WorldLayerNode& node) {
std::string bundle = node.bundle;
std::transform(bundle.begin(), bundle.end(), bundle.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
+4 -3
View File
@@ -1,5 +1,6 @@
#include "game_archives.h"
#include "Engine/Resource/cas_codec.h"
#include "Engine/Core/Platform/path_text.h"
#include <cstdio>
#include <cstring>
#include <fstream>
@@ -26,7 +27,7 @@ Layout read_layout(const fs::path& path) {
Layout layout{{std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()}, {}};
if (layout.buffer.size() <= native_db::envelope_size ||
std::memcmp(layout.buffer.data(), native_db::magic, sizeof(native_db::magic)))
throw std::runtime_error("Unrecognized native envelope in " + path.string());
throw std::runtime_error("Unrecognized native envelope in " + path_utf8(path));
layout.root = native_db::read(std::span<const unsigned char>(layout.buffer).subspan(native_db::envelope_size),
"layout.toc", nullptr, {.unique_fields = false});
return layout;
@@ -108,11 +109,11 @@ std::vector<std::byte> GameArchives::read(const fs::path& root, const fb::CasIde
const auto path = root / "Win32" / fs::path(directory(location.installChunk)) /
fs::path(archive_file(location.archive));
std::ifstream input(path, std::ios::binary);
if (!input) throw std::runtime_error("Cannot open " + path.string());
if (!input) throw std::runtime_error("Cannot open " + path_utf8(path));
input.seekg(static_cast<std::streamoff>(offset));
std::vector<std::byte> bytes(size);
if (size && !input.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(size)))
throw std::runtime_error("Cannot read " + path.string());
throw std::runtime_error("Cannot read " + path_utf8(path));
return bytes;
}
+3 -2
View File
@@ -1,5 +1,6 @@
#include "game_bundles.h"
#include "Engine/Resource/cas_codec.h"
#include "Engine/Core/Platform/path_text.h"
#include <fstream>
#include <stdexcept>
@@ -50,10 +51,10 @@ GameData::GameData(fs::path gameRoot)
fb::TocDocument GameData::read_toc(std::string_view relative) const {
const auto path = gameRoot_ / "Data" / fs::path(relative);
std::ifstream input(path, std::ios::binary);
if (!input) throw std::runtime_error("Cannot open " + path.string());
if (!input) throw std::runtime_error("Cannot open " + path_utf8(path));
std::vector<std::byte> bytes(static_cast<std::size_t>(fs::file_size(path)));
if (!input.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(bytes.size())))
throw std::runtime_error("Cannot read " + path.string());
throw std::runtime_error("Cannot read " + path_utf8(path));
return fb::read_toc(bytes);
}
+11 -10
View File
@@ -5,6 +5,7 @@
#include "Engine/Game/Build/supported_build.h"
#include "Engine/Game/Build/addresses.h"
#include "Engine/Game/Build/20260929/initfs.h"
#include "Engine/Core/Platform/path_text.h"
#include <Windows.h>
#include <bcrypt.h>
@@ -37,13 +38,13 @@ std::string lower(std::string_view text) {
}
Bytes read_file(const fs::path& path, std::size_t maximum) {
std::ifstream input(path, std::ios::binary | std::ios::ate);
if (!input) fail("Cannot open " + path.string());
if (!input) fail("Cannot open " + path_utf8(path));
const auto length = input.tellg();
if (length < 0 || static_cast<std::uint64_t>(length) > maximum) fail("File exceeds size limit: " + path.string());
if (length < 0 || static_cast<std::uint64_t>(length) > maximum) fail("File exceeds size limit: " + path_utf8(path));
Bytes bytes(static_cast<std::size_t>(length));
input.seekg(0);
if (!bytes.empty() && !input.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(bytes.size())))
fail("Cannot read " + path.string());
fail("Cannot read " + path_utf8(path));
return bytes;
}
@@ -128,7 +129,7 @@ void apply_archive(const fs::path& path, const Secret& key, Entries& effective,
auto input = read_file(path, maximum_archive);
if (input.size() <= native_db::envelope_size ||
std::memcmp(input.data(), native_db::magic, sizeof(native_db::magic)))
fail("Unrecognized native envelope in " + path.string());
fail("Unrecognized native envelope in " + path_utf8(path));
const auto body = std::span<const unsigned char>(input).subspan(native_db::envelope_size);
std::size_t consumed{};
auto root = native_db::read(body, "InitFS", &consumed);
@@ -160,12 +161,12 @@ void apply_archive(const fs::path& path, const Secret& key, Entries& effective,
if (const auto* file_system = file->field("fs"); file_system &&
(file_system->type != 7 || (!file_system->text.empty() && file_system->text != "/data")))
fail("Unsupported InitFS filesystem destination for " + name->text);
Entry entry{name->text, path.string(), Bytes(payload->bytes.begin(), payload->bytes.end())};
Entry entry{name->text, path_utf8(path), Bytes(payload->bytes.begin(), payload->bytes.end())};
if (!entries.emplace(lower(name->text), std::move(entry)).second) fail("Duplicate InitFS virtual path");
}
if (!inherit) effective.clear();
for (auto& [name, entry] : entries) effective.insert_or_assign(name, std::move(entry));
sources.push_back(Json{{"path", path.string()}, {"sha256", launcher::sha256_file(path)}, {"inherit", inherit}});
sources.push_back(Json{{"path", path_utf8(path)}, {"sha256", launcher::sha256_file(path)}, {"inherit", inherit}});
}
bool within(const fs::path& root, const fs::path& path) {
@@ -178,7 +179,7 @@ bool within(const fs::path& root, const fs::path& path) {
bool write_atomic(const fs::path& root, const fs::path& path, std::span<const unsigned char> bytes, bool replace) {
if (!within(root, fs::weakly_canonical(path))) fail("Output path escapes the export directory");
if (!replace && fs::exists(path)) {
if (!fs::is_regular_file(path)) fail("Output path is not a regular file: " + path.string());
if (!fs::is_regular_file(path)) fail("Output path is not a regular file: " + path_utf8(path));
return false;
}
fs::create_directories(path.parent_path());
@@ -186,17 +187,17 @@ bool write_atomic(const fs::path& root, const fs::path& path, std::span<const un
const auto temporary = fs::path(path.wstring() + L".reskate-" + std::to_wstring(GetCurrentProcessId()) +
L"-" + std::to_wstring(GetTickCount64()) + L"-" + std::to_wstring(sequence.fetch_add(1)) + L".tmp");
const auto file = CreateFileW(temporary.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) fail("Cannot create output file: " + path.string());
if (file == INVALID_HANDLE_VALUE) fail("Cannot create output file: " + path_utf8(path));
DWORD written{};
const bool ready = WriteFile(file, bytes.data(), static_cast<DWORD>(bytes.size()), &written, nullptr) &&
written == bytes.size() && FlushFileBuffers(file);
CloseHandle(file);
if (!ready) { DeleteFileW(temporary.c_str()); fail("Cannot write output file: " + path.string()); }
if (!ready) { DeleteFileW(temporary.c_str()); fail("Cannot write output file: " + path_utf8(path)); }
if (MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_WRITE_THROUGH | (replace ? MOVEFILE_REPLACE_EXISTING : 0))) return true;
const auto error = GetLastError();
DeleteFileW(temporary.c_str());
if (!replace && (error == ERROR_ALREADY_EXISTS || error == ERROR_FILE_EXISTS) && fs::is_regular_file(path)) return false;
fail("Cannot publish output file: " + path.string());
fail("Cannot publish output file: " + path_utf8(path));
}
Json read_preferences(const fs::path& root) {
const auto path = root / L"ReSkate.settings.json";
+2 -1
View File
@@ -1,5 +1,6 @@
#include "item_thumbnails.h"
#include "game_bundles.h"
#include "Engine/Core/Platform/path_text.h"
#include <algorithm>
#include <stdexcept>
@@ -32,7 +33,7 @@ ThumbnailRead read_build_kit_thumbnails(const std::filesystem::path& gameRoot, s
bundle = data.read_bundle(toc, bundle_name);
} catch (const std::exception& failure) {
if (problems.size() < 600)
problems += "; " + it->path().filename().string() + ": " + failure.what();
problems += "; " + path_utf8(it->path().filename()) + ": " + failure.what();
}
}
if (!bundle) throw std::runtime_error("The game's thumbnail bundle was not found" + problems);
+6 -5
View File
@@ -2,6 +2,7 @@
#include "Engine/Game/Build/supported_build.h"
#include "Engine/Core/Json/json.h"
#include "Engine/Core/Platform/path_text.h"
#include <Windows.h>
@@ -35,15 +36,15 @@ std::string lower(std::string_view text) {
std::vector<unsigned char> read_file(const fs::path& path, std::size_t maximum) {
std::ifstream input(path, std::ios::binary | std::ios::ate);
if (!input) throw std::runtime_error("Cannot open " + path.string());
if (!input) throw std::runtime_error("Cannot open " + path_utf8(path));
const auto length = input.tellg();
if (length < 0 || static_cast<std::uint64_t>(length) > maximum)
throw std::runtime_error("File exceeds size limit: " + path.string());
throw std::runtime_error("File exceeds size limit: " + path_utf8(path));
std::vector<unsigned char> bytes(static_cast<std::size_t>(length));
input.seekg(0);
if (!bytes.empty() && !input.read(reinterpret_cast<char*>(bytes.data()),
static_cast<std::streamsize>(bytes.size())))
throw std::runtime_error("Cannot read " + path.string());
throw std::runtime_error("Cannot read " + path_utf8(path));
return bytes;
}
@@ -420,12 +421,12 @@ void save_mod_order(const std::filesystem::path& mods_root, const std::vector<Mo
{
std::ofstream output(temporary, std::ios::binary | std::ios::trunc);
if (!output || !output.write(text.data(), static_cast<std::streamsize>(text.size())) || !output.flush())
throw std::runtime_error("Cannot write " + temporary.string());
throw std::runtime_error("Cannot write " + path_utf8(temporary));
}
if (!MoveFileExW(temporary.c_str(), target.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
std::error_code ignored;
fs::remove(temporary, ignored);
throw std::runtime_error("Cannot replace " + target.string() + " (Windows error " +
throw std::runtime_error("Cannot replace " + path_utf8(target) + " (Windows error " +
std::to_string(GetLastError()) + ")");
}
}
+22 -14
View File
@@ -6,6 +6,7 @@
#include <algorithm>
#include <array>
#include <cstring>
#include <limits>
#include <map>
#include <optional>
#include <set>
@@ -98,10 +99,11 @@ MergeReport merge_mods(const Catalog& catalog, const MergeObserver& observe, con
// What the layout declares in each package directory. A free index is
// judged against its own directory rather than every directory at once
// -- judged against the union, three mods exhausted the range -- and it
// stays at or below the highest index that directory already declares:
// the engine sizes each install chunk's archive table from that range,
// and an index past it brings the game down during startup.
// (judged against the union, three mods used up the gaps), lowest
// first, and carries on past the highest index the game declares: the
// engine keeps archives in a table keyed by layer, install chunk and a
// 16-bit index, with no range to stay inside. What it cannot survive is
// a reference to an archive the layout never declared.
std::map<std::string, std::set<std::uint16_t>> declaredIn;
for (const auto* field : {"layeredInstallChunkFiles", "unlayeredInstallChunkFiles"}) {
const auto* node = layout.field(field);
@@ -110,16 +112,14 @@ MergeReport merge_mods(const Catalog& catalog, const MergeObserver& observe, con
if (const auto* directory = store.find_directory(id)) declaredIn[*directory].insert(archive);
});
}
const auto ceiling = [&](const std::string& directory) -> std::uint16_t {
const auto found = declaredIn.find(directory);
return found == declaredIn.end() || found->second.empty() ? 0 : *found->second.rbegin();
};
std::map<std::string, std::set<std::uint16_t>> claimedArchives;
const auto claim = [&](const std::string& directory) -> std::optional<std::uint16_t> {
const auto& declared = declaredIn[directory];
auto& claimed = claimedArchives[directory];
for (std::uint16_t candidate = 1; candidate <= ceiling(directory); ++candidate)
if (!declared.contains(candidate) && claimed.insert(candidate).second) return candidate;
for (std::uint32_t candidate = 1; candidate <= std::numeric_limits<std::uint16_t>::max(); ++candidate) {
const auto index = static_cast<std::uint16_t>(candidate);
if (!declared.contains(index) && claimed.insert(index).second) return index;
}
return std::nullopt;
};
@@ -172,6 +172,14 @@ MergeReport merge_mods(const Catalog& catalog, const MergeObserver& observe, con
const auto target = output / path.parent_path() / archive_file(manifestArchive);
if (!fs::exists(target, error))
throw std::runtime_error(mod->name + " needs a restart: the patch has no archive 1 in " + directory);
// Checked before anything is appended: past 4 GB the mod's
// payloads could not be addressed, and the next launch
// gives it an archive of its own anyway.
const auto held = fs::file_size(target, error);
const auto added = error ? std::uintmax_t{} : fs::file_size(mod->directory / path, error);
if (error || held + added > std::numeric_limits<std::uint32_t>::max())
throw std::runtime_error(mod->name + " needs a restart: the patch's archive 1 in " + directory +
" has no room left for it while the game runs");
ArchivePlacement::Spot spot{manifestArchive, append_file(mod->directory / path, target)};
placements[mod].at.emplace(std::pair{directory, *number}, spot);
report.notes.push_back(mod->name + ": " + directory + "/cas_" + std::to_string(*number) +
@@ -194,12 +202,12 @@ MergeReport merge_mods(const Catalog& catalog, const MergeObserver& observe, con
auto directory = lower(path.parent_path().generic_string());
if (directory.starts_with("win32/")) directory.erase(0, 6);
// The first mod to ship an index keeps it. A later one takes a
// free index of its own where the base leaves one, so the file
// is hard linked rather than copied; failing that it is
// free index of its own, so the file is hard linked rather
// than copied and no two mods share an archive's 4 GB of
// addressable bytes; only with every index taken is it
// concatenated onto the archive it collides with.
auto& claimed = claimedArchives[directory];
ArchivePlacement::Spot spot{number, 0};
const auto keepable = number <= ceiling(directory) || declaredIn[directory].contains(number);
// Archive 1 stays the patch's own file, never a link to a mod's:
// a live apply appends manifests and new mods to it while the
// game has it open, and a linked file cannot be swapped for a
@@ -212,7 +220,7 @@ MergeReport merge_mods(const Catalog& catalog, const MergeObserver& observe, con
} else {
spot.offset = append_file(mod->directory / path, output / path);
}
} else if (keepable && claimed.insert(number).second) {
} else if (claimed.insert(number).second) {
link_or_copy(mod->directory / path, output / path);
} else if (const auto free = claim(directory); free) {
spot.archive = *free;
+7 -1
View File
@@ -1,4 +1,5 @@
#include "mod_merge_internal.h"
#include "Engine/Core/Platform/path_text.h"
#include <fstream>
#include <limits>
@@ -21,10 +22,15 @@ fb::BundleFileInfo CasStore::write(std::uint32_t installChunk, std::uint16_t arc
const auto existing = fs::file_size(path, error);
if (!error) offset = existing;
}
// A placement holds a 32-bit offset; one past it would wrap round and
// point the game at some other payload.
if (offset + encoded.size() > std::numeric_limits<std::uint32_t>::max())
throw std::runtime_error("The merged patch's own archive " + path_utf8(path) +
" is full (4 GB); restart Skate so it is built again");
std::ofstream out(path, std::ios::binary | std::ios::app);
if (!out || !out.write(reinterpret_cast<const char*>(encoded.data()),
static_cast<std::streamsize>(encoded.size())))
throw std::runtime_error("Cannot append to " + path.string());
throw std::runtime_error("Cannot append to " + path_utf8(path));
fb::BundleFileInfo info{{true, installChunk, archive}, static_cast<std::uint32_t>(offset),
static_cast<std::uint32_t>(encoded.size())};
offset += encoded.size();
+2 -1
View File
@@ -5,6 +5,7 @@
#include "Engine/Resource/ebx_merge.h"
#include "Engine/Resource/ebx_writer.h"
#include "Engine/Resource/shader_lookup.h"
#include "Engine/Core/Platform/path_text.h"
#include <algorithm>
#include <map>
@@ -153,7 +154,7 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot,
auto region = fb::read_bundle_region(bundle.region);
const auto provider = providers.find(key);
const auto shared = provider != providers.end() && provider->second > 1;
const auto modName = isBase ? std::string("base") : root.filename().string();
const auto modName = isBase ? std::string("base") : path_utf8(root.filename());
// The bundle holding the live material grid is rebuilt to carry the
// combined one, and a mod whose surfaces moved has its collision
// renumbered; both need the asset list even where one mod ships it.
+13 -12
View File
@@ -1,5 +1,6 @@
#include "mod_merge_internal.h"
#include "Engine/Core/Json/json.h"
#include "Engine/Core/Platform/path_text.h"
#include <Windows.h>
#include <bcrypt.h>
@@ -21,15 +22,15 @@ std::string lower(std::string_view text) {
std::vector<std::byte> read_file(const fs::path& path) {
std::ifstream input(path, std::ios::binary | std::ios::ate);
if (!input) throw std::runtime_error("Cannot open " + path.string());
if (!input) throw std::runtime_error("Cannot open " + path_utf8(path));
const auto length = input.tellg();
if (length < 0 || static_cast<std::uint64_t>(length) > maximumTocBytes)
throw std::runtime_error("File exceeds the TOC size limit: " + path.string());
throw std::runtime_error("File exceeds the TOC size limit: " + path_utf8(path));
std::vector<std::byte> bytes(static_cast<std::size_t>(length));
input.seekg(0);
if (!bytes.empty() &&
!input.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(bytes.size())))
throw std::runtime_error("Cannot read " + path.string());
throw std::runtime_error("Cannot read " + path_utf8(path));
return bytes;
}
@@ -40,12 +41,12 @@ void write_file(const fs::path& path, std::span<const std::byte> bytes) {
std::ofstream output(temporary, std::ios::binary | std::ios::trunc);
if (!output || !output.write(reinterpret_cast<const char*>(bytes.data()),
static_cast<std::streamsize>(bytes.size())))
throw std::runtime_error("Cannot write " + temporary.string());
throw std::runtime_error("Cannot write " + path_utf8(temporary));
}
if (!MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
const auto error = GetLastError();
DeleteFileW(temporary.c_str());
throw std::runtime_error("Cannot publish " + path.string() +
throw std::runtime_error("Cannot publish " + path_utf8(path) +
" (Windows error " + std::to_string(error) + ")");
}
}
@@ -64,12 +65,12 @@ void unshare(const fs::path& path) {
const auto temporary = fs::path(path.wstring() + L".unshared");
std::error_code error;
fs::copy_file(path, temporary, fs::copy_options::overwrite_existing, error);
if (error) throw std::runtime_error("Cannot give " + path.string() + " a private copy: " +
if (error) throw std::runtime_error("Cannot give " + path_utf8(path) + " a private copy: " +
error.message());
if (!MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING)) {
const auto failure = GetLastError();
DeleteFileW(temporary.c_str());
throw std::runtime_error("Cannot replace the shared " + path.string() +
throw std::runtime_error("Cannot replace the shared " + path_utf8(path) +
" (Windows error " + std::to_string(failure) + ")");
}
}
@@ -78,22 +79,22 @@ std::uint64_t append_file(const fs::path& from, const fs::path& to) {
fs::create_directories(to.parent_path());
unshare(to);
std::ifstream input(from, std::ios::binary);
if (!input) throw std::runtime_error("Cannot read " + from.string());
if (!input) throw std::runtime_error("Cannot read " + path_utf8(from));
// tellp() on a stream opened for append reads 0 until the first write, so
// the block's start comes from the file itself.
std::error_code error;
const auto existing = fs::file_size(to, error);
const auto start = error ? std::uint64_t{} : existing;
std::ofstream output(to, std::ios::binary | std::ios::app);
if (!output) throw std::runtime_error("Cannot append to " + to.string());
if (!output) throw std::runtime_error("Cannot append to " + path_utf8(to));
// A megabyte of stack is a stack overflow; the copy buffer lives on the heap.
std::vector<char> buffer(1 << 20);
while (input) {
input.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
if (input.gcount() && !output.write(buffer.data(), input.gcount()))
throw std::runtime_error("Cannot append to " + to.string());
throw std::runtime_error("Cannot append to " + path_utf8(to));
}
if (!output) throw std::runtime_error("Cannot append to " + to.string());
if (!output) throw std::runtime_error("Cannot append to " + path_utf8(to));
return start;
}
@@ -104,7 +105,7 @@ void link_or_copy(const fs::path& from, const fs::path& to) {
fs::remove(to, error);
if (CreateHardLinkW(to.c_str(), from.c_str(), nullptr)) return;
fs::copy_file(from, to, fs::copy_options::overwrite_existing, error);
if (error) throw std::runtime_error("Cannot place " + to.string() + ": " + error.message());
if (error) throw std::runtime_error("Cannot place " + path_utf8(to) + ": " + error.message());
}
RelativeFiles scan(const fs::path& directory) {
+3 -2
View File
@@ -2,6 +2,7 @@
#include "Engine/Resource/cas_codec.h"
#include "Engine/Resource/ebx_document.h"
#include "Engine/Resource/ebx_writer.h"
#include "Engine/Core/Platform/path_text.h"
#include <optional>
#include <stdexcept>
@@ -128,7 +129,7 @@ GridPlan plan_material_grid(const std::vector<const Mod*>& mods,
// Lowest priority first, the order the merge layers mods in.
for (auto at = mods.rbegin(); at != mods.rend(); ++at) {
const auto* mod = *at;
const auto modName = mod->directory.filename().string();
const auto modName = path_utf8(mod->directory.filename());
for (const auto& relative : modFiles.at(mod).tocs) {
try {
const auto toc = fb::read_toc(read_file(mod->directory / fs::path(relative)));
@@ -190,7 +191,7 @@ GridPlan plan_material_grid(const std::vector<const Mod*>& mods,
grid::Combiner combiner(*base);
std::size_t combined{};
for (auto& contribution : contributions) {
const auto modName = contribution.mod->directory.filename().string();
const auto modName = path_utf8(contribution.mod->directory.filename());
const auto before = combiner.slots();
grid::SlotMap slots;
try {
+9 -7
View File
@@ -39,14 +39,16 @@ struct RelativeFiles {
RelativeFiles scan(const fs::path& directory);
// Every mod's archives are concatenated into one file per index, so a patch
// reference keeps the archive number the mod was built for and moves only by
// the byte offset that mod's block landed at. Re-indexing was the alternative,
// and the engine reads a mod's payloads from no index but its own.
// Every mod is built against archive 1, so the merge moves each mod's archives
// to indices of their own and rewrites the mod's patch references to match. A
// placement addresses its payload with a 32-bit offset, so an archive holds at
// most 4 GB: one index per mod archive is what keeps the merged patch from
// having a size limit of its own.
struct ArchivePlacement {
// Where a mod's archive ended up: a free index of its own when one is
// available, which costs nothing to place, or a byte offset inside an
// archive it now shares with a higher-priority mod.
// Where a mod's archive ended up: an index of its own, which costs nothing
// to place, or a byte offset inside an archive it shares. Sharing is for a
// mod added while the game runs, which has no declared index to take, and
// for a package directory with no index left.
struct Spot {
std::uint16_t archive{};
std::uint64_t offset{};
+2 -1
View File
@@ -1,5 +1,6 @@
#include "mod_merge_internal.h"
#include "Engine/Resource/ebx_document.h"
#include "Engine/Core/Platform/path_text.h"
#include <cstring>
#include <stdexcept>
@@ -33,7 +34,7 @@ std::vector<MergeReport::LoadScreen> read_load_screens(const std::vector<const M
const fs::path& gameRoot, MergeReport& report) {
std::vector<MergeReport::LoadScreen> result;
for (const auto* mod : mods) {
const auto modName = mod->directory.filename().string();
const auto modName = path_utf8(mod->directory.filename());
for (const auto& relative : modFiles.at(mod).tocs) {
if (lower(relative) != configurationToc) continue;
try {
+4 -3
View File
@@ -1,4 +1,5 @@
#include "mod_merge_internal.h"
#include "Engine/Core/Platform/path_text.h"
#include <Windows.h>
#include <algorithm>
@@ -69,7 +70,7 @@ std::string merge_fingerprint(const Catalog& catalog, const std::vector<const Mo
if (it->is_regular_file(error) && !error) files.push_back(it->path());
error.clear();
}
if (error) throw std::runtime_error("Cannot list " + mod->directory.string());
if (error) throw std::runtime_error("Cannot list " + path_utf8(mod->directory));
std::ranges::sort(files);
for (const auto& file : files)
describe(utf8(fs::relative(file, mod->directory, error)), file);
@@ -151,10 +152,10 @@ void write_stamp(const fs::path& output, const std::string& fingerprint, const M
for (fs::recursive_directory_iterator it(output, error), end; it != end && !error; it.increment(error)) {
if (!it->is_regular_file(error) || error) { error.clear(); continue; }
const auto size = fs::file_size(it->path(), error);
if (error) throw std::runtime_error("Cannot size " + it->path().string());
if (error) throw std::runtime_error("Cannot size " + path_utf8(it->path()));
text += "file " + std::to_string(size) + ' ' + utf8(fs::relative(it->path(), output, error)) + '\n';
}
if (error) throw std::runtime_error("Cannot list " + output.string());
if (error) throw std::runtime_error("Cannot list " + path_utf8(output));
for (auto note : report.notes) {
std::ranges::replace(note, '\n', ' ');
std::ranges::replace(note, '\r', ' ');
+13 -2
View File
@@ -3,6 +3,7 @@
#include "game_bundles.h"
#include "Engine/Core/Json/json.h"
#include "Engine/Resource/ebx_document.h"
#include "Engine/Core/Platform/path_text.h"
#include <algorithm>
#include <array>
#include <fstream>
@@ -77,6 +78,15 @@ std::vector<Reference> read_references(const fs::path& gameRoot, const char* toc
return result;
}
// Layers there is no sense in switching off. They stay in the node tree, so
// their children are still found and still switchable, but they get no row of
// their own: CoreGameAssets holds what every map is built out of, and a player
// who turned it off lost the world with no clue which row had done it. A
// choice already stored for one is keyed by row, so it simply stops applying.
bool essential_layer(std::string_view bundle) {
return contains(leaf_of(lower(bundle)), "coregameassets");
}
std::string category(std::string_view bundle) {
const auto path = lower(bundle);
const auto leaf = leaf_of(path);
@@ -142,6 +152,7 @@ void add_map(WorldLayerCatalog& catalog, WorldMap map, const std::vector<Referen
const std::string prefix(world_map_key(map));
for (auto slot = first; slot < catalog.nodes.size(); ++slot) {
const auto& node = catalog.nodes[slot];
if (essential_layer(node.bundle)) continue;
const auto leaf = leaf_of(node.bundle);
const auto key = prefix + "_" + (leaves[slug(leaf)] > 1 ? slug(node.bundle) : slug(leaf));
auto label = leaf;
@@ -153,7 +164,7 @@ void add_map(WorldLayerCatalog& catalog, WorldMap map, const std::vector<Referen
std::string stamp(const fs::path& gameRoot) {
std::ostringstream out;
out << "1";
out << "2"; // 2: no row for the layers essential_layer keeps out
for (const auto& source : maps) {
const auto path = gameRoot / "Data" / fs::path(source.toc);
std::error_code error;
@@ -252,7 +263,7 @@ WorldLayerCatalog load_or_scan(const fs::path& gameRoot, const fs::path& file) {
WorldLayerCatalog read(const fs::path& file) {
std::ifstream input(file, std::ios::binary);
if (!input) throw std::runtime_error("Cannot open " + file.string());
if (!input) throw std::runtime_error("Cannot open " + path_utf8(file));
const std::string text{std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
return from_json(Json::parse(text.begin(), text.end()));
}
@@ -20,7 +20,8 @@ bool cosmetic_diagnostic(const char* operation, const char* reason, std::string_
// ..._rejected are), so a player's ordinary log says why; the rest are routine.
const std::string_view why(reason);
const bool routine = why == "callback_entered" || why == "unchanged" || why == "no_saved_loadout" ||
why == "card_only_preset_recovered" || why == "card_is_not_skater_preset";
why == "card_only_preset_recovered" || why == "card_is_not_skater_preset" ||
why == "retried_after_catalog_ready";
const dingosdk::Json event{{"event", routine ? "local_cosmetic_loadout_diagnostic" : "local_cosmetic_loadout_rejected"},
{"operation", operation}, {"reason", reason}, {"preset", id}};
dingosdk::logging::event(dingosdk::logging::Channel::customization, event.dump().c_str());
@@ -297,14 +298,11 @@ bool recover_card_only_preset(std::string_view id, const profile::CosmeticLoadou
return true;
}
bool load_cosmetic_hook(const void* wrapper, void* destination) {
// A load, taking the preset id instead of the game's wrapper so that
// retry_pending_cosmetic_loads can run it again. The caller holds native_mutex.
bool apply_saved_loadout(const std::string& id, void* destination) {
auto& s = local_runtime();
if (!s.active.load(std::memory_order_acquire)) return s.load_cosmetic(wrapper, destination);
PreserveError preserve;
std::lock_guard lock(s.native_mutex);
std::string id;
try {
if (!identifier(wrapper, id)) return cosmetic_diagnostic("load", "invalid_preset_id");
const auto saved = s.store->cosmetic_loadout(id);
if (!saved) {
profile::CosmeticLoadout starter; std::vector<CosmeticNativeRecipe> native;
@@ -324,7 +322,13 @@ bool load_cosmetic_hook(const void* wrapper, void* destination) {
profile::CosmeticLoadout defaults; std::vector<CosmeticNativeRecipe> native;
if (!read_cosmetic_recipes(destination, defaults, native)) return cosmetic_diagnostic("load", "invalid_native_recipe", id);
if (recover_card_only_preset(id, *saved, defaults)) return false;
if (!refresh_cosmetic_catalog()) return cosmetic_diagnostic("load", "catalog_not_ready", id);
if (!refresh_cosmetic_catalog()) {
// The cosmetics manager is not ready this early in a level load.
// The preset stays blocked, so nothing is written over the outfit
// on disk, and the pump applies this record once it is ready.
c.pending_loads[id] = destination;
return cosmetic_diagnostic("load", "catalog_not_ready", id);
}
if (saved->recipes.size() != defaults.recipes.size()) return cosmetic_diagnostic("load", "recipe_count_mismatch", id);
const auto snapshot_shared = s.store->shared_snapshot();
const auto& snapshot = *snapshot_shared;
@@ -378,6 +382,7 @@ bool load_cosmetic_hook(const void* wrapper, void* destination) {
}
s.copy_cosmetic(destination, native.data(), native.data() + native.size());
c.blocked_loadouts.erase(id);
c.pending_loads.erase(id);
if (held.empty()) c.held_slots.erase(id);
else c.held_slots[id] = std::move(held);
dingosdk::logging::event(dingosdk::logging::Channel::customization, "{\"event\":\"local_cosmetic_loadout_loaded\"}");
@@ -389,6 +394,45 @@ bool load_cosmetic_hook(const void* wrapper, void* destination) {
}
}
bool load_cosmetic_hook(const void* wrapper, void* destination) {
auto& s = local_runtime();
if (!s.active.load(std::memory_order_acquire)) return s.load_cosmetic(wrapper, destination);
PreserveError preserve;
std::lock_guard lock(s.native_mutex);
std::string id;
try {
if (!identifier(wrapper, id)) return cosmetic_diagnostic("load", "invalid_preset_id");
} catch (...) {
return cosmetic_diagnostic("load", "invalid_preset_id");
}
return apply_saved_loadout(id, destination);
}
void retry_pending_cosmetic_loads() {
auto& c = cosmetic_runtime();
if (c.pending_loads.empty()) return;
auto& s = local_runtime();
// The records belong to the loadout manager's array, which can be replaced
// between the refused load and now. Check each pointer against the live
// array exactly as a save does, and drop one that no longer belongs rather
// than writing through it.
std::uintptr_t manager{}, begin{}, end{};
if (!read(s.base + addr::engine::loadout_manager, manager) || !manager ||
!read(manager + 0x18, begin) || !read(manager + 0x20, end) || begin > end ||
(end - begin) % 0x48 || (end - begin) / 0x48 > 10) return;
auto pending = std::move(c.pending_loads);
c.pending_loads.clear();
for (const auto& [id, destination] : pending) {
const auto p = reinterpret_cast<std::uintptr_t>(destination);
if (!destination || p < begin || p >= end || (p - begin) % 0x48) {
cosmetic_diagnostic("load", "retry_record_moved", id);
continue;
}
if (apply_saved_loadout(id, destination))
cosmetic_diagnostic("load", "retried_after_catalog_ready", id);
}
}
void save_cosmetic_hook(std::uintptr_t manager, void* record, const char* raw_id, const char* reason, bool changed_only) {
auto& s = local_runtime();
if (!s.active.load(std::memory_order_acquire)) { s.save_cosmetic(manager, record, raw_id, reason, changed_only); return; }
@@ -16,6 +16,12 @@ struct CosmeticRuntime {
std::uint32_t item_count{};
std::map<std::string, CosmeticItemInfo, std::less<>> items;
std::set<std::string> blocked_loadouts;
// Presets whose load was refused only because the cosmetics catalog was not
// ready yet, with the record the game asked us to fill. The game asks once
// per level load and does not ask again, so the pump retries these once the
// catalog is ready. Until one succeeds the preset stays in
// `blocked_loadouts`, so a save cannot write over the outfit on disk.
std::map<std::string, void*, std::less<>> pending_loads;
// Saved outfit items that were not installed when their preset loaded (a
// disabled or removed costume mod). The slot shows its default this session;
// saves keep the original item unless the player changes that slot.
@@ -106,6 +112,10 @@ bool recover_card_only_preset(std::string_view id, const profile::CosmeticLoadou
const profile::CosmeticLoadout& defaults);
bool load_cosmetic_hook(const void* wrapper, void* destination);
// Applies the outfits whose load was refused for a catalog that was not ready.
// Call from the customization pump, holding native_mutex, once
// refresh_cosmetic_catalog() is true.
void retry_pending_cosmetic_loads();
void save_cosmetic_hook(std::uintptr_t manager, void* record, const char* raw_id, const char* reason, bool changed_only);
}
@@ -303,6 +303,10 @@ void update_local_customization() noexcept {
if (!c.catalog_failed && refresh_cosmetic_catalog()) {
publish_cosmetic_catalog();
publish_cosmetic_inventory();
// Outfits the game asked for before the catalog was ready: it does
// not ask again, so they are applied here instead of the player
// finding a standard skater that will not save over itself.
retry_pending_cosmetic_loads();
}
} catch (...) {
cosmetic_runtime().catalog_failed = true;
+19 -1
View File
@@ -3,6 +3,8 @@
#include "gui_internal.h"
#include "gui_renderer.h"
#include "Engine/Core/Log/logging.h"
#include <dwmapi.h>
#include <shellapi.h>
@@ -221,6 +223,12 @@ Fonts load_fonts() {
fonts.action = font(L"FONT_HEADING", L"seguibl.ttf", 50);
fonts.title = font(L"FONT_BRUSH", L"seguibl.ttf", 86);
io.FontDefault = fonts.body;
// Built here rather than on the first frame, so the log says whether it
// worked. A window with no atlas draws nothing but its clear colour.
const bool built = io.Fonts->Build();
logging::log(built ? logging::Level::info : logging::Level::error, logging::Channel::launcher,
"Launcher fonts: atlas {}x{} {}.", io.Fonts->TexWidth, io.Fonts->TexHeight,
built ? "built" : "COULD NOT BE BUILT, so the window will be empty");
return fonts;
}
@@ -313,6 +321,9 @@ int run(const launcher_app::Session& session, const std::vector<std::wstring>& a
const int width = static_cast<int>(S(design_width));
const int height = static_cast<int>(S(design_height));
logging::log(logging::Level::info, logging::Channel::launcher,
"Launcher window: {}x{} at scale {:.2f} (system DPI {}), work area {}x{}.", width, height, g_scale,
GetDpiForSystem(), work.right - work.left, work.bottom - work.top);
const HWND window = CreateWindowExW(WS_EX_APPWINDOW, window_class.lpszClassName, L"ReSkate",
WS_POPUP | WS_MINIMIZEBOX | WS_SYSMENU, work.left + (work.right - work.left - width) / 2,
work.top + (work.bottom - work.top - height) / 2, width, height, nullptr, nullptr, instance, nullptr);
@@ -334,7 +345,9 @@ int run(const launcher_app::Session& session, const std::vector<std::wstring>& a
ImGui_ImplWin32_Shutdown();
ImGui::DestroyContext();
DestroyWindow(window);
throw std::runtime_error("Cannot start Direct3D 12 for the launcher window");
throw std::runtime_error("This PC's graphics driver cannot draw the launcher's window. Update your "
"graphics driver and try again. If that does not help, the graphics card is below what ReSkate and "
"skate. need. The log names the card it tried to use.");
}
g_renderer = &renderer;
{
@@ -376,6 +389,7 @@ int run(const launcher_app::Session& session, const std::vector<std::wstring>& a
HANDLE game{};
DWORD game_id{};
bool hidden{};
bool drew{};
while (running) {
MSG message;
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
@@ -423,6 +437,10 @@ int run(const launcher_app::Session& session, const std::vector<std::wstring>& a
frame(launcher, fonts, window, ui, mods_panel);
ImGui::Render();
renderer.render();
if (!drew) {
drew = true;
logging::write(logging::Level::info, logging::Channel::launcher, "Launcher drew its first frame.");
}
}
ShowWindow(window, SW_HIDE);
if (game) CloseHandle(game);
+64 -4
View File
@@ -1,5 +1,9 @@
#include "gui_renderer.h"
#include "Engine/Core/Log/logging.h"
#include <dxgi1_6.h>
#include <backends/imgui_impl_dx12.h>
#include <wincodec.h>
@@ -13,11 +17,52 @@ namespace dingosdk::launcher_gui::detail {
bool Renderer::init(HWND window) {
UINT factory_flags = 0;
ComPtr<IDXGIFactory4> factory;
if (FAILED(CreateDXGIFactory2(factory_flags, IID_PPV_ARGS(&factory)))) return false;
if (FAILED(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device_)))) {
if (FAILED(CreateDXGIFactory2(factory_flags, IID_PPV_ARGS(&factory)))) {
logging::write(logging::Level::error, logging::Channel::launcher, "Launcher renderer: no DXGI factory.");
return false;
}
bool software = false;
// Ask for the best card rather than whichever DXGI lists first. Where
// onboard graphics sit beside a real card, the default adapter is usually
// the onboard one, and its Direct3D 12 driver can be too old to draw the
// window at all while the card that would have worked goes unused.
if (ComPtr<IDXGIFactory6> ranked; SUCCEEDED(factory.As(&ranked))) {
for (UINT index = 0; !device_; ++index) {
ComPtr<IDXGIAdapter1> candidate;
if (FAILED(ranked->EnumAdapterByGpuPreference(index, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE,
IID_PPV_ARGS(&candidate)))) break;
DXGI_ADAPTER_DESC1 description{};
// WARP is the last resort below, not a candidate here.
if (FAILED(candidate->GetDesc1(&description)) || (description.Flags & DXGI_ADAPTER_FLAG_SOFTWARE))
continue;
D3D12CreateDevice(candidate.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device_));
}
}
if (!device_ && FAILED(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device_)))) {
ComPtr<IDXGIAdapter> warp;
if (FAILED(factory->EnumWarpAdapter(IID_PPV_ARGS(&warp))) ||
FAILED(D3D12CreateDevice(warp.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device_)))) return false;
FAILED(D3D12CreateDevice(warp.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device_)))) {
logging::write(logging::Level::error, logging::Channel::launcher,
"Launcher renderer: no Direct3D 12 device, not even the software one.");
return false;
}
software = true;
}
{
// Which card drew the window, so a blank one can be told apart from a
// driver problem, a software fallback or a remote session.
DXGI_ADAPTER_DESC1 description{};
ComPtr<IDXGIAdapter1> adapter;
if (SUCCEEDED(factory->EnumAdapterByLuid(device_->GetAdapterLuid(), IID_PPV_ARGS(&adapter))) &&
SUCCEEDED(adapter->GetDesc1(&description)))
logging::write(logging::Level::info, logging::Channel::launcher,
std::wstring(L"Launcher renderer: ") + description.Description +
(software ? L" (software fallback)" : L"") + L", " +
std::to_wstring(description.DedicatedVideoMemory / (1024 * 1024)) + L" MB dedicated");
else
logging::write(logging::Level::warning, logging::Channel::launcher,
software ? "Launcher renderer: software fallback, adapter unidentified."
: "Launcher renderer: adapter unidentified.");
}
D3D12_DESCRIPTOR_HEAP_DESC rtv_desc{D3D12_DESCRIPTOR_HEAP_TYPE_RTV, frame_count, D3D12_DESCRIPTOR_HEAP_FLAG_NONE, 1};
D3D12_DESCRIPTOR_HEAP_DESC srv_desc{D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, 1 + max_textures,
@@ -67,7 +112,22 @@ bool Renderer::init(HWND window) {
info.SrvDescriptorHeap = srv_heap_.Get();
info.LegacySingleSrvCpuDescriptor = srv_heap_->GetCPUDescriptorHandleForHeapStart();
info.LegacySingleSrvGpuDescriptor = srv_heap_->GetGPUDescriptorHandleForHeapStart();
return ImGui_ImplDX12_Init(&info);
if (!ImGui_ImplDX12_Init(&info)) {
logging::write(logging::Level::error, logging::Channel::launcher,
"Launcher renderer: the Direct3D 12 backend did not start.");
return false;
}
// The backend builds its pipeline and font texture on the first frame and
// ignores whether that worked, so a driver that cannot make one draws
// nothing at all and the window shows only its clear colour -- a black
// window, with a log that says everything succeeded. Build it here, where
// the answer can be acted on.
if (!ImGui_ImplDX12_CreateDeviceObjects()) {
logging::write(logging::Level::error, logging::Channel::launcher,
"Launcher renderer: this driver cannot create the Direct3D 12 pipeline the window draws with.");
return false;
}
return true;
}
void Renderer::render() {
+25 -13
View File
@@ -423,7 +423,10 @@ RemoteModule inject_dll(HANDLE process, DWORD process_id, const fs::path& dll) {
SIZE_T written{};
if (!WriteProcessMemory(process, remote_path.get(), path.c_str(), byte_count, &written) ||
written != byte_count) win32_failure(L"WriteProcessMemory(ReSkate.dll path)");
const auto loader = dingosdk::launcher::validated_remote_load_library(process);
std::string hook_note;
const auto loader = dingosdk::launcher::validated_remote_load_library(process, &hook_note);
if (!hook_note.empty())
dingosdk::logging::write(dingosdk::logging::Level::warning, dingosdk::logging::Channel::launcher, hook_note);
std::wostringstream address;
address << L"Starting ordinary LoadLibraryW injection at 0x" << std::hex << loader;
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::launcher, address.str());
@@ -648,21 +651,30 @@ DWORD start_game(const Session& session, const launcher::LaunchOptions& options,
logging::log(logging::Level::warning, logging::Channel::world, "World layers could not be read: {}", error.what());
}
configure_environment(paths.logs);
const bool loose_files = options.loose_files && initfs::loose_files_preference(paths.directory);
set_environment(L"RESKATE_LOOSE_FILES", loose_files ? L"1" : L"0");
bool loose_files = options.loose_files && initfs::loose_files_preference(paths.directory);
if (loose_files) {
fs::create_directories(paths.directory / L"scripts" / L"Custom");
const auto exported = initfs::export_files(paths.game,
initfs::data_directory(paths.directory, options.game_arguments), paths.directory, true);
if (exported.already_exported) {
logging::write(logging::Level::info, logging::Channel::assets,
"Using existing scripts/ and config/ export; edits and removed files are preserved.");
} else {
logging::log(logging::Level::info, logging::Channel::assets,
"InitFS exported: {} Lua, {} configs; {} created, {} existing files preserved, {} skipped.",
exported.scripts, exported.configs, exported.created, exported.preserved, exported.skipped);
try {
fs::create_directories(paths.directory / L"scripts" / L"Custom");
const auto exported = initfs::export_files(paths.game,
initfs::data_directory(paths.directory, options.game_arguments), paths.directory, true);
if (exported.already_exported) {
logging::write(logging::Level::info, logging::Channel::assets,
"Using existing scripts/ and config/ export; edits and removed files are preserved.");
} else {
logging::log(logging::Level::info, logging::Channel::assets,
"InitFS exported: {} Lua, {} configs; {} created, {} existing files preserved, {} skipped.",
exported.scripts, exported.configs, exported.created, exported.preserved, exported.skipped);
}
} catch (const std::exception& failure) {
// An install under Program Files is the usual reason. Skate plays
// without loose scripts; only editing them is lost.
loose_files = false;
logging::log(logging::Level::warning, logging::Channel::assets,
"Loose files are off for this launch: scripts/ and config/ could not be exported ({}). Skate still "
"starts; editing them needs a folder ReSkate is allowed to write to.", failure.what());
}
}
set_environment(L"RESKATE_LOOSE_FILES", loose_files ? L"1" : L"0");
set_environment(L"RESKATE_LOG_CONSOLE", options.window_console ? L"1" : L"0");
set_environment(L"RESKATE_LOG_LEVEL", widen(options.log_level).c_str());
set_environment(L"RESKATE_FORCE_WINDOWED", options.force_windowed ? L"1" : L"0");
+27
View File
@@ -69,6 +69,23 @@ inline Problem explain(std::string_view raw) {
if (mentions(raw, "need different keys"))
return {"The menu and console share a key",
"Open Settings > KEYS and pick a different key for one of them."};
// ERROR_ELEVATION_REQUIRED. Windows localises the text, so match the
// number win32_failure puts in front of it.
if (mentions(raw, "failed (740)"))
return {"Skate.exe is set to always run as administrator",
"ReSkate has to start Skate itself, and Windows does not let it start a program marked to "
"need administrator. Right-click Skate.exe, pick Properties, then Compatibility, and untick "
"\"Run this program as an administrator\". Starting ReSkateLauncher.exe as "
"administrator works too."};
if (mentions(raw, "LoadLibraryW is hooked")) {
std::string advice = "Security software or an overlay is modifying Skate while it starts, and ReSkate will "
"not load itself through that. Allow ReSkateLauncher.exe and Skate.exe in it, or close "
"it, then press RETRY.";
// The launcher named what it found in Skate; that is the useful half.
if (const auto found = raw.find("Loaded into Skate"); found != std::string_view::npos)
advice += " " + std::string(raw.substr(found));
return {"Something is hooking Skate as it starts", std::move(advice)};
}
if (mentions(raw, "Skate closed while it was starting"))
return {"Skate closed while it was starting", std::string(attach_advice)};
if (mentions(raw, "timed out after"))
@@ -79,6 +96,16 @@ inline Problem explain(std::string_view raw) {
mentions(raw, "module list") || mentions(raw, "VirtualAllocEx") || mentions(raw, "NtQueryInformationProcess") ||
mentions(raw, "DingoSDKDebugInitialize") || mentions(raw, "Remote "))
return {"ReSkate could not attach to Skate", std::string(attach_advice)};
// Windows reports a refused write in the user's own language, so these
// key on the operation names and the path, which are never translated.
const bool write_failure = mentions(raw, "create_directories") || mentions(raw, "create_directory") ||
mentions(raw, "Cannot write") || mentions(raw, "Cannot create") ||
mentions(raw, "Cannot publish") || mentions(raw, "Cannot append") ||
mentions(raw, "Cannot replace");
if (write_failure)
return {"ReSkate cannot write to its own folder",
"Move the whole ReSkate folder somewhere else, such as C:\\Games\\ReSkate, and start it from "
"there. Program Files and OneDrive do not let it write."};
return {std::string(raw),
"Press RETRY. If it keeps happening, copy the details below and share them with logs\\ReSkate.log "
"on the ReSkate Discord."};
+2 -1
View File
@@ -5,6 +5,7 @@
#include "Engine/Core/Platform/launcher_support.h"
#include "launcher_update_config.h"
#include "text_encoding.h"
#include "Engine/Core/Platform/path_text.h"
#include <winhttp.h>
#include <miniz.h>
@@ -197,7 +198,7 @@ struct Zip {
// Writes one ZIP entry to `output`.
void extract_index(Zip& zip, mz_uint index, const mz_zip_archive_file_stat& entry, const fs::path& output) {
Handle file{CreateFileW(output.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
if (file.value == INVALID_HANDLE_VALUE) fail("Cannot create " + output.string());
if (file.value == INVALID_HANDLE_VALUE) fail("Cannot create " + path_utf8(output));
struct Sink { HANDLE file; std::uint64_t count{}; } sink{file.value};
const auto write = [](void* opaque, mz_uint64 offset, const void* buffer, size_t count) -> size_t {
auto& target = *static_cast<Sink*>(opaque);