mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-12 17:45:52 -04:00
init
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include "isa/big_endian.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace PadStatusContract {
|
||||
|
||||
inline constexpr std::size_t kGuestStatusSize = 0x0C;
|
||||
using GuestStatus = std::array<uint8_t, kGuestStatusSize>;
|
||||
|
||||
struct Fields {
|
||||
uint16_t buttons = 0;
|
||||
int8_t stickX = 0;
|
||||
int8_t stickY = 0;
|
||||
int8_t substickX = 0;
|
||||
int8_t substickY = 0;
|
||||
uint8_t triggerLeft = 0;
|
||||
uint8_t triggerRight = 0;
|
||||
uint8_t analogA = 0;
|
||||
uint8_t analogB = 0;
|
||||
int8_t error = 0;
|
||||
};
|
||||
|
||||
inline GuestStatus Encode(const Fields& fields)
|
||||
{
|
||||
GuestStatus status{};
|
||||
BigEndian::Write16(status.data(), fields.buttons);
|
||||
status[0x02] = static_cast<uint8_t>(fields.stickX);
|
||||
status[0x03] = static_cast<uint8_t>(fields.stickY);
|
||||
status[0x04] = static_cast<uint8_t>(fields.substickX);
|
||||
status[0x05] = static_cast<uint8_t>(fields.substickY);
|
||||
status[0x06] = fields.triggerLeft;
|
||||
status[0x07] = fields.triggerRight;
|
||||
status[0x08] = fields.analogA;
|
||||
status[0x09] = fields.analogB;
|
||||
status[0x0A] = static_cast<uint8_t>(fields.error);
|
||||
return status;
|
||||
}
|
||||
|
||||
} // namespace PadStatusContract
|
||||
|
||||
namespace WpadContract {
|
||||
|
||||
inline constexpr std::size_t kChannelCount = 4;
|
||||
inline constexpr int32_t kStatusDisabled = 0;
|
||||
inline constexpr int32_t kStatusReady = 3;
|
||||
inline constexpr int32_t kErrorNoController = -1;
|
||||
inline constexpr int32_t kErrorNotReady = -2;
|
||||
inline constexpr int32_t kErrorBadChannel = -6;
|
||||
inline constexpr int32_t kExtensionCore = 0;
|
||||
|
||||
class State {
|
||||
public:
|
||||
void Initialize() { m_initialized = true; }
|
||||
bool IsInitialized() const { return m_initialized; }
|
||||
int32_t GetLibraryStatus() const { return m_initialized ? kStatusReady : kStatusDisabled; }
|
||||
|
||||
int32_t GetDataFormat(uint32_t chan) const
|
||||
{
|
||||
if (chan >= kChannelCount) {
|
||||
return kErrorBadChannel;
|
||||
}
|
||||
return m_initialized ? 0 : kErrorNotReady;
|
||||
}
|
||||
|
||||
int32_t SetDataFormat(uint32_t chan, int32_t format) const
|
||||
{
|
||||
(void)format;
|
||||
if (chan >= kChannelCount) {
|
||||
return kErrorBadChannel;
|
||||
}
|
||||
return m_initialized ? kErrorNoController : kErrorNotReady;
|
||||
}
|
||||
|
||||
private:
|
||||
bool m_initialized = false;
|
||||
};
|
||||
|
||||
} // namespace WpadContract
|
||||
@@ -0,0 +1,338 @@
|
||||
#pragma once
|
||||
|
||||
#include "isa/big_endian.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace DvdFstContract {
|
||||
|
||||
struct RegisteredFile {
|
||||
std::string hostPath;
|
||||
std::string dvdPath;
|
||||
uint32_t size = 0;
|
||||
uint32_t discOffsetWords = 0;
|
||||
};
|
||||
|
||||
struct IndexedEntry {
|
||||
std::string hostPath;
|
||||
std::string dvdPath;
|
||||
uint32_t size = 0;
|
||||
uint32_t discOffsetWords = 0;
|
||||
uint32_t parentIndex = 0;
|
||||
uint32_t subtreeEnd = 0;
|
||||
bool isDirectory = false;
|
||||
};
|
||||
|
||||
struct Image {
|
||||
std::vector<IndexedEntry> entries;
|
||||
std::map<std::string, int32_t> pathToEntry;
|
||||
std::vector<uint8_t> bytes;
|
||||
};
|
||||
|
||||
struct GuestPlacement {
|
||||
uint32_t address = 0;
|
||||
uint32_t reservedArenaHi = 0;
|
||||
};
|
||||
|
||||
inline std::string CanonicalizePath(const std::string& input) {
|
||||
std::string path = input;
|
||||
std::replace(path.begin(), path.end(), '\\', '/');
|
||||
|
||||
std::vector<std::string> components;
|
||||
size_t cursor = 0;
|
||||
while (cursor < path.size()) {
|
||||
while (cursor < path.size() && path[cursor] == '/') {
|
||||
++cursor;
|
||||
}
|
||||
const size_t start = cursor;
|
||||
while (cursor < path.size() && path[cursor] != '/') {
|
||||
++cursor;
|
||||
}
|
||||
if (start == cursor) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string component = path.substr(start, cursor - start);
|
||||
if (component == ".") {
|
||||
continue;
|
||||
}
|
||||
if (component == "..") {
|
||||
if (!components.empty()) {
|
||||
components.pop_back();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
components.push_back(std::move(component));
|
||||
}
|
||||
|
||||
std::string canonical = "/";
|
||||
for (size_t i = 0; i < components.size(); ++i) {
|
||||
if (i != 0) {
|
||||
canonical.push_back('/');
|
||||
}
|
||||
canonical += components[i];
|
||||
}
|
||||
return canonical;
|
||||
}
|
||||
|
||||
inline std::string NormalizeLookupPath(const std::string& input) {
|
||||
std::string path = CanonicalizePath(input);
|
||||
std::transform(path.begin(), path.end(), path.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return path;
|
||||
}
|
||||
|
||||
namespace Detail {
|
||||
|
||||
struct TreeNode {
|
||||
std::string name;
|
||||
std::map<std::string, std::unique_ptr<TreeNode>> children;
|
||||
std::optional<RegisteredFile> file;
|
||||
};
|
||||
|
||||
inline std::vector<std::string> Components(const std::string& canonicalPath) {
|
||||
std::vector<std::string> result;
|
||||
size_t cursor = canonicalPath == "/" ? canonicalPath.size() : 1;
|
||||
while (cursor < canonicalPath.size()) {
|
||||
const size_t slash = canonicalPath.find('/', cursor);
|
||||
const size_t end = slash == std::string::npos ? canonicalPath.size() : slash;
|
||||
result.push_back(canonicalPath.substr(cursor, end - cursor));
|
||||
cursor = end + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
inline std::string Lowercase(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
inline uint32_t EmitTree(const TreeNode& directory,
|
||||
uint32_t directoryIndex,
|
||||
const std::string& directoryPath,
|
||||
Image& image,
|
||||
std::vector<std::string>& names) {
|
||||
for (const auto& [lookupName, childPointer] : directory.children) {
|
||||
(void)lookupName;
|
||||
const TreeNode& child = *childPointer;
|
||||
const std::string childPath = directoryPath == "/"
|
||||
? "/" + child.name
|
||||
: directoryPath + "/" + child.name;
|
||||
const uint32_t index = static_cast<uint32_t>(image.entries.size());
|
||||
|
||||
if (!child.children.empty()) {
|
||||
if (child.file.has_value()) {
|
||||
throw std::runtime_error("DVD FST path is both a file and a directory: " + childPath);
|
||||
}
|
||||
image.entries.push_back({{}, childPath, 0, 0, directoryIndex, 0, true});
|
||||
names.push_back(child.name);
|
||||
image.pathToEntry.emplace(NormalizeLookupPath(childPath), static_cast<int32_t>(index));
|
||||
image.entries[index].subtreeEnd = EmitTree(child, index, childPath, image, names);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!child.file.has_value()) {
|
||||
throw std::runtime_error("DVD FST contains an empty implicit node: " + childPath);
|
||||
}
|
||||
const RegisteredFile& file = *child.file;
|
||||
image.entries.push_back({file.hostPath, childPath, file.size, file.discOffsetWords,
|
||||
directoryIndex, index + 1, false});
|
||||
names.push_back(child.name);
|
||||
image.pathToEntry.emplace(NormalizeLookupPath(childPath), static_cast<int32_t>(index));
|
||||
}
|
||||
return static_cast<uint32_t>(image.entries.size());
|
||||
}
|
||||
|
||||
} // namespace Detail
|
||||
|
||||
inline Image BuildImage(const std::vector<RegisteredFile>& registrations) {
|
||||
// Overlay scanning deliberately registers later mappings last. Collapse those
|
||||
// mappings before assigning FST indices so one guest path has one stable entry.
|
||||
std::map<std::string, RegisteredFile> filesByPath;
|
||||
for (RegisteredFile file : registrations) {
|
||||
file.dvdPath = CanonicalizePath(file.dvdPath);
|
||||
if (file.dvdPath == "/") {
|
||||
throw std::runtime_error("DVD FST cannot register the root as a file");
|
||||
}
|
||||
filesByPath[NormalizeLookupPath(file.dvdPath)] = std::move(file);
|
||||
}
|
||||
|
||||
Detail::TreeNode root;
|
||||
for (const auto& [lookupPath, file] : filesByPath) {
|
||||
(void)lookupPath;
|
||||
Detail::TreeNode* node = &root;
|
||||
const std::vector<std::string> components = Detail::Components(file.dvdPath);
|
||||
for (const std::string& component : components) {
|
||||
const std::string key = Detail::Lowercase(component);
|
||||
auto& child = node->children[key];
|
||||
if (!child) {
|
||||
child = std::make_unique<Detail::TreeNode>();
|
||||
child->name = component;
|
||||
}
|
||||
node = child.get();
|
||||
}
|
||||
node->name = components.back();
|
||||
node->file = file;
|
||||
}
|
||||
|
||||
Image image;
|
||||
image.entries.push_back({{}, "/", 0, 0, 0, 0, true});
|
||||
image.pathToEntry.emplace("/", 0);
|
||||
std::vector<std::string> names(1);
|
||||
image.entries[0].subtreeEnd = Detail::EmitTree(root, 0, "/", image, names);
|
||||
|
||||
if (image.entries.size() > std::numeric_limits<uint32_t>::max() / 12u) {
|
||||
throw std::runtime_error("DVD FST contains too many entries");
|
||||
}
|
||||
|
||||
const size_t entriesSize = image.entries.size() * 12u;
|
||||
std::vector<uint8_t> stringTable(1, 0);
|
||||
std::vector<uint32_t> nameOffsets(image.entries.size(), 0);
|
||||
for (size_t i = 1; i < names.size(); ++i) {
|
||||
if (stringTable.size() > 0x00FFFFFFu) {
|
||||
throw std::runtime_error("DVD FST name table exceeds the Wii 24-bit offset limit");
|
||||
}
|
||||
nameOffsets[i] = static_cast<uint32_t>(stringTable.size());
|
||||
stringTable.insert(stringTable.end(), names[i].begin(), names[i].end());
|
||||
stringTable.push_back(0);
|
||||
}
|
||||
|
||||
image.bytes.assign(entriesSize + stringTable.size(), 0);
|
||||
for (size_t i = 0; i < image.entries.size(); ++i) {
|
||||
const IndexedEntry& entry = image.entries[i];
|
||||
const uint32_t typeAndName = (entry.isDirectory ? 0x01000000u : 0u) | nameOffsets[i];
|
||||
const uint32_t word1 = entry.isDirectory ? entry.parentIndex : entry.discOffsetWords;
|
||||
const uint32_t word2 = entry.isDirectory ? entry.subtreeEnd : entry.size;
|
||||
BigEndian::Write32(image.bytes.data(), i * 12u + 0u, typeAndName);
|
||||
BigEndian::Write32(image.bytes.data(), i * 12u + 4u, word1);
|
||||
BigEndian::Write32(image.bytes.data(), i * 12u + 8u, word2);
|
||||
}
|
||||
std::copy(stringTable.begin(), stringTable.end(), image.bytes.begin() + entriesSize);
|
||||
return image;
|
||||
}
|
||||
|
||||
inline std::optional<GuestPlacement> ReserveBelowArena(uint32_t arenaLo,
|
||||
uint32_t arenaHi,
|
||||
size_t byteCount) {
|
||||
constexpr uint32_t kAlignment = 32;
|
||||
if (byteCount == 0 || byteCount > std::numeric_limits<uint32_t>::max() || arenaHi <= arenaLo) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const uint32_t size = static_cast<uint32_t>(byteCount);
|
||||
if (size > arenaHi - arenaLo) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const uint32_t unaligned = arenaHi - size;
|
||||
const uint32_t address = unaligned & ~(kAlignment - 1u);
|
||||
if (address < arenaLo || static_cast<uint64_t>(address) + size > arenaHi) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return GuestPlacement{address, address};
|
||||
}
|
||||
|
||||
} // namespace DvdFstContract
|
||||
|
||||
namespace DvdReadContract {
|
||||
|
||||
inline constexpr int32_t kInterruptTransferComplete = 1;
|
||||
inline constexpr int32_t kInterruptDriveError = 2;
|
||||
|
||||
struct LowReadCompletion {
|
||||
int32_t returnValue;
|
||||
int32_t callbackResult;
|
||||
};
|
||||
|
||||
inline constexpr LowReadCompletion CompletionFor(bool succeeded) noexcept {
|
||||
return succeeded ? LowReadCompletion{1, kInterruptTransferComplete}
|
||||
: LowReadCompletion{0, kInterruptDriveError};
|
||||
}
|
||||
|
||||
enum class HostReadFailure : uint8_t {
|
||||
None,
|
||||
MissingFile,
|
||||
BadOffset,
|
||||
ShortRead,
|
||||
};
|
||||
|
||||
inline constexpr const char* Describe(HostReadFailure failure) noexcept {
|
||||
switch (failure) {
|
||||
case HostReadFailure::None:
|
||||
return "no error";
|
||||
case HostReadFailure::MissingFile:
|
||||
return "host file is missing or cannot be opened";
|
||||
case HostReadFailure::BadOffset:
|
||||
return "read offset is outside the host file";
|
||||
case HostReadFailure::ShortRead:
|
||||
return "host file did not contain the complete requested range";
|
||||
}
|
||||
return "unknown host read error";
|
||||
}
|
||||
|
||||
// Read into private storage first and publish it only after the complete host
|
||||
// range has been obtained. Callers can therefore leave a guest DMA destination
|
||||
// untouched for every failure, including a host file truncated after indexing.
|
||||
inline bool ReadExact(const std::filesystem::path& hostPath,
|
||||
uint64_t offset,
|
||||
uint32_t length,
|
||||
std::vector<uint8_t>& destination,
|
||||
HostReadFailure& failure) {
|
||||
failure = HostReadFailure::None;
|
||||
|
||||
std::ifstream file(hostPath, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
failure = HostReadFailure::MissingFile;
|
||||
return false;
|
||||
}
|
||||
|
||||
file.seekg(0, std::ios::end);
|
||||
const std::streamoff fileSize = file.tellg();
|
||||
if (fileSize < 0 ||
|
||||
offset > static_cast<uint64_t>(std::numeric_limits<std::streamoff>::max()) ||
|
||||
offset >= static_cast<uint64_t>(fileSize)) {
|
||||
failure = HostReadFailure::BadOffset;
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint64_t remaining = static_cast<uint64_t>(fileSize) - offset;
|
||||
if (static_cast<uint64_t>(length) > remaining) {
|
||||
failure = HostReadFailure::ShortRead;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> staged(length);
|
||||
file.seekg(static_cast<std::streamoff>(offset), std::ios::beg);
|
||||
if (!file) {
|
||||
failure = HostReadFailure::BadOffset;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (length != 0) {
|
||||
file.read(reinterpret_cast<char*>(staged.data()),
|
||||
static_cast<std::streamsize>(length));
|
||||
if (file.gcount() != static_cast<std::streamsize>(length)) {
|
||||
failure = HostReadFailure::ShortRead;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
destination = std::move(staged);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace DvdReadContract
|
||||
@@ -0,0 +1,199 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
namespace NetworkDeferredContract {
|
||||
|
||||
enum class PreparationState : uint8_t {
|
||||
NotApplicable,
|
||||
Ready,
|
||||
Error,
|
||||
};
|
||||
|
||||
// A recognized operation must not be represented by an empty optional: doing
|
||||
// so makes malformed input and resource failures indistinguishable from an
|
||||
// unrelated ioctl and lets the caller fall through to a blocking fallback.
|
||||
template <typename Work> class Preparation {
|
||||
public:
|
||||
static Preparation NotApplicable() {
|
||||
return Preparation(PreparationState::NotApplicable, 0, std::nullopt);
|
||||
}
|
||||
|
||||
static Preparation Ready(Work work, int32_t failureResult) {
|
||||
return Preparation(PreparationState::Ready, failureResult,
|
||||
std::optional<Work>(std::move(work)));
|
||||
}
|
||||
|
||||
static Preparation Error(int32_t result) {
|
||||
return Preparation(PreparationState::Error, result, std::nullopt);
|
||||
}
|
||||
|
||||
PreparationState State() const noexcept { return state_; }
|
||||
int32_t FailureResult() const noexcept { return failureResult_; }
|
||||
|
||||
Work &&TakeWork() && { return std::move(*work_); }
|
||||
|
||||
private:
|
||||
Preparation(PreparationState state, int32_t failureResult,
|
||||
std::optional<Work> work)
|
||||
: state_(state), failureResult_(failureResult), work_(std::move(work)) {}
|
||||
|
||||
PreparationState state_ = PreparationState::NotApplicable;
|
||||
int32_t failureResult_ = 0;
|
||||
std::optional<Work> work_;
|
||||
};
|
||||
|
||||
enum class StartDisposition : uint8_t {
|
||||
NotApplicable,
|
||||
Started,
|
||||
ImmediateResult,
|
||||
};
|
||||
|
||||
struct StartOutcome {
|
||||
StartDisposition disposition = StartDisposition::NotApplicable;
|
||||
int32_t result = 0;
|
||||
uint64_t token = 0;
|
||||
|
||||
static constexpr StartOutcome NotApplicable() noexcept { return {}; }
|
||||
|
||||
static constexpr StartOutcome Started(uint64_t token) noexcept {
|
||||
return {StartDisposition::Started, 0, token};
|
||||
}
|
||||
|
||||
static constexpr StartOutcome Immediate(int32_t result) noexcept {
|
||||
return {StartDisposition::ImmediateResult, result, 0};
|
||||
}
|
||||
};
|
||||
|
||||
// Launcher returns a token when the operation is fully installed. Token zero
|
||||
// is valid for an asynchronous route because only synchronous callers consume
|
||||
// it. A launcher failure or exception becomes the operation-specific immediate
|
||||
// error; it can never be reinterpreted as "not applicable".
|
||||
template <typename Work, typename Launcher>
|
||||
StartOutcome StartPrepared(Preparation<Work> &&preparation,
|
||||
Launcher &&launcher) {
|
||||
const PreparationState state = preparation.State();
|
||||
const int32_t failureResult = preparation.FailureResult();
|
||||
if (state == PreparationState::NotApplicable) {
|
||||
return StartOutcome::NotApplicable();
|
||||
}
|
||||
if (state == PreparationState::Error) {
|
||||
return StartOutcome::Immediate(failureResult);
|
||||
}
|
||||
|
||||
try {
|
||||
const std::optional<uint64_t> token =
|
||||
std::forward<Launcher>(launcher)(std::move(preparation).TakeWork());
|
||||
return token ? StartOutcome::Started(*token)
|
||||
: StartOutcome::Immediate(failureResult);
|
||||
} catch (...) {
|
||||
return StartOutcome::Immediate(failureResult);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep an untouched copy of the host-only work value until resolution and
|
||||
// completion publication both succeed. Any worker-side exception is converted
|
||||
// into one failure-publication attempt instead of escaping the thread entry and
|
||||
// terminating the process.
|
||||
template <typename Work, typename Resolver, typename Publish,
|
||||
typename PublishFailure>
|
||||
void RunWorker(Work work, Resolver &&resolver, Publish &&publish,
|
||||
PublishFailure &&publishFailure) noexcept {
|
||||
try {
|
||||
auto completion = std::forward<Resolver>(resolver)(work);
|
||||
std::forward<Publish>(publish)(std::move(completion));
|
||||
} catch (...) {
|
||||
const std::exception_ptr error = std::current_exception();
|
||||
try {
|
||||
std::forward<PublishFailure>(publishFailure)(std::move(work), error);
|
||||
} catch (...) {
|
||||
// There is no safe blocking fallback from a detached worker. The
|
||||
// production failure publisher logs if its completion queue cannot
|
||||
// accept the already-normalized failure.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Worker, typename OnDetachFailure>
|
||||
Worker *DetachOrRelease(std::unique_ptr<Worker> worker,
|
||||
OnDetachFailure &&onDetachFailure) noexcept {
|
||||
if (!worker) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
try {
|
||||
worker->detach();
|
||||
return nullptr;
|
||||
} catch (...) {
|
||||
const std::exception_ptr error = std::current_exception();
|
||||
Worker *const runningWorker = worker.release();
|
||||
try {
|
||||
std::forward<OnDetachFailure>(onDetachFailure)(error);
|
||||
} catch (...) {
|
||||
// Diagnostics must not turn containment of a running worker into a
|
||||
// second failure path.
|
||||
}
|
||||
return runningWorker;
|
||||
}
|
||||
}
|
||||
|
||||
// IOS encodes an IPv4 sockaddr as a two-byte length/family header followed by
|
||||
// sockaddr::sa_data. Advertising any larger ai_addrlen would expose bytes that
|
||||
// were never copied into the guest result.
|
||||
inline constexpr size_t kWiiSockAddrHeaderBytes = 2;
|
||||
inline constexpr size_t kWiiSockAddrPayloadBytes = 14;
|
||||
inline constexpr size_t kWiiIpv4SockAddrBytes =
|
||||
kWiiSockAddrHeaderBytes + kWiiSockAddrPayloadBytes;
|
||||
|
||||
inline constexpr bool CanCopyIpv4SockAddr(int nativeFamily, size_t nativeLength,
|
||||
int nativeIpv4Family) noexcept {
|
||||
return nativeFamily == nativeIpv4Family &&
|
||||
nativeLength >= kWiiIpv4SockAddrBytes;
|
||||
}
|
||||
|
||||
inline constexpr bool
|
||||
AdvertisedSockAddrFits(uint32_t advertisedLength) noexcept {
|
||||
return advertisedLength <= kWiiIpv4SockAddrBytes;
|
||||
}
|
||||
|
||||
} // namespace NetworkDeferredContract
|
||||
|
||||
namespace NetworkConnectContract {
|
||||
|
||||
// IOS presents a blocking socket to the guest while the retained host socket
|
||||
// stays nonblocking. A blocking guest connect therefore waits on the guest
|
||||
// OSThread, never inside WSAPoll/poll on the emulation scheduler thread.
|
||||
inline constexpr int64_t kGuestBlockingTimeoutMilliseconds = 10000;
|
||||
|
||||
enum class ProbeDisposition : uint8_t {
|
||||
StaleSocket,
|
||||
PollError,
|
||||
SocketReady,
|
||||
TimedOut,
|
||||
Pending,
|
||||
};
|
||||
|
||||
// Keep the ordering explicit: fd reuse invalidates the operation before any
|
||||
// host syscall, readiness wins at the deadline, and only a zero-result probe
|
||||
// may remain pending or time out.
|
||||
inline constexpr ProbeDisposition ClassifyProbe(
|
||||
bool socketIdentityIsCurrent, int pollResult, bool deadlineExpired) noexcept {
|
||||
if (!socketIdentityIsCurrent) {
|
||||
return ProbeDisposition::StaleSocket;
|
||||
}
|
||||
if (pollResult < 0) {
|
||||
return ProbeDisposition::PollError;
|
||||
}
|
||||
if (pollResult > 0) {
|
||||
return ProbeDisposition::SocketReady;
|
||||
}
|
||||
return deadlineExpired ? ProbeDisposition::TimedOut
|
||||
: ProbeDisposition::Pending;
|
||||
}
|
||||
|
||||
} // namespace NetworkConnectContract
|
||||
@@ -0,0 +1,161 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#else
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#endif
|
||||
|
||||
namespace NetworkPollContract {
|
||||
|
||||
constexpr size_t kMaxDescriptors = 24;
|
||||
|
||||
#ifdef _WIN32
|
||||
using NativeSocket = SOCKET;
|
||||
using NativePollFd = WSAPOLLFD;
|
||||
constexpr NativeSocket kInvalidSocket = INVALID_SOCKET;
|
||||
#else
|
||||
using NativeSocket = int;
|
||||
using NativePollFd = pollfd;
|
||||
constexpr NativeSocket kInvalidSocket = -1;
|
||||
#endif
|
||||
|
||||
struct CopiedDescriptor {
|
||||
uint32_t wiiFd = 0;
|
||||
NativeSocket nativeFd = kInvalidSocket;
|
||||
uint64_t socketGeneration = 0;
|
||||
short events = 0;
|
||||
short revents = 0;
|
||||
};
|
||||
|
||||
// A zero-timeout SO_POLL is a pure readiness probe. Running it directly on
|
||||
// the emulation thread is safe because ProbeNow always passes timeout zero to
|
||||
// the host API; putting the guest IOS caller to sleep until the next scheduler
|
||||
// pump only adds a needless context switch to every GameSpy update tick.
|
||||
inline bool RequiresSchedulerWait(int64_t timeoutMilliseconds) {
|
||||
return timeoutMilliseconds != 0;
|
||||
}
|
||||
|
||||
inline short WiiEventsToNative(uint32_t events) {
|
||||
int native = 0;
|
||||
if (events & 0x0001u) native |= POLLRDNORM;
|
||||
if (events & 0x0002u) native |= POLLRDBAND;
|
||||
if (events & 0x0004u) native |= POLLPRI;
|
||||
if (events & 0x0008u) native |= POLLWRNORM;
|
||||
if (events & 0x0010u) native |= POLLWRBAND;
|
||||
|
||||
// ERR/HUP/NVAL are return-only. Winsock's WSAPoll also rejects the
|
||||
// priority and write-band inputs which Dolphin masks on Windows.
|
||||
native &= ~(POLLERR | POLLHUP | POLLNVAL);
|
||||
#ifdef _WIN32
|
||||
native &= ~(POLLPRI | POLLWRBAND);
|
||||
#endif
|
||||
return static_cast<short>(native);
|
||||
}
|
||||
|
||||
inline uint32_t NativeEventsToWii(short events) {
|
||||
uint32_t wii = 0;
|
||||
if (events & POLLRDNORM) wii |= 0x0001u;
|
||||
if (events & POLLRDBAND) wii |= 0x0002u;
|
||||
if (events & POLLPRI) wii |= 0x0004u;
|
||||
if (events & POLLWRNORM) wii |= 0x0008u;
|
||||
if (events & POLLWRBAND) wii |= 0x0010u;
|
||||
if (events & POLLERR) wii |= 0x0020u;
|
||||
if (events & POLLHUP) wii |= 0x0040u;
|
||||
if (events & POLLNVAL) wii |= 0x0080u;
|
||||
return wii;
|
||||
}
|
||||
|
||||
class Timeout {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using TimePoint = Clock::time_point;
|
||||
|
||||
static Timeout FromMilliseconds(int64_t milliseconds, TimePoint now = Clock::now()) {
|
||||
Timeout timeout;
|
||||
if (milliseconds < 0) {
|
||||
timeout.m_infinite = true;
|
||||
timeout.m_deadline = TimePoint::max();
|
||||
return timeout;
|
||||
}
|
||||
|
||||
using Milliseconds = std::chrono::milliseconds;
|
||||
const int64_t maximum = std::chrono::duration_cast<Milliseconds>(TimePoint::max() - now).count();
|
||||
timeout.m_deadline = milliseconds >= maximum
|
||||
? TimePoint::max()
|
||||
: now + Milliseconds(milliseconds);
|
||||
return timeout;
|
||||
}
|
||||
|
||||
bool IsExpired(TimePoint now = Clock::now()) const {
|
||||
return !m_infinite && now >= m_deadline;
|
||||
}
|
||||
|
||||
bool ShouldRemainPending(int nativeResult, TimePoint now = Clock::now()) const {
|
||||
return nativeResult == 0 && !IsExpired(now);
|
||||
}
|
||||
|
||||
bool IsInfinite() const { return m_infinite; }
|
||||
TimePoint Deadline() const { return m_deadline; }
|
||||
|
||||
private:
|
||||
bool m_infinite = false;
|
||||
TimePoint m_deadline{};
|
||||
};
|
||||
|
||||
// Probes only descriptors whose copied socket identity is still live. A dead identity
|
||||
// (SOClose/SOCleanup/slot reuse) reports POLLNVAL and counts toward readiness like IOS/Dolphin;
|
||||
// skipping it silently would return 0 forever and strand an infinite-timeout SO_POLL parked
|
||||
// during socket teardown mid-WFC-connect.
|
||||
template <typename IsStillValid>
|
||||
int ProbeNow(std::vector<CopiedDescriptor>& descriptors, IsStillValid&& isStillValid) {
|
||||
if (descriptors.size() > kMaxDescriptors) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::array<NativePollFd, kMaxDescriptors> active{};
|
||||
std::array<size_t, kMaxDescriptors> originalIndices{};
|
||||
size_t activeCount = 0;
|
||||
int invalidCount = 0;
|
||||
for (size_t i = 0; i < descriptors.size(); ++i) {
|
||||
CopiedDescriptor& descriptor = descriptors[i];
|
||||
descriptor.revents = 0;
|
||||
if (!isStillValid(descriptor)) {
|
||||
descriptor.revents = POLLNVAL;
|
||||
++invalidCount;
|
||||
continue;
|
||||
}
|
||||
active[activeCount].fd = descriptor.nativeFd;
|
||||
active[activeCount].events = descriptor.events;
|
||||
active[activeCount].revents = 0;
|
||||
originalIndices[activeCount] = i;
|
||||
++activeCount;
|
||||
}
|
||||
|
||||
if (activeCount == 0) {
|
||||
return invalidCount;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
const int result = WSAPoll(active.data(), static_cast<ULONG>(activeCount), 0);
|
||||
#else
|
||||
const int result = poll(active.data(), activeCount, 0);
|
||||
#endif
|
||||
if (result >= 0) {
|
||||
for (size_t i = 0; i < activeCount; ++i) {
|
||||
descriptors[originalIndices[i]].revents = active[i].revents;
|
||||
}
|
||||
return result + invalidCount;
|
||||
}
|
||||
return invalidCount > 0 ? invalidCount : result;
|
||||
}
|
||||
|
||||
} // namespace NetworkPollContract
|
||||
@@ -0,0 +1,692 @@
|
||||
// Riivolution patch-XML parsing and patch selection.
|
||||
//
|
||||
// Ported from Dolphin Emulator's DiscIO/RiivolutionParser.h/cpp and the
|
||||
// external-path resolution rules of DiscIO/RiivolutionPatcher.cpp
|
||||
// (https://github.com/dolphin-emu/dolphin).
|
||||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
//
|
||||
// Deviations from Dolphin, all deliberate:
|
||||
// - <memory> patches are parsed but never applied here: guest code patching
|
||||
// belongs to the translator's Code.pul/lowmem pipeline, not the runtime.
|
||||
// - Riivolution "macros" are not supported
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
namespace RiivolutionContract {
|
||||
|
||||
// ============================================================================
|
||||
// Minimal XML document model
|
||||
// ============================================================================
|
||||
|
||||
struct XmlNode {
|
||||
std::string name;
|
||||
std::vector<std::pair<std::string, std::string>> attributes;
|
||||
std::vector<XmlNode> children;
|
||||
|
||||
const std::string* FindAttribute(std::string_view attributeName) const {
|
||||
for (const auto& attribute : attributes) {
|
||||
if (attribute.first == attributeName) {
|
||||
return &attribute.second;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string Attribute(std::string_view attributeName, std::string_view fallback = {}) const {
|
||||
const std::string* value = FindAttribute(attributeName);
|
||||
return value ? *value : std::string(fallback);
|
||||
}
|
||||
|
||||
bool AttributeBool(std::string_view attributeName, bool fallback) const {
|
||||
const std::string* value = FindAttribute(attributeName);
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
if (*value == "true" || *value == "1" || *value == "yes") {
|
||||
return true;
|
||||
}
|
||||
if (*value == "false" || *value == "0" || *value == "no") {
|
||||
return false;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// Accepts decimal and 0x-prefixed hex, matching pugixml's number parsing
|
||||
// (Riivolution XMLs write memory offsets as 0x........).
|
||||
uint32_t AttributeUint(std::string_view attributeName, uint32_t fallback) const {
|
||||
const std::string* value = FindAttribute(attributeName);
|
||||
if (!value || value->empty()) {
|
||||
return fallback;
|
||||
}
|
||||
const std::string& text = *value;
|
||||
size_t index = 0;
|
||||
uint32_t base = 10;
|
||||
if (text.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) {
|
||||
base = 16;
|
||||
index = 2;
|
||||
}
|
||||
uint64_t result = 0;
|
||||
for (; index < text.size(); ++index) {
|
||||
const char c = text[index];
|
||||
uint32_t digit;
|
||||
if (c >= '0' && c <= '9') {
|
||||
digit = static_cast<uint32_t>(c - '0');
|
||||
} else if (base == 16 && c >= 'a' && c <= 'f') {
|
||||
digit = static_cast<uint32_t>(c - 'a' + 10);
|
||||
} else if (base == 16 && c >= 'A' && c <= 'F') {
|
||||
digit = static_cast<uint32_t>(c - 'A' + 10);
|
||||
} else {
|
||||
return fallback;
|
||||
}
|
||||
result = result * base + digit;
|
||||
if (result > 0xffffffffull) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
return index > (base == 16 ? 2u : 0u) ? static_cast<uint32_t>(result) : fallback;
|
||||
}
|
||||
|
||||
int AttributeInt(std::string_view attributeName, int fallback) const {
|
||||
const std::string* value = FindAttribute(attributeName);
|
||||
if (!value || value->empty()) {
|
||||
return fallback;
|
||||
}
|
||||
const bool negative = (*value)[0] == '-';
|
||||
const uint32_t magnitude =
|
||||
AttributeUintFromText(negative ? value->substr(1) : *value, 0x80000000u);
|
||||
if (magnitude == 0x80000000u && !negative) {
|
||||
return fallback;
|
||||
}
|
||||
return negative ? -static_cast<int>(magnitude) : static_cast<int>(magnitude);
|
||||
}
|
||||
|
||||
private:
|
||||
static uint32_t AttributeUintFromText(const std::string& text, uint32_t fallback) {
|
||||
XmlNode probe;
|
||||
probe.attributes.push_back({"v", text});
|
||||
return probe.AttributeUint("v", fallback);
|
||||
}
|
||||
};
|
||||
|
||||
namespace XmlDetail {
|
||||
|
||||
inline bool StartsWith(std::string_view text, std::string_view prefix) {
|
||||
return text.size() >= prefix.size() && text.compare(0, prefix.size(), prefix) == 0;
|
||||
}
|
||||
|
||||
} // namespace XmlDetail
|
||||
|
||||
// Converts only element and attribute data from pugixml. Riivolution carries
|
||||
// its data in attributes, so text, declarations, comments, and CDATA do not
|
||||
// need to become part of the contract's data model.
|
||||
inline XmlNode CopyXmlNode(const pugi::xml_node& source) {
|
||||
XmlNode destination;
|
||||
destination.name = source.name();
|
||||
for (const pugi::xml_attribute& attribute : source.attributes()) {
|
||||
destination.attributes.emplace_back(attribute.name(), attribute.value());
|
||||
}
|
||||
for (const pugi::xml_node& child : source.children()) {
|
||||
if (child.type() == pugi::node_element) {
|
||||
destination.children.push_back(CopyXmlNode(child));
|
||||
}
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
// Parses a document with pugixml and returns its root element, or nullopt when
|
||||
// malformed. load_buffer accepts the UTF-8 BOM used by some Riivolution packs.
|
||||
inline std::optional<XmlNode> ParseXml(std::string_view text) {
|
||||
pugi::xml_document document;
|
||||
const pugi::xml_parse_result result = document.load_buffer(
|
||||
text.data(), text.size(), pugi::parse_default, pugi::encoding_utf8);
|
||||
if (!result) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const pugi::xml_node root = document.document_element();
|
||||
if (!root) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return CopyXmlNode(root);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Riivolution data model (mirrors Dolphin's DiscIO::Riivolution)
|
||||
// ============================================================================
|
||||
|
||||
struct GameFilter {
|
||||
std::optional<std::string> game;
|
||||
std::optional<std::string> developer;
|
||||
std::optional<int> disc;
|
||||
std::optional<int> version;
|
||||
std::optional<std::vector<std::string>> regions;
|
||||
};
|
||||
|
||||
struct PatchReference {
|
||||
std::string id;
|
||||
std::map<std::string, std::string> params;
|
||||
};
|
||||
|
||||
struct Choice {
|
||||
std::string name;
|
||||
std::vector<PatchReference> patchReferences;
|
||||
};
|
||||
|
||||
struct Option {
|
||||
std::string name;
|
||||
std::string id;
|
||||
std::vector<Choice> choices;
|
||||
|
||||
// 1-based index into choices; 0 means disabled.
|
||||
uint32_t selectedChoice = 0;
|
||||
};
|
||||
|
||||
struct Section {
|
||||
std::string name;
|
||||
std::vector<Option> options;
|
||||
};
|
||||
|
||||
struct File {
|
||||
std::string disc;
|
||||
std::string external;
|
||||
bool resize = true;
|
||||
bool create = false;
|
||||
uint32_t offset = 0;
|
||||
uint32_t fileoffset = 0;
|
||||
uint32_t length = 0;
|
||||
};
|
||||
|
||||
struct Folder {
|
||||
std::string disc;
|
||||
std::string external;
|
||||
bool resize = true;
|
||||
bool create = false;
|
||||
bool recursive = true;
|
||||
uint32_t length = 0;
|
||||
};
|
||||
|
||||
struct Savegame {
|
||||
std::string external;
|
||||
bool clone = true;
|
||||
};
|
||||
|
||||
// Parsed for completeness; the runtime never applies these (guest code and
|
||||
// lowmem patching is the translator pipeline's job).
|
||||
struct MemoryPatch {
|
||||
uint32_t offset = 0;
|
||||
std::string value;
|
||||
std::string valuefile;
|
||||
std::string original;
|
||||
bool ocarina = false;
|
||||
bool search = false;
|
||||
uint32_t align = 1;
|
||||
};
|
||||
|
||||
struct Patch {
|
||||
std::string id;
|
||||
std::string root;
|
||||
std::vector<File> filePatches;
|
||||
std::vector<Folder> folderPatches;
|
||||
std::vector<Savegame> savegamePatches;
|
||||
std::vector<MemoryPatch> memoryPatches;
|
||||
};
|
||||
|
||||
struct Disc {
|
||||
int version = 0;
|
||||
GameFilter gameFilter;
|
||||
std::vector<Section> sections;
|
||||
std::vector<Patch> patches;
|
||||
|
||||
bool IsValidForGame(const std::string& gameId, std::optional<uint16_t> revision,
|
||||
std::optional<uint8_t> discNumber) const;
|
||||
std::vector<Patch> GeneratePatches(const std::string& gameId) const;
|
||||
};
|
||||
|
||||
// riivolution/config/<GameID4>.xml - remembered option choices.
|
||||
struct ConfigOption {
|
||||
std::string id;
|
||||
uint32_t defaultChoice = 0;
|
||||
};
|
||||
|
||||
struct Config {
|
||||
int version = 0;
|
||||
std::vector<ConfigOption> options;
|
||||
};
|
||||
|
||||
// An option choice pinned by the distribution manifest (recomp.yml).
|
||||
struct OptionSelection {
|
||||
std::string section; // empty = match any section
|
||||
std::string option; // matches Option::id first, then Option::name
|
||||
uint32_t choice = 0;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Parsing
|
||||
// ============================================================================
|
||||
|
||||
namespace Detail {
|
||||
|
||||
inline std::map<std::string, std::string> ReadParams(const XmlNode& node,
|
||||
std::map<std::string, std::string> params = {}) {
|
||||
for (const XmlNode& paramNode : node.children) {
|
||||
if (paramNode.name != "param") {
|
||||
continue;
|
||||
}
|
||||
params[paramNode.Attribute("name")] = paramNode.Attribute("value");
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
} // namespace Detail
|
||||
|
||||
inline std::optional<Disc> ParseString(std::string_view xml) {
|
||||
const std::optional<XmlNode> root = ParseXml(xml);
|
||||
if (!root || root->name != "wiidisc") {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Disc disc;
|
||||
disc.version = root->AttributeInt("version", -1);
|
||||
if (disc.version != 1) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const std::string defaultRoot = root->Attribute("root");
|
||||
|
||||
for (const XmlNode& node : root->children) {
|
||||
if (node.name == "id") {
|
||||
for (const auto& attribute : node.attributes) {
|
||||
if (attribute.first == "game") {
|
||||
disc.gameFilter.game = attribute.second;
|
||||
} else if (attribute.first == "developer") {
|
||||
disc.gameFilter.developer = attribute.second;
|
||||
} else if (attribute.first == "disc") {
|
||||
disc.gameFilter.disc = node.AttributeInt("disc", -1);
|
||||
} else if (attribute.first == "version") {
|
||||
disc.gameFilter.version = node.AttributeInt("version", -1);
|
||||
}
|
||||
}
|
||||
std::vector<std::string> regions;
|
||||
for (const XmlNode& regionNode : node.children) {
|
||||
if (regionNode.name == "region") {
|
||||
regions.push_back(regionNode.Attribute("type"));
|
||||
}
|
||||
}
|
||||
if (!regions.empty()) {
|
||||
disc.gameFilter.regions = std::move(regions);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.name == "options") {
|
||||
for (const XmlNode& sectionNode : node.children) {
|
||||
if (sectionNode.name != "section") {
|
||||
continue;
|
||||
}
|
||||
Section section;
|
||||
section.name = sectionNode.Attribute("name");
|
||||
for (const XmlNode& optionNode : sectionNode.children) {
|
||||
if (optionNode.name != "option") {
|
||||
continue;
|
||||
}
|
||||
Option option;
|
||||
option.id = optionNode.Attribute("id");
|
||||
option.name = optionNode.Attribute("name");
|
||||
option.selectedChoice = optionNode.AttributeUint("default", 0);
|
||||
auto optionParams = Detail::ReadParams(optionNode);
|
||||
for (const XmlNode& choiceNode : optionNode.children) {
|
||||
if (choiceNode.name != "choice") {
|
||||
continue;
|
||||
}
|
||||
Choice choice;
|
||||
choice.name = choiceNode.Attribute("name");
|
||||
auto choiceParams = Detail::ReadParams(choiceNode, optionParams);
|
||||
for (const XmlNode& patchRefNode : choiceNode.children) {
|
||||
if (patchRefNode.name != "patch") {
|
||||
continue;
|
||||
}
|
||||
PatchReference patchReference;
|
||||
patchReference.id = patchRefNode.Attribute("id");
|
||||
patchReference.params = Detail::ReadParams(patchRefNode, choiceParams);
|
||||
choice.patchReferences.push_back(std::move(patchReference));
|
||||
}
|
||||
option.choices.push_back(std::move(choice));
|
||||
}
|
||||
section.options.push_back(std::move(option));
|
||||
}
|
||||
disc.sections.push_back(std::move(section));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.name == "patch") {
|
||||
Patch patch;
|
||||
patch.id = node.Attribute("id");
|
||||
patch.root = node.Attribute("root");
|
||||
if (patch.root.empty()) {
|
||||
patch.root = defaultRoot;
|
||||
}
|
||||
|
||||
for (const XmlNode& patchNode : node.children) {
|
||||
if (patchNode.name == "file") {
|
||||
File file;
|
||||
file.disc = patchNode.Attribute("disc");
|
||||
file.external = patchNode.Attribute("external");
|
||||
file.resize = patchNode.AttributeBool("resize", true);
|
||||
file.create = patchNode.AttributeBool("create", false);
|
||||
file.offset = patchNode.AttributeUint("offset", 0);
|
||||
file.fileoffset = patchNode.AttributeUint("fileoffset", 0);
|
||||
file.length = patchNode.AttributeUint("length", 0);
|
||||
patch.filePatches.push_back(std::move(file));
|
||||
} else if (patchNode.name == "folder") {
|
||||
Folder folder;
|
||||
folder.disc = patchNode.Attribute("disc");
|
||||
folder.external = patchNode.Attribute("external");
|
||||
folder.resize = patchNode.AttributeBool("resize", true);
|
||||
folder.create = patchNode.AttributeBool("create", false);
|
||||
folder.recursive = patchNode.AttributeBool("recursive", true);
|
||||
folder.length = patchNode.AttributeUint("length", 0);
|
||||
patch.folderPatches.push_back(std::move(folder));
|
||||
} else if (patchNode.name == "savegame") {
|
||||
Savegame savegame;
|
||||
savegame.external = patchNode.Attribute("external");
|
||||
savegame.clone = patchNode.AttributeBool("clone", true);
|
||||
patch.savegamePatches.push_back(std::move(savegame));
|
||||
} else if (patchNode.name == "memory") {
|
||||
MemoryPatch memory;
|
||||
memory.offset = patchNode.AttributeUint("offset", 0);
|
||||
memory.value = patchNode.Attribute("value");
|
||||
memory.valuefile = patchNode.Attribute("valuefile");
|
||||
memory.original = patchNode.Attribute("original");
|
||||
memory.ocarina = patchNode.AttributeBool("ocarina", false);
|
||||
memory.search = patchNode.AttributeBool("search", false);
|
||||
memory.align = patchNode.AttributeUint("align", 1);
|
||||
patch.memoryPatches.push_back(std::move(memory));
|
||||
}
|
||||
}
|
||||
disc.patches.push_back(std::move(patch));
|
||||
}
|
||||
}
|
||||
|
||||
return disc;
|
||||
}
|
||||
|
||||
inline std::optional<Config> ParseConfigString(std::string_view xml) {
|
||||
const std::optional<XmlNode> root = ParseXml(xml);
|
||||
if (!root || root->name != "riivolution") {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Config config;
|
||||
config.version = root->AttributeInt("version", -1);
|
||||
if (config.version != 2) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
for (const XmlNode& optionNode : root->children) {
|
||||
if (optionNode.name != "option") {
|
||||
continue;
|
||||
}
|
||||
ConfigOption option;
|
||||
option.id = optionNode.Attribute("id");
|
||||
option.defaultChoice = optionNode.AttributeUint("default", 0);
|
||||
config.options.push_back(std::move(option));
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Game matching and patch generation
|
||||
// ============================================================================
|
||||
|
||||
inline bool Disc::IsValidForGame(const std::string& gameId, std::optional<uint16_t> revision,
|
||||
std::optional<uint8_t> discNumber) const {
|
||||
if (gameId.size() != 6) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string_view gameIdFull(gameId);
|
||||
const std::string_view gameRegion = gameIdFull.substr(3, 1);
|
||||
const std::string_view gameDeveloper = gameIdFull.substr(4, 2);
|
||||
const int discNumberInt = discNumber ? static_cast<int>(*discNumber) : -1;
|
||||
const int revisionInt = revision ? static_cast<int>(*revision) : -1;
|
||||
|
||||
if (gameFilter.game && !XmlDetail::StartsWith(gameIdFull, *gameFilter.game)) {
|
||||
return false;
|
||||
}
|
||||
if (gameFilter.developer && gameDeveloper != *gameFilter.developer) {
|
||||
return false;
|
||||
}
|
||||
if (gameFilter.disc && discNumberInt != *gameFilter.disc) {
|
||||
return false;
|
||||
}
|
||||
if (gameFilter.version && revisionInt != *gameFilter.version) {
|
||||
return false;
|
||||
}
|
||||
if (gameFilter.regions) {
|
||||
const auto& regions = *gameFilter.regions;
|
||||
if (!regions.empty() &&
|
||||
std::find(regions.begin(), regions.end(), std::string(gameRegion)) == regions.end()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::vector<Patch> Disc::GeneratePatches(const std::string& gameId) const {
|
||||
const std::string_view gameIdFull(gameId);
|
||||
const std::string_view gameIdNoRegion = gameIdFull.substr(0, 3);
|
||||
const std::string_view gameRegion = gameIdFull.substr(3, 1);
|
||||
const std::string_view gameDeveloper = gameIdFull.size() >= 6 ? gameIdFull.substr(4, 2) : std::string_view();
|
||||
|
||||
const auto replaceVariables =
|
||||
[](std::string_view sv, const std::vector<std::pair<std::string, std::string_view>>& replacements) {
|
||||
std::string result;
|
||||
result.reserve(sv.size());
|
||||
while (!sv.empty()) {
|
||||
bool replaced = false;
|
||||
for (const auto& replacement : replacements) {
|
||||
if (XmlDetail::StartsWith(sv, replacement.first)) {
|
||||
result.append(replacement.second.data(), replacement.second.size());
|
||||
sv = sv.substr(replacement.first.size());
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (replaced) {
|
||||
continue;
|
||||
}
|
||||
result.push_back(sv[0]);
|
||||
sv = sv.substr(1);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Take only selected patches, replace placeholders in all strings, and
|
||||
// return them.
|
||||
std::vector<Patch> activePatches;
|
||||
for (const Section& section : sections) {
|
||||
for (const Option& option : section.options) {
|
||||
const uint32_t selected = option.selectedChoice;
|
||||
if (selected == 0 || selected > option.choices.size()) {
|
||||
continue;
|
||||
}
|
||||
const Choice& choice = option.choices[selected - 1];
|
||||
for (const PatchReference& patchReference : choice.patchReferences) {
|
||||
const auto patch = std::find_if(patches.begin(), patches.end(), [&](const Patch& candidate) {
|
||||
return candidate.id == patchReference.id;
|
||||
});
|
||||
if (patch == patches.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::string_view>> replacements;
|
||||
replacements.emplace_back("{$__gameid}", gameIdNoRegion);
|
||||
replacements.emplace_back("{$__region}", gameRegion);
|
||||
replacements.emplace_back("{$__maker}", gameDeveloper);
|
||||
for (const auto& param : patchReference.params) {
|
||||
replacements.emplace_back("{$" + param.first + "}", param.second);
|
||||
}
|
||||
|
||||
Patch newPatch = *patch;
|
||||
newPatch.root = replaceVariables(newPatch.root, replacements);
|
||||
for (File& file : newPatch.filePatches) {
|
||||
file.disc = replaceVariables(file.disc, replacements);
|
||||
file.external = replaceVariables(file.external, replacements);
|
||||
}
|
||||
for (Folder& folder : newPatch.folderPatches) {
|
||||
folder.disc = replaceVariables(folder.disc, replacements);
|
||||
folder.external = replaceVariables(folder.external, replacements);
|
||||
}
|
||||
for (Savegame& savegame : newPatch.savegamePatches) {
|
||||
savegame.external = replaceVariables(savegame.external, replacements);
|
||||
}
|
||||
for (MemoryPatch& memory : newPatch.memoryPatches) {
|
||||
memory.valuefile = replaceVariables(memory.valuefile, replacements);
|
||||
}
|
||||
activePatches.push_back(std::move(newPatch));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return activePatches;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Option selection
|
||||
// ============================================================================
|
||||
|
||||
// Dolphin's config identifier: an option is addressed by its id when it has
|
||||
// one, otherwise by the concatenation of section name and option name.
|
||||
inline void ApplyConfigDefaults(Disc& disc, const Config& config) {
|
||||
for (const ConfigOption& configOption : config.options) {
|
||||
for (Section& section : disc.sections) {
|
||||
for (Option& option : section.options) {
|
||||
const bool matches = option.id.empty()
|
||||
? (section.name + option.name) == configOption.id
|
||||
: option.id == configOption.id;
|
||||
if (matches) {
|
||||
option.selectedChoice = configOption.defaultChoice;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Applies distribution-pinned selections. Runs after ApplyConfigDefaults so a
|
||||
// pin always wins over the user's remembered choice.
|
||||
inline void ApplySelections(Disc& disc, const std::vector<OptionSelection>& selections) {
|
||||
for (const OptionSelection& selection : selections) {
|
||||
for (Section& section : disc.sections) {
|
||||
if (!selection.section.empty() && section.name != selection.section) {
|
||||
continue;
|
||||
}
|
||||
for (Option& option : section.options) {
|
||||
const bool matches = (!option.id.empty() && option.id == selection.option) ||
|
||||
option.name == selection.option;
|
||||
if (matches && selection.choice <= option.choices.size()) {
|
||||
option.selectedChoice = selection.choice;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// External path resolution (Dolphin FileDataLoaderHostFS semantics). A leading '/' is absolute
|
||||
// (relative to the SD card root); otherwise relative to the patch root (the XML's folder, or
|
||||
// its 'root' attribute override). All paths use '/' separators (callers convert host paths via
|
||||
// generic_string() first); returns nullopt for ".." traversal or a backslash, which Riivolution
|
||||
// treats as a filename character that Windows paths can't replicate.
|
||||
|
||||
inline std::optional<std::string> MakeAbsoluteFromRelative(std::string_view sdRoot,
|
||||
std::string_view patchRoot,
|
||||
std::string_view externalRelativePath) {
|
||||
if (externalRelativePath.find('\\') != std::string_view::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const bool absolute = !externalRelativePath.empty() && externalRelativePath[0] == '/';
|
||||
std::string result(absolute ? sdRoot : patchRoot);
|
||||
while (!result.empty() && result.back() == '/') {
|
||||
result.pop_back();
|
||||
}
|
||||
|
||||
std::string_view work = externalRelativePath;
|
||||
while (!work.empty() && work.front() == '/') {
|
||||
work.remove_prefix(1);
|
||||
}
|
||||
while (!work.empty() && work.back() == '/') {
|
||||
work.remove_suffix(1);
|
||||
}
|
||||
|
||||
size_t depth = 0;
|
||||
while (!work.empty()) {
|
||||
const size_t separator = work.find('/');
|
||||
const std::string_view element = work.substr(0, separator);
|
||||
|
||||
if (element == ".") {
|
||||
// Harmless, changes nothing.
|
||||
} else if (element == "..") {
|
||||
// Going up a level; never above the root.
|
||||
if (depth == 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
--depth;
|
||||
const size_t lastSlash = result.rfind('/');
|
||||
if (lastSlash == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
result.resize(lastSlash);
|
||||
} else if (!element.empty()) {
|
||||
++depth;
|
||||
result.push_back('/');
|
||||
result.append(element.data(), element.size());
|
||||
}
|
||||
|
||||
if (separator == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
work.remove_prefix(separator + 1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Computes a patch's effective root directory from the XML file's directory
|
||||
// and the patch's 'root' attribute.
|
||||
inline std::string ResolvePatchRoot(std::string_view sdRoot, std::string_view xmlDirectory,
|
||||
std::string_view rootAttribute) {
|
||||
std::string patchRoot(xmlDirectory);
|
||||
if (!rootAttribute.empty()) {
|
||||
if (auto resolved = MakeAbsoluteFromRelative(sdRoot, xmlDirectory, rootAttribute)) {
|
||||
patchRoot = std::move(*resolved);
|
||||
}
|
||||
}
|
||||
return patchRoot;
|
||||
}
|
||||
|
||||
// First <savegame> across the active patches, in order (Dolphin
|
||||
// ExtractSavegameRedirect).
|
||||
inline const Savegame* FindSavegamePatch(const std::vector<Patch>& activePatches) {
|
||||
for (const Patch& patch : activePatches) {
|
||||
if (!patch.savegamePatches.empty()) {
|
||||
return &patch.savegamePatches[0];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace RiivolutionContract
|
||||
Reference in New Issue
Block a user