mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-24 13:36:18 +02:00
A player whose game sat under a folder named U+200E (left-to-right mark, an invisible directory on their Desktop) could not launch: the status tile read "No mapping for the Unicode character exists in the target multi-byte code page". Nothing was wrong with their mods, and the merge in their log had just succeeded. fs::path::string() converts to the process's ANSI code page and throws exactly that when a character has no mapping there. initfs.cpp called it on the normal path, not in an error handler, so exporting loose files threw on every launch. Their log stops right where that export begins. Cyrillic, CJK or accented characters in an install path do the same on a code page that cannot hold them. - path_utf8() in Engine/Core/Platform/path_text.h: UTF-8 represents every path Windows allows, so it never throws. - Every path that becomes a narrow string uses it -- 35 of them across 13 files. Most are log lines and error messages, which were a second trap: reporting a problem with a path threw this instead of the problem. - The ~45 .string() calls on Json values are untouched; those are not paths. Nothing changes for an ASCII path, where both encodings agree, and a path that is not ASCII had no working behaviour to preserve. Reported with a log by a player on Discord. Moving the game to an ordinary folder works around it on an unpatched build.
86 lines
3.6 KiB
C++
86 lines
3.6 KiB
C++
#include "game_bundles.h"
|
|
#include "Engine/Resource/cas_codec.h"
|
|
#include "Engine/Core/Platform/path_text.h"
|
|
#include <fstream>
|
|
#include <stdexcept>
|
|
|
|
namespace dingosdk::vfs {
|
|
namespace fs = std::filesystem;
|
|
namespace fb = frostbite;
|
|
namespace {
|
|
std::size_t first_index(const fb::BinaryBundle& manifest, fb::AssetKind kind) {
|
|
switch (kind) {
|
|
case fb::AssetKind::ebx: return 0;
|
|
case fb::AssetKind::resource: return manifest.ebx.size();
|
|
default: return manifest.ebx.size() + manifest.resources.size();
|
|
}
|
|
}
|
|
const std::vector<fb::BundleAsset>& list(const fb::BinaryBundle& manifest, fb::AssetKind kind) {
|
|
return kind == fb::AssetKind::ebx ? manifest.ebx : kind == fb::AssetKind::resource ? manifest.resources : manifest.chunks;
|
|
}
|
|
}
|
|
|
|
const fb::BundleFileInfo* GameBundle::payload(fb::AssetKind kind, std::size_t index) const noexcept {
|
|
if (index >= list(manifest, kind).size()) return nullptr;
|
|
const auto at = first_index(manifest, kind) + index;
|
|
return at < payloads.size() ? &payloads[at] : nullptr;
|
|
}
|
|
|
|
const fb::BundleAsset* GameBundle::find(fb::AssetKind kind, std::string_view name, std::size_t* index) const noexcept {
|
|
const auto& assets = list(manifest, kind);
|
|
for (std::size_t i = 0; i < assets.size(); ++i) {
|
|
if (assets[i].name != name) continue;
|
|
if (index) *index = i;
|
|
return &assets[i];
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
const fb::BundleAsset* GameBundle::find_chunk(const fb::Guid& id, std::size_t* index) const noexcept {
|
|
for (std::size_t i = 0; i < manifest.chunks.size(); ++i) {
|
|
if (manifest.chunks[i].guid != id) continue;
|
|
if (index) *index = i;
|
|
return &manifest.chunks[i];
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
GameData::GameData(fs::path gameRoot)
|
|
: gameRoot_(std::move(gameRoot)), archives_(gameRoot_ / L"Data", read_layout(gameRoot_ / L"Data" / L"layout.toc").root) {}
|
|
|
|
fb::TocDocument GameData::read_toc(std::string_view relative) const {
|
|
const auto path = gameRoot_ / L"Data" / fs::path(relative);
|
|
std::ifstream input(path, std::ios::binary);
|
|
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_utf8(path));
|
|
return fb::read_toc(bytes);
|
|
}
|
|
|
|
std::optional<GameBundle> GameData::read_bundle(const fb::TocDocument& toc, std::string_view name) const {
|
|
for (const auto& bundle : toc.bundles) {
|
|
if (bundle.name != name) continue;
|
|
auto region = fb::read_bundle_region(bundle.region);
|
|
GameBundle result;
|
|
if (!region.inlineManifest.empty()) {
|
|
result.manifest = fb::read_binary_bundle(region.inlineManifest);
|
|
result.payloads = std::move(region.files);
|
|
} else {
|
|
// The manifest is the region's first file, in cas like any payload.
|
|
if (region.files.empty()) throw std::runtime_error("Bundle " + std::string(name) + " has no manifest");
|
|
const auto& first = region.files.front();
|
|
result.manifest = archives_.read_manifest(archives_.root(), first.location, first.offset, first.size, gameRoot_);
|
|
result.payloads.assign(region.files.begin() + 1, region.files.end());
|
|
}
|
|
return result;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::vector<std::byte> GameData::read(const fb::BundleFileInfo& payload) const {
|
|
const auto raw = archives_.read(archives_.root(), payload.location, payload.offset, payload.size);
|
|
return fb::decode_cas(raw, {gameRoot_});
|
|
}
|
|
}
|