mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-12 09:45:04 -04:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d836dab3e | |||
| 6bffedf028 | |||
| 09ca4e98f4 | |||
| 6dc4e59052 | |||
| 82b295b20c | |||
| eb1721fd72 | |||
| 129c714f02 | |||
| 2794024d3a | |||
| b5e5858e1d | |||
| 4897e7e27d | |||
| 7ebda6bf7f | |||
| 0403f176bd | |||
| 1912292c80 | |||
| e498e62622 | |||
| f73739f248 | |||
| 3389fe35fc | |||
| e11062a1ba | |||
| 1d1d064d37 | |||
| 9d1f3db1d5 | |||
| 251529d66e | |||
| 987b666296 | |||
| 260022b4ba | |||
| f1c2b60df1 | |||
| 5193dd9749 | |||
| ec9bd92415 | |||
| dbf6d05625 | |||
| 8ba44162fa | |||
| 9ee14e582d | |||
| 0c349a9296 | |||
| 21b57f08a4 | |||
| e146b10af8 | |||
| edd03f8991 | |||
| 9c4f4b738a | |||
| 8d5d93f02d | |||
| c75b482224 | |||
| e5ae29b27e |
@@ -25,6 +25,9 @@ Code.pul
|
||||
# Build output
|
||||
/build/
|
||||
/build-*/
|
||||
/.build-output/
|
||||
/.build-test/
|
||||
/native-build/
|
||||
/dist/
|
||||
/out/
|
||||
[Bb]in/
|
||||
|
||||
@@ -253,7 +253,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
|
||||
|
||||
$manifest = [ordered]@{
|
||||
SchemaVersion = 2
|
||||
ProductVersion = '0.2.21'
|
||||
ProductVersion = '0.2.24'
|
||||
ExpectedGameId = $pins.GameId
|
||||
ExpectedDolSha256 = $pins.DolSha256
|
||||
ExpectedRelSha256 = $pins.RelSha256
|
||||
|
||||
@@ -151,9 +151,10 @@ if ($Profile -ne 'both' -and -not [string]::IsNullOrWhiteSpace($BaseOutputDirect
|
||||
throw '-BaseOutputDirectory is valid only with -Profile both.'
|
||||
}
|
||||
$translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe'
|
||||
$cmake = Join-Path $Toolkit 'CMake\bin\cmake.exe'
|
||||
$ninja = Join-Path $Toolkit 'Ninja\ninja.exe'
|
||||
$toolchainBin = Join-Path $Toolkit 'llvm-mingw\bin'
|
||||
$toolchain = Get-MkwShellSafeToolchainRoot $Toolkit
|
||||
$cmake = Join-Path $toolchain 'CMake\bin\cmake.exe'
|
||||
$ninja = Join-Path $toolchain 'Ninja\ninja.exe'
|
||||
$toolchainBin = Join-Path $toolchain 'llvm-mingw\bin'
|
||||
$cc = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang.exe'
|
||||
$cxx = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang++.exe'
|
||||
$windres = Join-Path $toolchainBin 'x86_64-w64-mingw32-windres.exe'
|
||||
@@ -203,7 +204,7 @@ if ($Parallel -gt 0) {
|
||||
$oldPath = $env:PATH
|
||||
$oldDotnet = $env:DOTNET_ROOT
|
||||
try {
|
||||
$env:PATH = Get-MkwToolchainPath $Toolkit
|
||||
$env:PATH = Get-MkwToolchainPath $toolchain
|
||||
Remove-Item Env:DOTNET_ROOT -ErrorAction SilentlyContinue
|
||||
Push-Location $Workspace
|
||||
try {
|
||||
|
||||
@@ -40,6 +40,43 @@ function Get-MkwToolchainPath([string]$ToolchainRoot) {
|
||||
) -join ';')
|
||||
}
|
||||
|
||||
function Get-MkwShellSafeToolchainRoot([string]$ToolchainRoot) {
|
||||
if ([string]::IsNullOrWhiteSpace($ToolchainRoot)) { throw 'A toolchain root is required.' }
|
||||
$full = [IO.Path]::GetFullPath($ToolchainRoot)
|
||||
# A drive root keeps its separator: "C:" is relative to the current directory on that drive.
|
||||
if ($full -ne [IO.Path]::GetPathRoot($full)) { $full = $full.TrimEnd('\') }
|
||||
if ($full -notmatch '[()&^%!]') { return $full }
|
||||
|
||||
$sha = [Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($full.ToLowerInvariant()))
|
||||
} finally { $sha.Dispose() }
|
||||
$linkName = 'toolchain-' + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '')
|
||||
|
||||
$failures = @()
|
||||
foreach ($base in @($env:ProgramData, $env:PUBLIC)) {
|
||||
if ([string]::IsNullOrWhiteSpace($base) -or $base -match '[()&^%! ]') { continue }
|
||||
$link = Join-Path (Join-Path $base 'WiiCompiled') $linkName
|
||||
try {
|
||||
[IO.Directory]::CreateDirectory((Split-Path -Parent $link)) | Out-Null
|
||||
# The name already identifies the target, so an existing junction that still resolves is
|
||||
# this one; only a broken leftover is replaced. Directory.Delete removes the reparse
|
||||
# point itself, where Remove-Item -Recurse would delete the toolchain it points at.
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $link 'CMake\bin\cmake.exe') -PathType Leaf)) {
|
||||
if (Test-Path -LiteralPath $link) { [IO.Directory]::Delete($link) }
|
||||
New-Item -ItemType Junction -Path $link -Target $full -ErrorAction Stop | Out-Null
|
||||
}
|
||||
Write-Host "MKWCBUILD: Building through $link, because $full contains characters cmd.exe cannot parse"
|
||||
return $link
|
||||
} catch {
|
||||
$failures += "$link ($($_.Exception.Message))"
|
||||
}
|
||||
}
|
||||
throw ("The toolchain path $full contains a character (one of ( ) & ^ % !) that the compiler " +
|
||||
'cannot be invoked through, and no junction to it could be created: ' + ($failures -join '; ') +
|
||||
'. Install to a path without those characters.')
|
||||
}
|
||||
|
||||
function Get-MkwProjectPins([string]$ProjectFile) {
|
||||
<#
|
||||
The Mario Kart Wii facts pinned by projects/mkwii/recomp.yml (game identity, clean input
|
||||
|
||||
@@ -121,7 +121,7 @@ internal static class PlatformChecks
|
||||
internal static class ProductInfo
|
||||
{
|
||||
public const string Name = "WiiCompiled";
|
||||
public const string Version = "0.2.21";
|
||||
public const string Version = "0.2.24";
|
||||
|
||||
/// <summary>
|
||||
/// The setup executable is copied into the installation under this name. It is the launcher and
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<AssemblyName>WiiCompiled.Setup</AssemblyName>
|
||||
<RootNamespace>WiiCompiled.Setup</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Version>0.2.21</Version>
|
||||
<Version>0.2.24</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Command-line installer and launcher for WiiCompiled</Description>
|
||||
|
||||
@@ -63,7 +63,7 @@ inputs like paddles, touchpads and share buttons show up when the hardware repor
|
||||
- Windows 10 or 11, 64-bit
|
||||
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher
|
||||
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
|
||||
- About 20 GB of free disk space during installation
|
||||
- About 20 GB of free disk space during installation (Final game size ~5 GB)
|
||||
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
|
||||
GCZ, CISO, WBFS, WIA and RVZ are accepted.
|
||||
|
||||
|
||||
@@ -229,6 +229,7 @@ s32 PADGetNativeButtonPressed(u32 port);
|
||||
PADSignedNativeAxis PADGetNativeAxisPulled(u32 port);
|
||||
void PADRestoreDefaultMapping(u32 port);
|
||||
void PADBlockInput(bool block);
|
||||
bool PADIsInputBlocked(void);
|
||||
|
||||
/**
|
||||
* Set the default controller mapping used.
|
||||
|
||||
@@ -111,7 +111,9 @@ ECardResult CardGciFolder::createFile(const char* filename, size_t size, FileHan
|
||||
}
|
||||
|
||||
gciFileHeader->swapEndian();
|
||||
m_files.push_back({*gciFileHeader, fileSize, reinterpret_cast<const char8_t*>(gciFilename.c_str()), false}); // push non-endian swapped header first
|
||||
// push non-endian swapped header first
|
||||
m_files.push_back({*gciFileHeader, fileSize,
|
||||
std::u8string(gciFilename.begin(), gciFilename.end()), false});
|
||||
handleOut = FileHandle(m_files.size() - 1, 0);
|
||||
|
||||
return ECardResult::READY;
|
||||
|
||||
@@ -175,7 +175,7 @@ void CARDInit(const char* game, const char* maker) {
|
||||
|
||||
std::filesystem::path cardWorkingDir;
|
||||
if (aurora::g_config.userPath != nullptr)
|
||||
cardWorkingDir = reinterpret_cast<const char8_t*>(aurora::g_config.userPath);
|
||||
cardWorkingDir = fs_path_from_string(aurora::g_config.userPath);
|
||||
else
|
||||
cardWorkingDir = std::filesystem::current_path();
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "../../fs_helper.hpp"
|
||||
#include "../../input.hpp"
|
||||
#include "../../internal.hpp"
|
||||
#include <dolphin/pad.h>
|
||||
@@ -5,6 +6,7 @@
|
||||
#include <SDL3/SDL_mouse.h>
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <sys/stat.h>
|
||||
#include <ranges>
|
||||
|
||||
@@ -283,7 +285,7 @@ constexpr PADCLampRegion ClampRegion{
|
||||
|
||||
bool g_initialized;
|
||||
bool g_keyboardBindingsLoaded = false;
|
||||
bool g_blockPAD = false;
|
||||
std::atomic_bool g_blockPAD{false};
|
||||
bool g_suppressHeldOnRead = false;
|
||||
std::array<PADButton, PAD_CHANMAX> g_suppressedButtons{};
|
||||
std::array<bool, PAD_CHANMAX> g_suppressLeftTrigger{};
|
||||
@@ -491,7 +493,7 @@ void __PADLoadMapping(aurora::input::GameController* controller) /* NOLINT(*-re
|
||||
return;
|
||||
}
|
||||
|
||||
std::string basePath{aurora::g_config.userPath};
|
||||
const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath);
|
||||
if (!controller->m_mappingLoaded) {
|
||||
__PADSetDefaultMapping(controller);
|
||||
controller->m_axisMapping = g_defaultAxes;
|
||||
@@ -499,8 +501,9 @@ void __PADLoadMapping(aurora::input::GameController* controller) /* NOLINT(*-re
|
||||
|
||||
controller->m_mappingLoaded = true;
|
||||
|
||||
const auto path = fmt::format("{}/{}_{:04X}_{:04X}.controller", basePath, PADGetName(playerIndex), controller->m_vid,
|
||||
controller->m_pid);
|
||||
const auto path = fs_path_to_string(
|
||||
basePath / fmt::format("{}_{:04X}_{:04X}.controller", PADGetName(playerIndex), controller->m_vid,
|
||||
controller->m_pid));
|
||||
SDL_IOStream* file = SDL_IOFromFile(path.c_str(), "rb");
|
||||
if (file == nullptr) {
|
||||
return;
|
||||
@@ -659,7 +662,8 @@ u32 PADRead(PADStatus* status) {
|
||||
|
||||
int numKeys = 0;
|
||||
const bool* kbState = SDL_GetKeyboardState(&numKeys);
|
||||
const bool captureHeldInput = g_suppressHeldOnRead && !g_blockPAD;
|
||||
const bool inputBlocked = g_blockPAD.load(std::memory_order_acquire);
|
||||
const bool captureHeldInput = g_suppressHeldOnRead && !inputBlocked;
|
||||
g_suppressHeldOnRead = false;
|
||||
|
||||
uint32_t rumbleSupport = 0;
|
||||
@@ -882,7 +886,7 @@ u32 PADRead(PADStatus* status) {
|
||||
}
|
||||
}
|
||||
|
||||
if (g_blockPAD) {
|
||||
if (inputBlocked) {
|
||||
neutralize_status(status[i]);
|
||||
} else {
|
||||
apply_unblock_suppression(status[i], i, captureHeldInput);
|
||||
@@ -1247,8 +1251,8 @@ constexpr uint32_t k_keyboardMagic = SBIG('KBND');
|
||||
constexpr int32_t k_keyboardVersion = 3;
|
||||
|
||||
static void load_keyboard_bindings() {
|
||||
const auto filePath = std::filesystem::path{aurora::g_config.userPath} / "keyboard_bindings.dat";
|
||||
SDL_IOStream* file = SDL_IOFromFile(filePath.string().c_str(), "rb");
|
||||
const auto filePath = fs_path_from_string(aurora::g_config.userPath) / "keyboard_bindings.dat";
|
||||
SDL_IOStream* file = SDL_IOFromFile(fs_path_to_string(filePath).c_str(), "rb");
|
||||
if (file == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -1317,10 +1321,11 @@ static void load_keyboard_bindings() {
|
||||
}
|
||||
|
||||
static void save_keyboard_bindings() {
|
||||
const auto filePath = std::filesystem::path{aurora::g_config.userPath} / "keyboard_bindings.dat";
|
||||
SDL_IOStream* file = SDL_IOFromFile(filePath.string().c_str(), "wb");
|
||||
const auto filePath = fs_path_from_string(aurora::g_config.userPath) / "keyboard_bindings.dat";
|
||||
const auto filePathStr = fs_path_to_string(filePath);
|
||||
SDL_IOStream* file = SDL_IOFromFile(filePathStr.c_str(), "wb");
|
||||
if (file == nullptr) {
|
||||
aurora::input::Log.warn("save_keyboard_bindings: failed to open {} for writing", filePath.string());
|
||||
aurora::input::Log.warn("save_keyboard_bindings: failed to open {} for writing", filePathStr);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1344,14 +1349,14 @@ void __PADWriteDeadZones(SDL_IOStream* file, // NOLINT(*-reserved-identifier)
|
||||
}
|
||||
|
||||
void PADSerializeMappings() {
|
||||
const std::filesystem::path basePath{aurora::g_config.userPath};
|
||||
const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath);
|
||||
|
||||
for (auto& controller : aurora::input::g_GameControllers | std::views::values) {
|
||||
EnsureMappingLoaded(&controller);
|
||||
const auto filePath =
|
||||
basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index),
|
||||
controller.m_vid, controller.m_pid);
|
||||
std::string filePathStr = filePath.string();
|
||||
std::string filePathStr = fs_path_to_string(filePath);
|
||||
|
||||
// don't truncate the file if it already exists
|
||||
const char* openMode = std::filesystem::exists(filePath) ? "r+b" : "wb";
|
||||
@@ -1370,7 +1375,7 @@ void PADSerializeMappings() {
|
||||
// start writing data at next 32-byte aligned offset
|
||||
const int64_t dataStart = SDL_TellIO(file) + 31 & ~31;
|
||||
if (dataStart == -1) {
|
||||
aurora::input::Log.warn("Unable to seek in controller bindings! Path: \"{}\"", filePath.string());
|
||||
aurora::input::Log.warn("Unable to seek in controller bindings! Path: \"{}\"", filePathStr);
|
||||
return;
|
||||
}
|
||||
SDL_SeekIO(file, dataStart, SDL_IO_SEEK_SET);
|
||||
@@ -1530,12 +1535,13 @@ void PADRestoreDefaultMapping(const u32 port) {
|
||||
}
|
||||
|
||||
void PADBlockInput(const bool block) {
|
||||
if (g_blockPAD && !block) {
|
||||
if (g_blockPAD.exchange(block, std::memory_order_acq_rel) && !block) {
|
||||
g_suppressHeldOnRead = true;
|
||||
}
|
||||
g_blockPAD = block;
|
||||
}
|
||||
|
||||
bool PADIsInputBlocked() { return g_blockPAD.load(std::memory_order_acquire); }
|
||||
|
||||
SDL_Gamepad* PADGetSDLGamepadForIndex(const u32 index) {
|
||||
const auto* ctrl = __PADGetControllerForIndex(index);
|
||||
if (ctrl == nullptr) {
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
/**
|
||||
* Converts a std::filesystem::path to a std::string, UTF-8, without exploding on Windows.
|
||||
* Narrow path strings crossing the aurora boundary are UTF-8. path::string() and the
|
||||
* char path constructor go through the ANSI codepage on Windows, so they must not be
|
||||
* used for anything the host handed us or hands back to SDL, sqlite or ImGui.
|
||||
*/
|
||||
inline std::string fs_path_to_string(const std::filesystem::path& path) {
|
||||
const auto u8str = path.u8string();
|
||||
return { reinterpret_cast<const char*>(u8str.c_str()) };
|
||||
return { reinterpret_cast<const char*>(u8str.c_str()), u8str.size() };
|
||||
}
|
||||
|
||||
inline std::filesystem::path fs_path_from_string(std::string_view utf8) {
|
||||
return std::filesystem::path(std::u8string(utf8.begin(), utf8.end()));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "clear.hpp"
|
||||
#include "../gx/pipeline.hpp"
|
||||
#include "../fs_helper.hpp"
|
||||
#include "../sqlite_utils.hpp"
|
||||
#include "../webgpu/gpu.hpp"
|
||||
|
||||
@@ -715,7 +716,7 @@ static bool prepare_pipeline_cache_db() {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto path = (std::filesystem::path{g_config.pipelineCachePath} / "pipeline_cache.db").string();
|
||||
const auto path = fs_path_to_string(fs_path_from_string(g_config.pipelineCachePath) / "pipeline_cache.db");
|
||||
auto ret = sqlite3_open(path.c_str(), &g_pipelineCacheDb);
|
||||
if (ret != SQLITE_OK) {
|
||||
Log.error("Failed to open pipeline cache database: {}", sqlite3_errmsg(g_pipelineCacheDb));
|
||||
|
||||
@@ -506,8 +506,8 @@ void build_index() noexcept {
|
||||
return;
|
||||
}
|
||||
|
||||
auto userPath = std::filesystem::path{reinterpret_cast<const char8_t*>(g_config.userPath)};
|
||||
auto cachePath = std::filesystem::path{reinterpret_cast<const char8_t*>(g_config.cachePath)};
|
||||
auto userPath = fs_path_from_string(g_config.userPath);
|
||||
auto cachePath = fs_path_from_string(g_config.cachePath);
|
||||
|
||||
s_replacementRoot = userPath / "texture_replacements";
|
||||
s_dumpRoot = cachePath / "texture_dumps";
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <SDL3/SDL_render.h>
|
||||
|
||||
#include "fs_helper.hpp"
|
||||
#include "internal.hpp"
|
||||
#include "webgpu/gpu.hpp"
|
||||
#include "window.hpp"
|
||||
@@ -37,7 +38,7 @@ void remove_legacy_ini_file(const char* basePath) noexcept {
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(std::filesystem::path{basePath} / "imgui.ini", ec);
|
||||
std::filesystem::remove(fs_path_from_string(basePath) / "imgui.ini", ec);
|
||||
}
|
||||
|
||||
void create_context() noexcept {
|
||||
|
||||
@@ -255,6 +255,18 @@ IdentityMatch identity_match(const ControllerIdentity& saved, const ControllerId
|
||||
: IdentityMatch::None;
|
||||
}
|
||||
|
||||
void assign_player_index(GameController& controller, int32_t port) {
|
||||
SDL_SetGamepadPlayerIndex(controller.m_controller, port);
|
||||
controller.m_playerIndex = port;
|
||||
}
|
||||
|
||||
// SDL forgets the index for devices mapped after connect, so player_index() falls
|
||||
// back to the cached copy; both have to move together or a port looks doubly taken.
|
||||
int32_t effective_player_index(const GameController& controller) {
|
||||
const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller);
|
||||
return player >= 0 ? player : controller.m_playerIndex;
|
||||
}
|
||||
|
||||
bool is_instance_claimed(const std::array<Uint32, PAD_MAX_CONTROLLERS>& claimedControllers, size_t claimedCount,
|
||||
Uint32 instance) {
|
||||
return std::find(claimedControllers.begin(), claimedControllers.begin() + claimedCount, instance) !=
|
||||
@@ -269,10 +281,10 @@ void apply_port_preferences() noexcept {
|
||||
}
|
||||
|
||||
for (auto& [instance, controller] : g_GameControllers) {
|
||||
const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller);
|
||||
const int32_t player = effective_player_index(controller);
|
||||
if (player >= 0 && player < PAD_MAX_CONTROLLERS && g_portPreferences[player].state != PortPreferenceState::Unset) {
|
||||
// Keep SDL's default player assignment from taking explicitly configured ports
|
||||
SDL_SetGamepadPlayerIndex(controller.m_controller, -1);
|
||||
assign_player_index(controller, -1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +305,7 @@ void apply_port_preferences() noexcept {
|
||||
|
||||
switch (identity_match(preference.identity, controller_identity(controller))) {
|
||||
case IdentityMatch::Exact:
|
||||
SDL_SetGamepadPlayerIndex(controller.m_controller, static_cast<int32_t>(port));
|
||||
assign_player_index(controller, static_cast<int32_t>(port));
|
||||
claimedControllers[claimedCount++] = instance;
|
||||
fallbackController = nullptr;
|
||||
break;
|
||||
@@ -311,11 +323,18 @@ void apply_port_preferences() noexcept {
|
||||
}
|
||||
|
||||
if (fallbackController != nullptr) {
|
||||
SDL_SetGamepadPlayerIndex(fallbackController->m_controller, static_cast<int32_t>(port));
|
||||
assign_player_index(*fallbackController, static_cast<int32_t>(port));
|
||||
claimedControllers[claimedCount++] = fallbackInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ports are explicit assignments. SDL may choose a player index at connection
|
||||
// time, but accepting it would make a newly connected controller silently take
|
||||
// over a game port before the user assigns it in the controller menu.
|
||||
void ensure_player_index(GameController& controller) noexcept {
|
||||
assign_player_index(controller, -1);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
GameController* get_controller_for_player(uint32_t player) noexcept {
|
||||
@@ -364,6 +383,7 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
|
||||
controller.m_hasRgbLed = SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false);
|
||||
SDL_JoystickID instance = SDL_GetJoystickID(SDL_GetGamepadJoystick(ctrl));
|
||||
g_GameControllers[instance] = controller;
|
||||
ensure_player_index(g_GameControllers[instance]);
|
||||
apply_port_preferences();
|
||||
return instance;
|
||||
}
|
||||
@@ -371,6 +391,19 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool refresh_controller(SDL_JoystickID instance) noexcept {
|
||||
const auto it = g_GameControllers.find(instance);
|
||||
if (it == g_GameControllers.end()) {
|
||||
return false;
|
||||
}
|
||||
// The SDL mapping changed underneath us; drop the cached PAD bindings so they
|
||||
// are rebuilt from the new one.
|
||||
it->second.m_mappingLoaded = false;
|
||||
ensure_player_index(it->second);
|
||||
apply_port_preferences();
|
||||
return true;
|
||||
}
|
||||
|
||||
void remove_controller(Uint32 instance) noexcept {
|
||||
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
|
||||
SDL_CloseGamepad(it->second.m_controller);
|
||||
|
||||
@@ -51,6 +51,7 @@ struct GameController {
|
||||
GameController* get_controller_for_player(uint32_t player) noexcept;
|
||||
Sint32 get_instance_for_player(uint32_t player) noexcept;
|
||||
SDL_JoystickID add_controller(SDL_JoystickID which) noexcept;
|
||||
bool refresh_controller(SDL_JoystickID instance) noexcept;
|
||||
void remove_controller(Uint32 instance) noexcept;
|
||||
Sint32 player_index(Uint32 instance) noexcept;
|
||||
void set_player_index(Uint32 instance, Sint32 index) noexcept;
|
||||
|
||||
@@ -137,7 +137,7 @@ static void prune_stale_rows() {
|
||||
static bool cache_init_core() {
|
||||
Log.debug("SQLite version {}", sqlite3_libversion());
|
||||
|
||||
const auto path = std::filesystem::path{reinterpret_cast<const char8_t*>(g_config.cachePath)} / "dawn_cache.db";
|
||||
const auto path = fs_path_from_string(g_config.cachePath) / "dawn_cache.db";
|
||||
std::string file = fs_path_to_string(path);
|
||||
Log.debug("Using dawn cache at {}", file);
|
||||
auto ret = sqlite3_open(file.c_str(), &db);
|
||||
@@ -165,8 +165,12 @@ static bool cache_init_core() {
|
||||
db = nullptr;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(path, ec);
|
||||
std::filesystem::remove(std::filesystem::path{file + "-wal"}, ec);
|
||||
std::filesystem::remove(std::filesystem::path{file + "-shm"}, ec);
|
||||
auto wal = path;
|
||||
wal += "-wal";
|
||||
std::filesystem::remove(wal, ec);
|
||||
auto shm = path;
|
||||
shm += "-shm";
|
||||
std::filesystem::remove(shm, ec);
|
||||
ret = sqlite3_open(file.c_str(), &db);
|
||||
if (ret != SQLITE_OK) {
|
||||
Log.error("Failed to recreate database: {}", sqlite3_errmsg(db));
|
||||
|
||||
@@ -294,6 +294,15 @@ void process_event(SDL_Event& event) {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_GAMEPAD_REMAPPED: {
|
||||
if (input::refresh_controller(event.gdevice.which)) {
|
||||
g_events.push_back(AuroraEvent{
|
||||
.type = AURORA_CONTROLLER_ADDED,
|
||||
.controller = event.gdevice.which,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_GAMEPAD_REMOVED: {
|
||||
input::remove_controller(event.gdevice.which);
|
||||
g_events.push_back(AuroraEvent{
|
||||
|
||||
@@ -205,7 +205,7 @@ function(mkw_configure_product target)
|
||||
endif()
|
||||
|
||||
target_link_libraries(${target} PRIVATE
|
||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
|
||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp setupapi winusb)
|
||||
|
||||
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
foreach(runtime_dll libc++.dll libunwind.dll)
|
||||
|
||||
@@ -81,7 +81,8 @@ inline bool WriteSerial(const std::filesystem::path& path, const std::string& se
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::filesystem::path temporary = path.string() + ".tmp";
|
||||
std::filesystem::path temporary = path;
|
||||
temporary += ".tmp";
|
||||
{
|
||||
std::ofstream output(temporary, std::ios::trunc);
|
||||
if (!output) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_events.h>
|
||||
|
||||
// Press-to-bind setup for joysticks SDL either doesn't recognize as gamepads or
|
||||
// recognizes with a mapping that lacks the analog stick (e.g. raphnet adapters).
|
||||
// The wizard produces a standard SDL gamepad mapping, applies it live, and
|
||||
// persists it to gamecontrollerdb.txt in the user data directory.
|
||||
namespace controller_mapping_wizard {
|
||||
|
||||
void LoadPersistedMappings();
|
||||
void HandleSdlEvent(const SDL_Event& event);
|
||||
|
||||
// Lists devices that need setup inside the controller settings menu.
|
||||
void DrawSetupList();
|
||||
// Draws the wizard window when active; call once per overlay frame.
|
||||
void Draw();
|
||||
|
||||
bool IsActive();
|
||||
|
||||
} // namespace controller_mapping_wizard
|
||||
@@ -20,14 +20,14 @@
|
||||
namespace DvdFstContract {
|
||||
|
||||
struct RegisteredFile {
|
||||
std::string hostPath;
|
||||
std::filesystem::path hostPath;
|
||||
std::string dvdPath;
|
||||
uint32_t size = 0;
|
||||
uint32_t discOffsetWords = 0;
|
||||
};
|
||||
|
||||
struct IndexedEntry {
|
||||
std::string hostPath;
|
||||
std::filesystem::path hostPath;
|
||||
std::string dvdPath;
|
||||
uint32_t size = 0;
|
||||
uint32_t discOffsetWords = 0;
|
||||
|
||||
@@ -27,13 +27,14 @@ inline std::optional<std::filesystem::path> ExistingDirectory(const std::filesys
|
||||
if (path.empty()) {
|
||||
RT_LOGF(RT_TAG_NAND, "ERROR: %s\n", message);
|
||||
} else {
|
||||
RT_LOGF(RT_TAG_NAND, "ERROR: %s: %s\n", message, path.string().c_str());
|
||||
RT_LOGF(RT_TAG_NAND, "ERROR: %s: %s\n", message,
|
||||
RuntimeConfigFile::PathToUtf8(path).c_str());
|
||||
}
|
||||
RT_LOGF(RT_TAG_NAND, "Set [paths] nand_root in Config.toml.\n");
|
||||
std::string details = message ? message : "The configured NAND could not be initialized.";
|
||||
if (!path.empty()) {
|
||||
details += "\n\nPath: ";
|
||||
details += path.string();
|
||||
details += RuntimeConfigFile::PathToUtf8(path);
|
||||
}
|
||||
details += "\n\nSet [paths] nand_root in Config.toml and try again.";
|
||||
// Same fatal idiom as the DVD and OS paths: crash artifacts first so the run
|
||||
@@ -51,18 +52,6 @@ inline std::filesystem::path ResolveConfiguredPath(const std::string& value) {
|
||||
return RuntimeConfigFile::ResolveRelativeToConfig(value);
|
||||
}
|
||||
|
||||
inline std::string PathStringWithoutTrailingSeparators(std::filesystem::path path) {
|
||||
std::string text = path.string();
|
||||
while (!text.empty()) {
|
||||
const char tail = text.back();
|
||||
if (tail != '\\' && tail != '/') {
|
||||
break;
|
||||
}
|
||||
text.pop_back();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
inline std::filesystem::path ManagedNandRootPath() {
|
||||
return RuntimeConfigFile::ApplicationDataDirectory() / "NAND";
|
||||
}
|
||||
@@ -134,7 +123,9 @@ inline bool SeedMissingBootstrapFiles(const std::filesystem::path& root) {
|
||||
const std::filesystem::path relativePath{std::string(file)};
|
||||
ec.clear();
|
||||
if (!CopyBootstrapFile(*payload, root, relativePath, ec)) {
|
||||
RT_LOG(RT_TAG_NAND) << "could not create " << (root / relativePath).string() << std::endl;
|
||||
RT_LOG(RT_TAG_NAND) << "could not create "
|
||||
<< RuntimeConfigFile::PathToUtf8(root / relativePath)
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -167,7 +158,8 @@ inline std::filesystem::path CreateManagedNandRoot() {
|
||||
}
|
||||
}
|
||||
|
||||
RT_LOG(RT_TAG_NAND) << "using managed NAND root: " << root.string() << std::endl;
|
||||
RT_LOG(RT_TAG_NAND) << "using managed NAND root: " << RuntimeConfigFile::PathToUtf8(root)
|
||||
<< std::endl;
|
||||
return root;
|
||||
}
|
||||
|
||||
@@ -187,8 +179,4 @@ inline std::filesystem::path DiscoverNandRootPath() {
|
||||
return CreateManagedNandRoot();
|
||||
}
|
||||
|
||||
inline std::string DiscoverNandRootString() {
|
||||
return PathStringWithoutTrailingSeparators(DiscoverNandRootPath());
|
||||
}
|
||||
|
||||
} // namespace RuntimeNandPath
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -67,8 +68,10 @@ void RunMemoryInitializers();
|
||||
void RegisterPostRelInitializer(InitializerFn fn);
|
||||
void RunPostRelInitializers();
|
||||
|
||||
// The generated call site passes a UTF-8 literal; it is decoded once here and
|
||||
// stays a path from then on.
|
||||
void RegisterDvdOverlayRoot(std::string root);
|
||||
const std::vector<std::string>& DvdOverlayRoots();
|
||||
const std::vector<std::filesystem::path>& DvdOverlayRoots();
|
||||
|
||||
// Riivolution settings pinned by the distribution's recomp.yml. The XML path is
|
||||
// relative to the pack/overlay root; option selections use Riivolution's 1-based
|
||||
|
||||
@@ -60,10 +60,25 @@ struct RuntimeUserConfig {
|
||||
// comma-separated SDL-style physical button names ("south", or
|
||||
// "dpad_up,left_shoulder") as values; pressing either bound button counts.
|
||||
std::array<std::optional<std::string>, 12> controllerButtons;
|
||||
// One-based physical WUP-028 adapter port assigned to each game port.
|
||||
// Zero or a missing value means the adapter does not own that game port.
|
||||
std::array<uint32_t, 4> gameCubeAdapterPorts{};
|
||||
};
|
||||
|
||||
namespace RuntimeConfigFile {
|
||||
|
||||
// Narrow path strings are UTF-8 everywhere in the runtime; string() and the
|
||||
// char path constructor would use the ANSI codepage on Windows, which drops
|
||||
// characters the codepage cannot represent.
|
||||
inline std::string PathToUtf8(const std::filesystem::path& path) {
|
||||
const std::u8string text = path.u8string();
|
||||
return std::string(text.begin(), text.end());
|
||||
}
|
||||
|
||||
inline std::filesystem::path PathFromUtf8(std::string_view text) {
|
||||
return std::filesystem::path(std::u8string(text.begin(), text.end()));
|
||||
}
|
||||
|
||||
inline constexpr const char* kConfigFileName = "Config.toml";
|
||||
inline constexpr const char* kApplicationDirectoryName = "WiiCompiled";
|
||||
|
||||
@@ -323,6 +338,12 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
config.controllerButtons[index] =
|
||||
FindConfigValue<std::string>(document, "controller", buttonKeys[index]);
|
||||
}
|
||||
for (size_t index = 0; index < config.gameCubeAdapterPorts.size(); ++index) {
|
||||
const std::string key = "adapter_port_" + std::to_string(index + 1);
|
||||
if (auto value = FindConfigUint(document, "controller", key); value && *value <= 4) {
|
||||
config.gameCubeAdapterPorts[index] = *value;
|
||||
}
|
||||
}
|
||||
|
||||
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
|
||||
config.windowPosX = FindConfigInt(document, "video", "window_x");
|
||||
@@ -410,7 +431,7 @@ inline RuntimeUserConfig ParseConfig(std::istream& input, std::string sourceName
|
||||
inline RuntimeUserConfig LoadConfigFile() {
|
||||
EnsureConfigFile();
|
||||
std::ifstream file(ResolveConfigPath(), std::ios::binary);
|
||||
return file ? ParseConfig(file, ResolveConfigPath().string()) : RuntimeUserConfig{};
|
||||
return file ? ParseConfig(file, PathToUtf8(ResolveConfigPath())) : RuntimeUserConfig{};
|
||||
}
|
||||
|
||||
inline const RuntimeUserConfig& Get() {
|
||||
@@ -501,7 +522,7 @@ inline bool WriteSetting(std::string_view section, std::string_view key, std::st
|
||||
}
|
||||
std::ofstream output(path, std::ios::trunc);
|
||||
if (!output) {
|
||||
std::cerr << "[runtime-config] Unable to write " << path.string() << std::endl;
|
||||
std::cerr << "[runtime-config] Unable to write " << PathToUtf8(path) << std::endl;
|
||||
return false;
|
||||
}
|
||||
for (const auto& outputLine : lines) {
|
||||
@@ -586,6 +607,19 @@ inline bool SetControllerButton(size_t index, std::string value) {
|
||||
return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value));
|
||||
}
|
||||
|
||||
inline int GameCubeAdapterPort(size_t gamePort) {
|
||||
if (gamePort >= Get().gameCubeAdapterPorts.size()) return -1;
|
||||
const uint32_t physicalPort = Get().gameCubeAdapterPorts[gamePort];
|
||||
return physicalPort >= 1 && physicalPort <= 4 ? static_cast<int>(physicalPort - 1) : -1;
|
||||
}
|
||||
|
||||
inline bool SetGameCubeAdapterPort(size_t gamePort, int physicalPort) {
|
||||
if (gamePort >= Mutable().gameCubeAdapterPorts.size() || physicalPort < -1 || physicalPort >= 4) return false;
|
||||
const uint32_t storedPort = physicalPort < 0 ? 0u : static_cast<uint32_t>(physicalPort + 1);
|
||||
Mutable().gameCubeAdapterPorts[gamePort] = storedPort;
|
||||
return WriteSetting("controller", "adapter_port_" + std::to_string(gamePort + 1), std::to_string(storedPort));
|
||||
}
|
||||
|
||||
inline bool SetAudioVolume(float value) {
|
||||
value = std::clamp(value, 0.0f, 1.0f);
|
||||
Mutable().audioVolume = value;
|
||||
@@ -754,7 +788,7 @@ inline std::string DvdRoot(std::string fallback = "") {
|
||||
// never to the process working directory (docs/WHEELWIZARD_CONTRACT.md).
|
||||
inline std::filesystem::path ResolveRelativeTo(const std::filesystem::path& base,
|
||||
const std::string& value) {
|
||||
std::filesystem::path path(value);
|
||||
std::filesystem::path path = PathFromUtf8(value);
|
||||
if (path.is_relative()) {
|
||||
path = base / path;
|
||||
}
|
||||
@@ -784,7 +818,7 @@ inline void LogLoadedConfig() {
|
||||
static const bool logged = [] {
|
||||
const auto& config = Get();
|
||||
const auto configPath = ResolveConfigPath();
|
||||
std::cout << "[runtime-config] " << configPath.string();
|
||||
std::cout << "[runtime-config] " << PathToUtf8(configPath);
|
||||
if (!std::filesystem::exists(configPath)) {
|
||||
std::cout << " not found; using built-in defaults";
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
@@ -72,5 +73,5 @@ public:
|
||||
// `mem1Path` and MEM2 to `mem1Path + ".mem2"`, logging outcomes to `os`.
|
||||
static void DumpCrashHeuristics(std::ostream& os, const struct CpuContext* cpu,
|
||||
const uint32_t* missingGuestTarget);
|
||||
static void WriteGuestMemorySnapshot(std::ostream& os, const char* mem1Path);
|
||||
static void WriteGuestMemorySnapshot(std::ostream& os, const std::filesystem::path& mem1Path);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
struct PADStatus;
|
||||
|
||||
namespace Wup028Adapter {
|
||||
|
||||
enum class ConnectionState : uint8_t {
|
||||
Searching,
|
||||
DriverError,
|
||||
Connected,
|
||||
};
|
||||
|
||||
struct AdapterInfo {
|
||||
ConnectionState state = ConnectionState::Searching;
|
||||
std::string deviceName;
|
||||
std::string detail;
|
||||
float pollRateHz = 0.0f;
|
||||
uint8_t inputEndpoint = 0;
|
||||
uint8_t outputEndpoint = 0;
|
||||
std::array<bool, 4> ports{};
|
||||
// Raw adapter type/status byte. High nibble 1 is wired, 2 is wireless.
|
||||
std::array<uint8_t, 4> portStatus{};
|
||||
std::array<uint64_t, 4> portChangeSequence{};
|
||||
};
|
||||
|
||||
// Starts the project-owned WUP-028 worker. Safe to call more than once.
|
||||
void Initialize();
|
||||
void Shutdown();
|
||||
|
||||
// Returns true while an official adapter is open. Each entry is a native
|
||||
// GameCube port; an empty port is represented by PAD_ERR_NO_CONTROLLER.
|
||||
bool Read(std::array<PADStatus, 4>& statuses);
|
||||
// Assigns a physical adapter port to a game port. Pass -1 to leave the game
|
||||
// port under Aurora's normal controller assignment.
|
||||
void SetPortAssignment(uint32_t gamePort, int physicalPort);
|
||||
int GetPortAssignment(uint32_t gamePort);
|
||||
// Returns true when the command belongs to an active WUP-028, allowing the
|
||||
// caller to avoid also sending it to Aurora's unrelated controller backend.
|
||||
bool SetRumble(uint32_t port, bool enabled);
|
||||
AdapterInfo GetInfo();
|
||||
|
||||
} // namespace Wup028Adapter
|
||||
@@ -0,0 +1,477 @@
|
||||
#include "controller_mapping_wizard.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <SDL3/SDL_joystick.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace controller_mapping_wizard {
|
||||
namespace {
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
constexpr int16_t kStickThreshold = 16000;
|
||||
constexpr int16_t kTriggerThreshold = 10000;
|
||||
constexpr auto kCaptureDebounce = std::chrono::milliseconds(350);
|
||||
|
||||
enum class StepKind {
|
||||
Button, // button or single-direction hat press
|
||||
Trigger, // button press or axis pull
|
||||
Stick, // axis motion in the prompted direction
|
||||
};
|
||||
|
||||
struct Step {
|
||||
const char* mappingKey;
|
||||
const char* prompt;
|
||||
StepKind kind;
|
||||
};
|
||||
|
||||
// Prompts describe what the control does in-game; the SDL fields land on the
|
||||
// right GC controls through pad.cpp's "standard" defaults (Z lives on
|
||||
// rightshoulder, L/R on the trigger axes).
|
||||
constexpr std::array<Step, 16> kSteps = {{
|
||||
{"a", "Press the button for A (accelerate / select)", StepKind::Button},
|
||||
{"b", "Press the button for B (brake / back)", StepKind::Button},
|
||||
{"x", "Press the button for X", StepKind::Button},
|
||||
{"y", "Press the button for Y", StepKind::Button},
|
||||
{"start", "Press the button for pause (Start)", StepKind::Button},
|
||||
{"rightshoulder", "Press the button for rear view (Z)", StepKind::Button},
|
||||
{"lefttrigger", "Press or pull the control for using items (L)", StepKind::Trigger},
|
||||
{"righttrigger", "Press or pull the control for hop / drift (R)", StepKind::Trigger},
|
||||
{"dpup", "Press D-pad Up", StepKind::Button},
|
||||
{"dpdown", "Press D-pad Down", StepKind::Button},
|
||||
{"dpleft", "Press D-pad Left", StepKind::Button},
|
||||
{"dpright", "Press D-pad Right", StepKind::Button},
|
||||
{"leftx", "Move the Control Stick LEFT", StepKind::Stick},
|
||||
{"lefty", "Move the Control Stick UP", StepKind::Stick},
|
||||
{"rightx", "Move the C-Stick LEFT (or Skip)", StepKind::Stick},
|
||||
{"righty", "Move the C-Stick UP (or Skip)", StepKind::Stick},
|
||||
}};
|
||||
|
||||
struct WizardState {
|
||||
bool active = false;
|
||||
SDL_JoystickID instance = 0;
|
||||
SDL_Joystick* joystick = nullptr;
|
||||
bool ownsJoystick = false;
|
||||
std::string deviceName;
|
||||
size_t stepIndex = 0;
|
||||
std::array<std::optional<std::string>, kSteps.size()> bindings{};
|
||||
std::vector<int16_t> axisBaseline;
|
||||
Clock::time_point acceptAfter{};
|
||||
std::string status;
|
||||
};
|
||||
|
||||
WizardState g_wizard;
|
||||
|
||||
std::filesystem::path MappingDbPath() {
|
||||
return RuntimeConfigFile::ApplicationDataDirectory() / "gamecontrollerdb.txt";
|
||||
}
|
||||
|
||||
std::string GuidString(SDL_JoystickID instance) {
|
||||
char buf[33] = {};
|
||||
SDL_GUIDToString(SDL_GetJoystickGUIDForID(instance), buf, sizeof(buf));
|
||||
return buf;
|
||||
}
|
||||
|
||||
bool BindingUsed(const std::string& value) {
|
||||
return std::any_of(g_wizard.bindings.begin(), g_wizard.bindings.end(),
|
||||
[&](const std::optional<std::string>& b) { return b && *b == value; });
|
||||
}
|
||||
|
||||
void SnapshotAxes() {
|
||||
g_wizard.axisBaseline.clear();
|
||||
const int axes = SDL_GetNumJoystickAxes(g_wizard.joystick);
|
||||
for (int i = 0; i < axes; ++i) {
|
||||
g_wizard.axisBaseline.push_back(SDL_GetJoystickAxis(g_wizard.joystick, i));
|
||||
}
|
||||
}
|
||||
|
||||
void AdvanceStep(std::optional<std::string> value) {
|
||||
g_wizard.bindings[g_wizard.stepIndex] = std::move(value);
|
||||
++g_wizard.stepIndex;
|
||||
g_wizard.acceptAfter = Clock::now() + kCaptureDebounce;
|
||||
SnapshotAxes();
|
||||
}
|
||||
|
||||
void StopWizard() {
|
||||
if (g_wizard.ownsJoystick && g_wizard.joystick != nullptr) {
|
||||
SDL_CloseJoystick(g_wizard.joystick);
|
||||
}
|
||||
g_wizard = WizardState{};
|
||||
}
|
||||
|
||||
void StartWizard(SDL_JoystickID instance) {
|
||||
StopWizard();
|
||||
SDL_Joystick* joystick = nullptr;
|
||||
bool owns = false;
|
||||
if (SDL_Gamepad* gamepad = SDL_GetGamepadFromID(instance)) {
|
||||
joystick = SDL_GetGamepadJoystick(gamepad);
|
||||
} else {
|
||||
joystick = SDL_OpenJoystick(instance);
|
||||
owns = true;
|
||||
}
|
||||
if (joystick == nullptr) {
|
||||
RT_LOG(RT_TAG_CONFIG) << "controller wizard: failed to open joystick " << instance << ": "
|
||||
<< SDL_GetError() << std::endl;
|
||||
return;
|
||||
}
|
||||
g_wizard.active = true;
|
||||
g_wizard.instance = instance;
|
||||
g_wizard.joystick = joystick;
|
||||
g_wizard.ownsJoystick = owns;
|
||||
const char* name = SDL_GetJoystickNameForID(instance);
|
||||
g_wizard.deviceName = name != nullptr ? name : "Controller";
|
||||
g_wizard.acceptAfter = Clock::now() + kCaptureDebounce;
|
||||
SnapshotAxes();
|
||||
}
|
||||
|
||||
std::string BuildMappingString() {
|
||||
std::string name = g_wizard.deviceName;
|
||||
std::replace(name.begin(), name.end(), ',', ' ');
|
||||
std::string mapping = GuidString(g_wizard.instance) + "," + name + ",";
|
||||
for (size_t i = 0; i < kSteps.size(); ++i) {
|
||||
if (g_wizard.bindings[i]) {
|
||||
mapping += std::string(kSteps[i].mappingKey) + ":" + *g_wizard.bindings[i] + ",";
|
||||
}
|
||||
}
|
||||
mapping += "platform:Windows,";
|
||||
return mapping;
|
||||
}
|
||||
|
||||
bool PersistMapping(const std::string& guid, const std::string& mapping) {
|
||||
const std::filesystem::path path = MappingDbPath();
|
||||
std::vector<std::string> lines;
|
||||
{
|
||||
std::ifstream in(path);
|
||||
std::string line;
|
||||
while (std::getline(in, line)) {
|
||||
if (line.rfind(guid + ",", 0) != 0) {
|
||||
lines.push_back(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.push_back(mapping);
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
std::ofstream out(path, std::ios::trunc);
|
||||
if (!out) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& line : lines) {
|
||||
out << line << '\n';
|
||||
}
|
||||
out.close();
|
||||
return static_cast<bool>(out);
|
||||
}
|
||||
|
||||
void FinishWizard() {
|
||||
const std::string guid = GuidString(g_wizard.instance);
|
||||
const std::string mapping = BuildMappingString();
|
||||
if (SDL_AddGamepadMapping(mapping.c_str()) < 0) {
|
||||
g_wizard.status = std::string("Failed to apply mapping: ") + SDL_GetError();
|
||||
RT_LOG(RT_TAG_CONFIG) << "controller wizard: " << g_wizard.status << " (" << mapping << ")"
|
||||
<< std::endl;
|
||||
return;
|
||||
}
|
||||
if (!PersistMapping(guid, mapping)) {
|
||||
g_wizard.status =
|
||||
"Failed to save mapping to " + RuntimeConfigFile::PathToUtf8(MappingDbPath());
|
||||
RT_LOG(RT_TAG_CONFIG) << "controller wizard: " << g_wizard.status << std::endl;
|
||||
return;
|
||||
}
|
||||
RT_LOG(RT_TAG_CONFIG) << "controller wizard: applied mapping " << mapping << std::endl;
|
||||
StopWizard();
|
||||
}
|
||||
|
||||
struct SetupCandidate {
|
||||
SDL_JoystickID id;
|
||||
std::string name;
|
||||
bool incompleteMapping;
|
||||
};
|
||||
|
||||
// A device needs setup when SDL has no gamepad mapping for it at all, or when
|
||||
// the mapping it matched has no analog stick even though the hardware reports
|
||||
// axes (SDL's built-in raphnet WUSBMote entry is button-only).
|
||||
std::vector<SetupCandidate> CollectCandidates() {
|
||||
std::vector<SetupCandidate> candidates;
|
||||
int count = 0;
|
||||
SDL_JoystickID* ids = SDL_GetJoysticks(&count);
|
||||
if (ids == nullptr) {
|
||||
return candidates;
|
||||
}
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const SDL_JoystickID id = ids[i];
|
||||
const char* rawName = SDL_GetJoystickNameForID(id);
|
||||
const std::string name = rawName != nullptr ? rawName : "Unknown controller";
|
||||
if (!SDL_IsGamepad(id)) {
|
||||
candidates.push_back({id, name, false});
|
||||
continue;
|
||||
}
|
||||
SDL_Gamepad* gamepad = SDL_GetGamepadFromID(id);
|
||||
if (gamepad == nullptr) {
|
||||
continue;
|
||||
}
|
||||
char* mapping = SDL_GetGamepadMappingForID(id);
|
||||
if (mapping == nullptr) {
|
||||
continue;
|
||||
}
|
||||
const std::string mappingStr = mapping;
|
||||
SDL_free(mapping);
|
||||
const bool hasStick = mappingStr.find("leftx:") != std::string::npos &&
|
||||
mappingStr.find("lefty:") != std::string::npos;
|
||||
SDL_Joystick* joystick = SDL_GetGamepadJoystick(gamepad);
|
||||
if (!hasStick && joystick != nullptr && SDL_GetNumJoystickAxes(joystick) >= 2) {
|
||||
candidates.push_back({id, name, true});
|
||||
}
|
||||
}
|
||||
SDL_free(ids);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
void HandleButtonDown(const SDL_JoyButtonEvent& event) {
|
||||
const Step& step = kSteps[g_wizard.stepIndex];
|
||||
if (step.kind == StepKind::Stick) {
|
||||
return;
|
||||
}
|
||||
const std::string value = "b" + std::to_string(event.button);
|
||||
if (BindingUsed(value)) {
|
||||
g_wizard.status = "That button is already bound";
|
||||
return;
|
||||
}
|
||||
g_wizard.status.clear();
|
||||
AdvanceStep(value);
|
||||
}
|
||||
|
||||
void HandleHatMotion(const SDL_JoyHatEvent& event) {
|
||||
const Step& step = kSteps[g_wizard.stepIndex];
|
||||
if (step.kind == StepKind::Stick) {
|
||||
return;
|
||||
}
|
||||
// Only single-direction presses bind cleanly; diagonals are ignored.
|
||||
if (event.value != SDL_HAT_UP && event.value != SDL_HAT_RIGHT && event.value != SDL_HAT_DOWN &&
|
||||
event.value != SDL_HAT_LEFT) {
|
||||
return;
|
||||
}
|
||||
const std::string value =
|
||||
"h" + std::to_string(event.hat) + "." + std::to_string(static_cast<int>(event.value));
|
||||
if (BindingUsed(value)) {
|
||||
g_wizard.status = "That direction is already bound";
|
||||
return;
|
||||
}
|
||||
g_wizard.status.clear();
|
||||
AdvanceStep(value);
|
||||
}
|
||||
|
||||
void HandleAxisMotion(const SDL_JoyAxisEvent& event) {
|
||||
const Step& step = kSteps[g_wizard.stepIndex];
|
||||
if (step.kind == StepKind::Button) {
|
||||
return;
|
||||
}
|
||||
if (event.axis >= g_wizard.axisBaseline.size()) {
|
||||
return;
|
||||
}
|
||||
const int32_t delta =
|
||||
static_cast<int32_t>(event.value) - static_cast<int32_t>(g_wizard.axisBaseline[event.axis]);
|
||||
const int16_t threshold = step.kind == StepKind::Stick ? kStickThreshold : kTriggerThreshold;
|
||||
if (std::abs(delta) < threshold) {
|
||||
return;
|
||||
}
|
||||
// Stick prompts ask for LEFT/UP, which SDL expects to be negative; triggers
|
||||
// are expected to increase when pulled. A wrong-way delta means the raw
|
||||
// axis is inverted, which the mapping expresses with a '~' suffix.
|
||||
const bool expectNegative = step.kind == StepKind::Stick;
|
||||
const bool inverted = expectNegative ? delta > 0 : delta < 0;
|
||||
std::string value = "a" + std::to_string(event.axis);
|
||||
// Reject reusing an axis already bound (with or without inversion).
|
||||
if (BindingUsed(value) || BindingUsed(value + "~")) {
|
||||
g_wizard.status = "That axis is already bound";
|
||||
return;
|
||||
}
|
||||
if (inverted) {
|
||||
value += "~";
|
||||
}
|
||||
g_wizard.status.clear();
|
||||
AdvanceStep(value);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void LoadPersistedMappings() {
|
||||
std::ifstream in(MappingDbPath());
|
||||
if (!in) {
|
||||
return;
|
||||
}
|
||||
std::string line;
|
||||
int added = 0;
|
||||
while (std::getline(in, line)) {
|
||||
const std::string trimmed = RuntimeConfigFile::Trim(line);
|
||||
if (trimmed.empty() || trimmed[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
if (SDL_AddGamepadMapping(trimmed.c_str()) >= 0) {
|
||||
++added;
|
||||
} else {
|
||||
RT_LOG(RT_TAG_CONFIG) << "gamecontrollerdb.txt: rejected mapping: " << trimmed
|
||||
<< " (" << SDL_GetError() << ")" << std::endl;
|
||||
}
|
||||
}
|
||||
if (added > 0) {
|
||||
RT_LOG(RT_TAG_CONFIG) << "gamecontrollerdb.txt: applied " << added << " custom mapping"
|
||||
<< (added == 1 ? "" : "s") << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void HandleSdlEvent(const SDL_Event& event) {
|
||||
if (!g_wizard.active) {
|
||||
return;
|
||||
}
|
||||
if (event.type == SDL_EVENT_JOYSTICK_REMOVED && event.jdevice.which == g_wizard.instance) {
|
||||
StopWizard();
|
||||
return;
|
||||
}
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||
StopWizard();
|
||||
return;
|
||||
}
|
||||
if (g_wizard.stepIndex >= kSteps.size() || Clock::now() < g_wizard.acceptAfter) {
|
||||
return;
|
||||
}
|
||||
switch (event.type) {
|
||||
case SDL_EVENT_JOYSTICK_BUTTON_DOWN:
|
||||
if (event.jbutton.which == g_wizard.instance) {
|
||||
HandleButtonDown(event.jbutton);
|
||||
}
|
||||
break;
|
||||
case SDL_EVENT_JOYSTICK_HAT_MOTION:
|
||||
if (event.jhat.which == g_wizard.instance) {
|
||||
HandleHatMotion(event.jhat);
|
||||
}
|
||||
break;
|
||||
case SDL_EVENT_JOYSTICK_AXIS_MOTION:
|
||||
if (event.jaxis.which == g_wizard.instance) {
|
||||
HandleAxisMotion(event.jaxis);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DrawSetupList() {
|
||||
const std::vector<SetupCandidate> candidates = CollectCandidates();
|
||||
if (candidates.empty()) {
|
||||
return;
|
||||
}
|
||||
ImGui::SeparatorText("Unrecognized controllers");
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
||||
ImGui::TextDisabled(
|
||||
"These devices have no usable gamepad mapping. Set one up by pressing "
|
||||
"each control when asked.");
|
||||
ImGui::PopTextWrapPos();
|
||||
for (const auto& candidate : candidates) {
|
||||
ImGui::PushID(static_cast<int>(candidate.id));
|
||||
ImGui::TextUnformatted(candidate.name.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(candidate.incompleteMapping ? "Fix mapping" : "Set up")) {
|
||||
StartWizard(candidate.id);
|
||||
}
|
||||
if (candidate.incompleteMapping && ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("SDL matched a mapping without an analog stick for this device");
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
void Draw() {
|
||||
if (!g_wizard.active) {
|
||||
return;
|
||||
}
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x + viewport->Size.x * 0.5f,
|
||||
viewport->Pos.y + viewport->Size.y * 0.5f),
|
||||
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
ImGui::SetNextWindowSize(ImVec2(420.0f, 0.0f), ImGuiCond_Appearing);
|
||||
bool open = true;
|
||||
if (ImGui::Begin("Controller setup", &open,
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings)) {
|
||||
ImGui::TextUnformatted(g_wizard.deviceName.c_str());
|
||||
ImGui::Separator();
|
||||
if (g_wizard.stepIndex < kSteps.size()) {
|
||||
ImGui::Text("Step %d of %d", static_cast<int>(g_wizard.stepIndex + 1),
|
||||
static_cast<int>(kSteps.size()));
|
||||
ImGui::Spacing();
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 400.0f);
|
||||
ImGui::TextUnformatted(kSteps[g_wizard.stepIndex].prompt);
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Skip")) {
|
||||
g_wizard.status.clear();
|
||||
AdvanceStep(std::nullopt);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(g_wizard.stepIndex == 0);
|
||||
if (ImGui::Button("Back")) {
|
||||
--g_wizard.stepIndex;
|
||||
g_wizard.bindings[g_wizard.stepIndex].reset();
|
||||
g_wizard.status.clear();
|
||||
g_wizard.acceptAfter = Clock::now() + kCaptureDebounce;
|
||||
SnapshotAxes();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel")) {
|
||||
open = false;
|
||||
}
|
||||
} else {
|
||||
const size_t boundCount =
|
||||
std::count_if(g_wizard.bindings.begin(), g_wizard.bindings.end(),
|
||||
[](const std::optional<std::string>& b) { return b.has_value(); });
|
||||
ImGui::Text("Captured %d of %d controls.", static_cast<int>(boundCount),
|
||||
static_cast<int>(kSteps.size()));
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 400.0f);
|
||||
ImGui::TextDisabled("Save applies the mapping now and remembers it for future launches.");
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Spacing();
|
||||
ImGui::BeginDisabled(boundCount == 0);
|
||||
if (ImGui::Button("Save")) {
|
||||
FinishWizard();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Back")) {
|
||||
--g_wizard.stepIndex;
|
||||
g_wizard.bindings[g_wizard.stepIndex].reset();
|
||||
g_wizard.acceptAfter = Clock::now() + kCaptureDebounce;
|
||||
SnapshotAxes();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel")) {
|
||||
open = false;
|
||||
}
|
||||
}
|
||||
if (!g_wizard.status.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.3f, 1.0f), "%s", g_wizard.status.c_str());
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
if (!open && g_wizard.active) {
|
||||
StopWizard();
|
||||
}
|
||||
}
|
||||
|
||||
bool IsActive() { return g_wizard.active; }
|
||||
|
||||
} // namespace controller_mapping_wizard
|
||||
@@ -695,12 +695,14 @@ private:
|
||||
const auto path = FindDspCoefficientRom();
|
||||
std::ifstream stream(path, std::ios::binary | std::ios::ate);
|
||||
if (!stream || stream.tellg() != static_cast<std::streamoff>(m_coeffs.size() * 2)) {
|
||||
throw std::runtime_error("Bundled Wii DSP coefficient ROM has an invalid size: " + path.string());
|
||||
throw std::runtime_error("Bundled Wii DSP coefficient ROM has an invalid size: " +
|
||||
RuntimeConfigFile::PathToUtf8(path));
|
||||
}
|
||||
stream.seekg(0);
|
||||
std::array<uint8_t, kResamplingCoefficientCount * 2> bytes{};
|
||||
if (!stream.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) {
|
||||
throw std::runtime_error("Failed to read bundled Wii DSP coefficient ROM: " + path.string());
|
||||
throw std::runtime_error("Failed to read bundled Wii DSP coefficient ROM: " +
|
||||
RuntimeConfigFile::PathToUtf8(path));
|
||||
}
|
||||
for (size_t i = 0; i < m_coeffs.size(); ++i) {
|
||||
const uint16_t word = static_cast<uint16_t>(bytes[i * 2]) << 8 |
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#include "hle_stubs.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include "memory.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
// Native because a crafted Yaz0 run writes past the caller's buffer
|
||||
// (github.com/vabold/szsHaxx)
|
||||
// https://github.com/vabold/Kinoko/blob/main/source/egg/core/Decomp.cc
|
||||
|
||||
extern "C" uint32_t EGG_Decomp_decodeSZS_80218c2c(uint32_t src, uint32_t dst)
|
||||
{
|
||||
const uint32_t expandSize = (static_cast<uint32_t>(MemoryInline::FlatRead8(src + 4)) << 24) |
|
||||
(static_cast<uint32_t>(MemoryInline::FlatRead8(src + 5)) << 16) |
|
||||
(static_cast<uint32_t>(MemoryInline::FlatRead8(src + 6)) << 8) |
|
||||
static_cast<uint32_t>(MemoryInline::FlatRead8(src + 7));
|
||||
|
||||
uint32_t srcIdx = 16;
|
||||
uint32_t dstIdx = 0;
|
||||
uint32_t mask = 0;
|
||||
uint32_t flags = 0;
|
||||
|
||||
while (static_cast<int32_t>(dstIdx) < static_cast<int32_t>(expandSize)) {
|
||||
if (mask == 0) {
|
||||
flags = MemoryInline::FlatRead8(src + srcIdx++);
|
||||
mask = 0x80;
|
||||
}
|
||||
|
||||
if ((flags & mask) != 0) {
|
||||
MemoryInline::FlatWrite8(dst + dstIdx++, MemoryInline::FlatRead8(src + srcIdx++));
|
||||
} else {
|
||||
const uint32_t high = MemoryInline::FlatRead8(src + srcIdx);
|
||||
const uint32_t low = MemoryInline::FlatRead8(src + srcIdx + 1);
|
||||
srcIdx += 2;
|
||||
|
||||
const uint32_t rep = (high << 8) | low;
|
||||
// Without this check dstIdx - distance underflows and the
|
||||
// copy leaks guest memory from before the destination buffer.
|
||||
const uint32_t distance = (rep & 0xFFF) + 1;
|
||||
if (distance > dstIdx) {
|
||||
RT_LOG(RT_TAG_HLE) << "decodeSZS: malformed stream from 0x" << std::hex << src
|
||||
<< std::dec << ", back-reference before output" << std::endl;
|
||||
ShowRuntimeFatalPopup("corrupt compressed file",
|
||||
"The game stopped decoding a malformed Yaz0 file.");
|
||||
std::abort();
|
||||
}
|
||||
uint32_t copyIdx = dstIdx - distance;
|
||||
uint32_t count = rep >> 12;
|
||||
count = count != 0
|
||||
? count + 2
|
||||
: static_cast<uint32_t>(MemoryInline::FlatRead8(src + srcIdx++)) + 18;
|
||||
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
if (dstIdx >= expandSize) {
|
||||
RT_LOG(RT_TAG_HLE) << "decodeSZS: malformed stream from 0x" << std::hex << src
|
||||
<< std::dec << ", output overran " << expandSize << " bytes"
|
||||
<< std::endl;
|
||||
ShowRuntimeFatalPopup("corrupt compressed file",
|
||||
"The game stopped decoding a malformed Yaz0 file.");
|
||||
std::abort();
|
||||
}
|
||||
MemoryInline::FlatWrite8(dst + dstIdx++, MemoryInline::FlatRead8(dst + copyIdx++));
|
||||
}
|
||||
}
|
||||
|
||||
mask >>= 1;
|
||||
}
|
||||
|
||||
return expandSize;
|
||||
}
|
||||
|
||||
PPC_NATIVE_OVERRIDE(80218C2C, EGG_Decomp_decodeSZS_80218c2c, uint32_t,
|
||||
(uint32_t src, uint32_t dst), (src, dst));
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "hle_stubs.h"
|
||||
#include "memory.h"
|
||||
#include "hle/controller_status_contract.h"
|
||||
#include "wup028_adapter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
@@ -33,6 +34,7 @@ void WritePadStatus(uint32_t base, const PADStatus& status) {
|
||||
|
||||
extern "C" uint32_t PAD__Init_HLE()
|
||||
{
|
||||
Wup028Adapter::Initialize();
|
||||
return PADInit() ? 1u : 0u;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801AF2F0, PAD__Init_HLE, uint32_t, (), ());
|
||||
@@ -44,7 +46,16 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
|
||||
}
|
||||
|
||||
PADStatus statuses[PAD_CHANMAX]{};
|
||||
const uint32_t rumbleMask = PADRead(statuses);
|
||||
std::array<PADStatus, PAD_CHANMAX> adapterStatuses{};
|
||||
uint32_t rumbleMask = PADRead(statuses);
|
||||
if (Wup028Adapter::Read(adapterStatuses) && !PADIsInputBlocked()) {
|
||||
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
|
||||
if (adapterStatuses[port].err == PAD_ERR_NONE) {
|
||||
statuses[port] = adapterStatuses[port];
|
||||
rumbleMask |= PAD_CHAN0_BIT >> port;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
for (uint32_t i = 0; i < PAD_CHANMAX; ++i) {
|
||||
@@ -73,6 +84,8 @@ PPC_NATIVE_OVERRIDE(801AF1E4, PAD__Recalibrate_HLE, uint32_t, (uint32_t mask), (
|
||||
|
||||
extern "C" void PAD__ControlMotor_HLE(int32_t chan, uint32_t command)
|
||||
{
|
||||
PADControlMotor(chan, command);
|
||||
if (!Wup028Adapter::SetRumble(static_cast<uint32_t>(chan), command == PAD_MOTOR_RUMBLE)) {
|
||||
PADControlMotor(chan, command);
|
||||
}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801AF908, PAD__ControlMotor_HLE, (int32_t chan, uint32_t command), (chan, command));
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -103,17 +104,26 @@ static void HLE_LogOSReport(CpuContext* cpu, const char* fmt)
|
||||
[&state]() { return NextOsReportDouble(state); },
|
||||
[](uint32_t address) { return ReadGuestStringForReport(address); });
|
||||
|
||||
// nw4r warnings arrive as "<file>:<line> Warning:" plus a bare newline, so
|
||||
// consecutive identical messages never land back to back. Blank lines are
|
||||
// transparent to the repeat tracker so the pair still collapses.
|
||||
static thread_local std::string lastBuffer;
|
||||
static thread_local size_t repeated = 0;
|
||||
if (buffer == lastBuffer) {
|
||||
const bool blank = buffer.find_first_not_of(" \t\r\n") == std::string::npos;
|
||||
if (blank) {
|
||||
if (repeated != 0) {
|
||||
return;
|
||||
}
|
||||
} else if (buffer == lastBuffer) {
|
||||
++repeated;
|
||||
return;
|
||||
} else {
|
||||
if (repeated != 0) {
|
||||
std::cout << "[OSReport] previous message repeated " << repeated << " time(s)" << std::endl;
|
||||
repeated = 0;
|
||||
}
|
||||
lastBuffer = buffer;
|
||||
}
|
||||
if (repeated != 0) {
|
||||
std::cout << "[OSReport] previous message repeated " << repeated << " time(s)" << std::endl;
|
||||
repeated = 0;
|
||||
}
|
||||
lastBuffer = buffer;
|
||||
|
||||
std::cout << "[OSReport] " << buffer;
|
||||
|
||||
@@ -128,9 +138,23 @@ static void HLE_LogOSReport(CpuContext* cpu, const char* fmt)
|
||||
// the guest caller because OS__Report is an HLE boundary. The context is
|
||||
// synchronized at this boundary, so capture the guest backchain at the
|
||||
// first warning/panic instead of attributing the later PPCHalt unwind.
|
||||
//
|
||||
// The dump is expensive and stdio is an unbuffered pipe, so a guest that
|
||||
// warns every frame would stall the game thread on backpressure. One dump
|
||||
// per distinct site, with an overall cap.
|
||||
if (buffer.find(" Warning:") != std::string::npos ||
|
||||
buffer.find(" Panic:") != std::string::npos) {
|
||||
SystemBridge::DumpCpuState(cpu);
|
||||
constexpr size_t kMaxWarningDumps = 8;
|
||||
static thread_local std::set<std::string> dumpedSites;
|
||||
static thread_local size_t dumpsEmitted = 0;
|
||||
if (dumpsEmitted < kMaxWarningDumps && dumpedSites.insert(buffer).second) {
|
||||
++dumpsEmitted;
|
||||
SystemBridge::DumpCpuState(cpu);
|
||||
if (dumpsEmitted == kMaxWarningDumps) {
|
||||
std::cerr << "[runtime] guest warning context dumps capped at " << kMaxWarningDumps
|
||||
<< "; further warnings log the message only." << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace fs = std::filesystem;
|
||||
// The extracted ISO "DATA" folder. We map its "files" subfolder to the DVD
|
||||
// root "/" and its "sys" subfolder to "/sys/". The root is user-owned input:
|
||||
// it is never embedded into or copied by the public runtime.
|
||||
static std::string g_dvdRoot;
|
||||
static fs::path g_dvdRoot;
|
||||
static std::once_flag g_dvdRootOnce;
|
||||
|
||||
static uint32_t CurrentDiscGameCode() {
|
||||
@@ -66,7 +66,7 @@ static uint32_t CurrentDiscGameCode() {
|
||||
#define DVD_FILEINFO_OFFSET_LEN 0x34
|
||||
|
||||
struct DVDFileEntry {
|
||||
std::string hostPath; // Full Windows path
|
||||
fs::path hostPath;
|
||||
std::string dvdPath; // Virtual Wii path (e.g., "/Race/Course.szs")
|
||||
uint32_t size;
|
||||
uint32_t discOffsetWords = 0;
|
||||
@@ -78,7 +78,7 @@ struct FstFileEntry {
|
||||
uint32_t end;
|
||||
uint32_t size;
|
||||
std::string dvdPath;
|
||||
std::string hostPath;
|
||||
fs::path hostPath;
|
||||
};
|
||||
|
||||
// Global State
|
||||
@@ -111,11 +111,9 @@ static void CopyToGuestAsDma(uint32_t dest, const uint8_t* data, size_t size) {
|
||||
GxNotifyGuestRamDmaWrite(dest, static_cast<uint32_t>(size));
|
||||
}
|
||||
|
||||
static std::string NormalizeDvdHostPath(std::string path) {
|
||||
while (!path.empty() && (path.back() == '\\' || path.back() == '/')) {
|
||||
path.pop_back();
|
||||
}
|
||||
return path;
|
||||
// Host path strings only ever leave this module as UTF-8 display text.
|
||||
static std::string HostPathText(const fs::path& path) {
|
||||
return RuntimeConfigFile::PathToUtf8(path);
|
||||
}
|
||||
|
||||
static bool IsDvdDataRoot(const fs::path& path) {
|
||||
@@ -140,7 +138,7 @@ static bool IsDvdDataRoot(const fs::path& path) {
|
||||
[[noreturn]] static void FailDvdRoot(const char* source, const fs::path& path = {}) {
|
||||
RT_LOGF(RT_TAG_DVD, "ERROR: %s", source);
|
||||
if (!path.empty()) {
|
||||
std::fprintf(stderr, ": %s", path.string().c_str());
|
||||
std::fprintf(stderr, ": %s", HostPathText(path).c_str());
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
"\n[dvd] Set [paths] dvd_root in Config.toml "
|
||||
@@ -148,14 +146,14 @@ static bool IsDvdDataRoot(const fs::path& path) {
|
||||
std::string details = source ? source : "The configured DVD root could not be opened.";
|
||||
if (!path.empty()) {
|
||||
details += "\n\nPath: ";
|
||||
details += path.string();
|
||||
details += HostPathText(path);
|
||||
}
|
||||
details += "\n\nSet [paths] dvd_root in Config.toml to the extracted "
|
||||
"Mario Kart Wii DATA directory.";
|
||||
FailDvd("dvd_root", "DVD data is unavailable", details);
|
||||
}
|
||||
|
||||
static const std::string& GetDvdRoot() {
|
||||
static const fs::path& GetDvdRoot() {
|
||||
std::call_once(g_dvdRootOnce, []() {
|
||||
const fs::path path = RuntimeConfigFile::ResolvedDvdRoot();
|
||||
if (path.empty()) {
|
||||
@@ -164,14 +162,14 @@ static const std::string& GetDvdRoot() {
|
||||
if (!IsDvdDataRoot(path)) {
|
||||
FailDvdRoot("Configured DVD root is not an extracted DATA directory", path);
|
||||
}
|
||||
g_dvdRoot = NormalizeDvdHostPath(path.string());
|
||||
g_dvdRoot = path;
|
||||
});
|
||||
|
||||
return g_dvdRoot;
|
||||
}
|
||||
|
||||
static std::string NormalizePath(const std::string& path);
|
||||
static std::string ResolveDvdMappedHostPath(const std::string& dvdPath, const std::string& fallbackHostPath);
|
||||
static fs::path ResolveDvdMappedHostPath(const std::string& dvdPath, const fs::path& fallbackHostPath);
|
||||
|
||||
static void InvokeDvdCallback(uint32_t callbackPtr, int32_t result, uint32_t fileInfoPtr) {
|
||||
if (callbackPtr == 0) {
|
||||
@@ -240,7 +238,7 @@ static void LoadFstIndex() {
|
||||
}
|
||||
g_fstLoaded = true;
|
||||
|
||||
fs::path fstPath = fs::path(GetDvdRoot()) / "sys" / "fst.bin";
|
||||
const fs::path fstPath = GetDvdRoot() / "sys" / "fst.bin";
|
||||
std::ifstream fstFile(fstPath, std::ios::binary);
|
||||
if (!fstFile.is_open()) {
|
||||
return;
|
||||
@@ -333,7 +331,7 @@ static void LoadFstIndex() {
|
||||
entry.end = endBytes;
|
||||
entry.size = fileSize;
|
||||
entry.dvdPath = "/" + relPath;
|
||||
const std::string baseHostPath = (fs::path(GetDvdRoot()) / "files" / fs::path(relPath)).string();
|
||||
const fs::path baseHostPath = GetDvdRoot() / "files" / fs::path(relPath);
|
||||
entry.hostPath = ResolveDvdMappedHostPath(entry.dvdPath, baseHostPath);
|
||||
g_fstFiles.push_back(std::move(entry));
|
||||
}
|
||||
@@ -408,7 +406,7 @@ static void RegisterFileEntry(std::string dvdPath, const fs::path& hostPath, uin
|
||||
dvdPath = DvdFstContract::CanonicalizePath(dvdPath);
|
||||
|
||||
DVDFileEntry fileEntry;
|
||||
fileEntry.hostPath = hostPath.string();
|
||||
fileEntry.hostPath = hostPath;
|
||||
fileEntry.dvdPath = dvdPath;
|
||||
fileEntry.size = size;
|
||||
|
||||
@@ -445,7 +443,7 @@ static void WalkDirectory(const fs::path& root, bool recursive, bool announceErr
|
||||
fs::recursive_directory_iterator it(root, fs::directory_options::skip_permission_denied, ec);
|
||||
if (ec) {
|
||||
if (announceErrors) {
|
||||
RT_LOG(RT_TAG_DVD) << "WARNING: cannot enumerate " << root.string() << ": "
|
||||
RT_LOG(RT_TAG_DVD) << "WARNING: cannot enumerate " << HostPathText(root) << ": "
|
||||
<< ec.message() << std::endl;
|
||||
}
|
||||
return;
|
||||
@@ -458,7 +456,7 @@ static void WalkDirectory(const fs::path& root, bool recursive, bool announceErr
|
||||
it.increment(ec);
|
||||
if (ec) {
|
||||
if (announceErrors) {
|
||||
RT_LOG(RT_TAG_DVD) << "WARNING: stopped enumerating " << root.string() << ": "
|
||||
RT_LOG(RT_TAG_DVD) << "WARNING: stopped enumerating " << HostPathText(root) << ": "
|
||||
<< ec.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
@@ -488,7 +486,7 @@ static void ScanDirectory(const fs::path& root, const std::string& virtualPrefix
|
||||
|
||||
const std::uintmax_t size = fs::file_size(entry.path(), entryEc);
|
||||
if (entryEc) {
|
||||
RT_LOG(RT_TAG_DVD) << "WARNING: skipping " << entry.path().string() << ": "
|
||||
RT_LOG(RT_TAG_DVD) << "WARNING: skipping " << HostPathText(entry.path()) << ": "
|
||||
<< entryEc.message() << std::endl;
|
||||
return;
|
||||
}
|
||||
@@ -502,7 +500,7 @@ static void ScanDirectory(const fs::path& root, const std::string& virtualPrefix
|
||||
if (prefix.back() != '/' && prefix.back() != '\\') {
|
||||
prefix += "/";
|
||||
}
|
||||
std::string dvdPath = prefix + relative.string();
|
||||
std::string dvdPath = prefix + HostPathText(relative);
|
||||
if (!addNewFiles && !DvdEntryExists(dvdPath)) {
|
||||
return;
|
||||
}
|
||||
@@ -536,7 +534,7 @@ static void ApplyFolderByNameMapping(const RuntimeRiivolution::Mapping& mapping)
|
||||
if (!entry.is_regular_file(entryEc) || entryEc) {
|
||||
return;
|
||||
}
|
||||
std::string name = entry.path().filename().string();
|
||||
std::string name = HostPathText(entry.path().filename());
|
||||
RuntimeHle::LowerInPlace(name);
|
||||
const auto matches = discPathsByName.find(name);
|
||||
if (matches == discPathsByName.end()) {
|
||||
@@ -554,7 +552,7 @@ static void ApplyFolderByNameMapping(const RuntimeRiivolution::Mapping& mapping)
|
||||
|
||||
WalkDirectory(mapping.hostPath, mapping.recursive, /*announceErrors=*/false, applyEntry);
|
||||
|
||||
RT_LOG(RT_TAG_DVD) << mapping.hostPath.string() << ": replaced " << replaced
|
||||
RT_LOG(RT_TAG_DVD) << HostPathText(mapping.hostPath) << ": replaced " << replaced
|
||||
<< " disc file(s) by filename" << std::endl;
|
||||
}
|
||||
|
||||
@@ -562,7 +560,7 @@ static void ScanOverlayRoot(const RuntimeRiivolution::Overlay& overlay) {
|
||||
if (!overlay.patches) {
|
||||
// Fallback for mod roots that mirror the disc filesystem directly (not a
|
||||
// Riivolution pack, which wouldn't map anything useful this way).
|
||||
RT_LOG(RT_TAG_DVD) << overlay.root.string()
|
||||
RT_LOG(RT_TAG_DVD) << HostPathText(overlay.root)
|
||||
<< ": no Riivolution XML found, treating the root as a disc-shaped overlay"
|
||||
<< std::endl;
|
||||
ScanDirectory(overlay.root, "/");
|
||||
@@ -592,7 +590,7 @@ static void ScanOverlayRoot(const RuntimeRiivolution::Overlay& overlay) {
|
||||
}
|
||||
}
|
||||
|
||||
static std::string ResolveDvdMappedHostPath(const std::string& dvdPath, const std::string& fallbackHostPath) {
|
||||
static fs::path ResolveDvdMappedHostPath(const std::string& dvdPath, const fs::path& fallbackHostPath) {
|
||||
const std::string normalized = NormalizePath(dvdPath);
|
||||
const auto it = g_pathToEntry.find(normalized);
|
||||
if (it != g_pathToEntry.end() && it->second >= 0 &&
|
||||
@@ -604,7 +602,7 @@ static std::string ResolveDvdMappedHostPath(const std::string& dvdPath, const st
|
||||
const fs::path candidate = overlay.root / fs::path(normalized.substr(1));
|
||||
std::error_code ec;
|
||||
if (fs::is_regular_file(candidate, ec)) {
|
||||
return candidate.string();
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -740,7 +738,7 @@ extern "C" const char* DVDResolveHostPathForTest(const char* dvdPath)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
resolved = g_fileEntries[it->second].hostPath;
|
||||
resolved = HostPathText(g_fileEntries[it->second].hostPath);
|
||||
return resolved.c_str();
|
||||
}
|
||||
|
||||
@@ -808,7 +806,7 @@ extern "C" void DVDInit_8015EA1C()
|
||||
Memory::Write16(diskHeader + 0x04, 0x3031); // '01' (Maker)
|
||||
Memory::Write8(diskHeader + 0x06, 0x01); // Disk #1
|
||||
// 4. Scan Files
|
||||
fs::path rootPath(GetDvdRoot());
|
||||
const fs::path& rootPath = GetDvdRoot();
|
||||
|
||||
// Map "<dvd_root>/files" -> "/"
|
||||
ScanDirectory(rootPath / "files", "/");
|
||||
@@ -879,7 +877,7 @@ extern "C" int32_t DVDReadPrio_8015E834(uint32_t fileInfoPtr, uint32_t bufferPtr
|
||||
const DVDFileEntry& entry = g_fileEntries[extent->entryIndex];
|
||||
|
||||
if (offset < 0 || length < 0) {
|
||||
return DvdReadFatal(fileInfoPtr, entry.hostPath, offset,
|
||||
return DvdReadFatal(fileInfoPtr, HostPathText(entry.hostPath), offset,
|
||||
length > 0 ? static_cast<uint32_t>(length) : 0,
|
||||
"negative DVD read offset or length");
|
||||
}
|
||||
@@ -889,7 +887,7 @@ extern "C" int32_t DVDReadPrio_8015E834(uint32_t fileInfoPtr, uint32_t bufferPtr
|
||||
uint32_t uLength = (uint32_t)length;
|
||||
|
||||
if (requestedOffset >= entry.size) {
|
||||
return DvdReadFatal(fileInfoPtr, entry.hostPath, offset, uLength,
|
||||
return DvdReadFatal(fileInfoPtr, HostPathText(entry.hostPath), offset, uLength,
|
||||
"read offset is outside the indexed DVD file");
|
||||
}
|
||||
const uint32_t uOffset = static_cast<uint32_t>(requestedOffset);
|
||||
@@ -899,14 +897,14 @@ extern "C" int32_t DVDReadPrio_8015E834(uint32_t fileInfoPtr, uint32_t bufferPtr
|
||||
}
|
||||
|
||||
if (uLength != 0 && !Memory::Contains(bufferPtr, uLength)) {
|
||||
return DvdReadFatal(fileInfoPtr, entry.hostPath, offset, uLength,
|
||||
return DvdReadFatal(fileInfoPtr, HostPathText(entry.hostPath), offset, uLength,
|
||||
"DVD read destination is outside guest memory");
|
||||
}
|
||||
|
||||
std::vector<uint8_t> tempBuf;
|
||||
DvdReadContract::HostReadFailure failure;
|
||||
if (!DvdReadContract::ReadExact(entry.hostPath, uOffset, uLength, tempBuf, failure)) {
|
||||
return DvdReadFatal(fileInfoPtr, entry.hostPath, offset, uLength,
|
||||
return DvdReadFatal(fileInfoPtr, HostPathText(entry.hostPath), offset, uLength,
|
||||
DvdReadContract::Describe(failure));
|
||||
}
|
||||
|
||||
@@ -962,11 +960,11 @@ extern "C" int32_t DVD__ReadAbsAsyncPrio_HLE_801628cc(uint32_t cmdBlockPtr,
|
||||
requestedLength,
|
||||
"absolute DVD read offset is not mapped to a host file");
|
||||
} else if (requestedLength != 0 && readInfo.readLength != requestedLength) {
|
||||
bytesRead = DvdReadFatal(cmdBlockPtr, readInfo.entry->hostPath,
|
||||
bytesRead = DvdReadFatal(cmdBlockPtr, HostPathText(readInfo.entry->hostPath),
|
||||
readInfo.fileOffset, requestedLength,
|
||||
"requested range extends beyond the indexed DVD file");
|
||||
} else if (requestedLength != 0 && !Memory::Contains(bufferPtr, requestedLength)) {
|
||||
bytesRead = DvdReadFatal(cmdBlockPtr, readInfo.entry->hostPath,
|
||||
bytesRead = DvdReadFatal(cmdBlockPtr, HostPathText(readInfo.entry->hostPath),
|
||||
readInfo.fileOffset, requestedLength,
|
||||
"DVD read destination is outside guest memory");
|
||||
} else {
|
||||
@@ -977,7 +975,7 @@ extern "C" int32_t DVD__ReadAbsAsyncPrio_HLE_801628cc(uint32_t cmdBlockPtr,
|
||||
readInfo.readLength,
|
||||
tempBuf,
|
||||
failure)) {
|
||||
bytesRead = DvdReadFatal(cmdBlockPtr, readInfo.entry->hostPath,
|
||||
bytesRead = DvdReadFatal(cmdBlockPtr, HostPathText(readInfo.entry->hostPath),
|
||||
readInfo.fileOffset, readInfo.readLength,
|
||||
DvdReadContract::Describe(failure));
|
||||
} else {
|
||||
@@ -1076,12 +1074,12 @@ extern "C" int32_t DVDLowRead_80166330(uint32_t buffer, uint32_t length, uint32_
|
||||
return finish(false);
|
||||
}
|
||||
if (readInfo.readLength != length) {
|
||||
ReportDvdReadError(readInfo.entry->hostPath, readInfo.fileOffset, length,
|
||||
ReportDvdReadError(HostPathText(readInfo.entry->hostPath), readInfo.fileOffset, length,
|
||||
"requested range extends beyond the indexed DVD file");
|
||||
return finish(false);
|
||||
}
|
||||
if (!Memory::Contains(buffer, length)) {
|
||||
ReportDvdReadError(readInfo.entry->hostPath, readInfo.fileOffset, length,
|
||||
ReportDvdReadError(HostPathText(readInfo.entry->hostPath), readInfo.fileOffset, length,
|
||||
"DVD read destination is outside guest memory");
|
||||
return finish(false);
|
||||
}
|
||||
@@ -1093,7 +1091,7 @@ extern "C" int32_t DVDLowRead_80166330(uint32_t buffer, uint32_t length, uint32_
|
||||
readInfo.readLength,
|
||||
tempBuf,
|
||||
failure)) {
|
||||
ReportDvdReadError(readInfo.entry->hostPath, readInfo.fileOffset,
|
||||
ReportDvdReadError(HostPathText(readInfo.entry->hostPath), readInfo.fileOffset,
|
||||
readInfo.readLength, DvdReadContract::Describe(failure));
|
||||
return finish(false);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ extern "C" int32_t NANDOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint32_t
|
||||
return NAND_RESULT_INVALID;
|
||||
}
|
||||
|
||||
std::string hostPath = TranslateNandPath(path);
|
||||
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||
|
||||
// Existing-file write opens go through a shadow copy seeded from the original, so a
|
||||
// crash between NANDWrite and NANDClose cannot leave a torn file (the game patches
|
||||
@@ -101,23 +101,23 @@ extern "C" int32_t NANDOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint32_t
|
||||
// Another live handle already refers to this file. A shadow would hide the
|
||||
// writes from that handle, so stay in place for this open.
|
||||
LogNandWarning("NANDOpen", "WARNING: '%s' already has a live handle, writing in place",
|
||||
hostPath.c_str());
|
||||
HostPathText(hostPath).c_str());
|
||||
} else {
|
||||
const std::string tempPath = SafeTempPathFor(hostPath);
|
||||
const std::filesystem::path tempPath = SafeTempPathFor(hostPath);
|
||||
if (DiscardStaleSafeTemp(tempPath)) {
|
||||
std::error_code ec;
|
||||
std::filesystem::copy_file(hostPath, tempPath,
|
||||
std::filesystem::copy_options::overwrite_existing, ec);
|
||||
if (ec) {
|
||||
LogNandWarning("NANDOpen", "WARNING: could not seed shadow '%s' (%s), writing in place",
|
||||
tempPath.c_str(), ec.message().c_str());
|
||||
std::remove(tempPath.c_str());
|
||||
HostPathText(tempPath).c_str(), ec.message().c_str());
|
||||
NandRemove(tempPath);
|
||||
} else {
|
||||
FILE* shadow = std::fopen(tempPath.c_str(), "r+b");
|
||||
FILE* shadow = NandFopen(tempPath, "r+b");
|
||||
if (!shadow) {
|
||||
LogNandWarning("NANDOpen", "WARNING: could not open shadow '%s', writing in place",
|
||||
tempPath.c_str());
|
||||
std::remove(tempPath.c_str());
|
||||
HostPathText(tempPath).c_str());
|
||||
NandRemove(tempPath);
|
||||
} else {
|
||||
const int32_t shadowFd = AllocateFd(tempPath, shadow, static_cast<int32_t>(mode));
|
||||
{
|
||||
@@ -141,20 +141,20 @@ extern "C" int32_t NANDOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint32_t
|
||||
else if (mode == 2) fopenMode = "r+b";
|
||||
else if (mode == 3) fopenMode = "r+b";
|
||||
|
||||
FILE* file = std::fopen(hostPath.c_str(), fopenMode);
|
||||
FILE* file = NandFopen(hostPath, fopenMode);
|
||||
if (!file && mode >= 2) {
|
||||
// Try creating for write modes
|
||||
file = std::fopen(hostPath.c_str(), "w+b");
|
||||
file = NandFopen(hostPath, "w+b");
|
||||
}
|
||||
|
||||
// Create parent directories and retry
|
||||
if (!file && CreateParentDirectories(hostPath)) {
|
||||
file = std::fopen(hostPath.c_str(), mode >= 2 ? "w+b" : "rb");
|
||||
file = NandFopen(hostPath, mode >= 2 ? "w+b" : "rb");
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
if (IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) {
|
||||
file = std::fopen(hostPath.c_str(), fopenMode);
|
||||
file = NandFopen(hostPath, fopenMode);
|
||||
}
|
||||
if (!file) {
|
||||
LogNandError("NANDOpen", "FAILED to open");
|
||||
@@ -282,7 +282,7 @@ extern "C" int32_t NANDCreate_HLE(uint32_t pathPtr, uint32_t perm, uint32_t attr
|
||||
return NAND_RESULT_INVALID;
|
||||
}
|
||||
|
||||
std::string hostPath = TranslateNandPath(path);
|
||||
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||
CreateParentDirectories(hostPath);
|
||||
|
||||
// Check if file already exists
|
||||
@@ -291,7 +291,7 @@ extern "C" int32_t NANDCreate_HLE(uint32_t pathPtr, uint32_t perm, uint32_t attr
|
||||
}
|
||||
|
||||
// Create empty file
|
||||
FILE* f = std::fopen(hostPath.c_str(), "wb");
|
||||
FILE* f = NandFopen(hostPath, "wb");
|
||||
if (!f) {
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
@@ -307,13 +307,13 @@ extern "C" int32_t NANDDelete_HLE(uint32_t pathPtr) {
|
||||
return NAND_RESULT_INVALID;
|
||||
}
|
||||
|
||||
std::string hostPath = TranslateNandPath(path);
|
||||
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||
|
||||
if (!PathExists(hostPath)) {
|
||||
return NAND_RESULT_NOEXISTS;
|
||||
}
|
||||
|
||||
if (std::remove(hostPath.c_str()) == 0) {
|
||||
if (NandRemove(hostPath)) {
|
||||
return NAND_RESULT_OK;
|
||||
}
|
||||
|
||||
@@ -327,7 +327,7 @@ extern "C" int32_t NANDCreateDir_HLE(uint32_t pathPtr, uint32_t perm, uint32_t a
|
||||
return NAND_RESULT_INVALID;
|
||||
}
|
||||
|
||||
std::string hostPath = TranslateNandPath(path);
|
||||
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||
|
||||
if (PathExists(hostPath)) {
|
||||
if (IsDirectory(hostPath)) {
|
||||
@@ -337,11 +337,6 @@ extern "C" int32_t NANDCreateDir_HLE(uint32_t pathPtr, uint32_t perm, uint32_t a
|
||||
}
|
||||
|
||||
if (CreateDirectoryPath(hostPath)) {
|
||||
#ifdef _WIN32
|
||||
_mkdir(hostPath.c_str());
|
||||
#else
|
||||
mkdir(hostPath.c_str(), 0755);
|
||||
#endif
|
||||
return NAND_RESULT_OK;
|
||||
}
|
||||
|
||||
@@ -369,13 +364,13 @@ extern "C" int32_t NANDMove_HLE(uint32_t srcPathPtr, uint32_t dstPathPtr) {
|
||||
// filename (for example /tmp/banner.bin -> <title home>/banner.bin).
|
||||
const std::filesystem::path dstHost = dstDirectoryHost / srcName;
|
||||
|
||||
if (!PathExists(srcHost.string())) {
|
||||
if (!PathExists(srcHost)) {
|
||||
return NAND_RESULT_NOEXISTS;
|
||||
}
|
||||
if (!IsDirectory(dstDirectoryHost.string())) {
|
||||
if (!IsDirectory(dstDirectoryHost)) {
|
||||
return NAND_RESULT_NOEXISTS;
|
||||
}
|
||||
if (PathExists(dstHost.string())) {
|
||||
if (PathExists(dstHost)) {
|
||||
return NAND_RESULT_EXISTS;
|
||||
}
|
||||
|
||||
@@ -396,7 +391,7 @@ extern "C" int32_t NANDGetStatus_HLE(uint32_t pathPtr, uint32_t outStatusPtr) {
|
||||
return NAND_RESULT_INVALID;
|
||||
}
|
||||
|
||||
std::string hostPath = TranslateNandPath(path);
|
||||
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||
|
||||
if (!PathExists(hostPath)) {
|
||||
return NAND_RESULT_NOEXISTS;
|
||||
@@ -417,7 +412,7 @@ extern "C" int32_t NANDGetType_HLE(uint32_t pathPtr, uint32_t outTypePtr) {
|
||||
return NAND_RESULT_INVALID;
|
||||
}
|
||||
|
||||
std::string hostPath = TranslateNandPath(path);
|
||||
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||
|
||||
if (!PathExists(hostPath)) {
|
||||
return NAND_RESULT_NOEXISTS;
|
||||
|
||||
@@ -234,8 +234,10 @@ PPC_NATIVE_OVERRIDE(8019E7B4, NANDPrivateGetTypeAsync_HLE, int32_t,
|
||||
|
||||
static const char kNandSafeTempSuffix[] = ".nandsafe.tmp";
|
||||
|
||||
std::string SafeTempPathFor(const std::string& hostPath) {
|
||||
return hostPath + kNandSafeTempSuffix;
|
||||
std::filesystem::path SafeTempPathFor(const std::filesystem::path& hostPath) {
|
||||
std::filesystem::path tempPath = hostPath;
|
||||
tempPath += kNandSafeTempSuffix;
|
||||
return tempPath;
|
||||
}
|
||||
|
||||
// Push the CRT buffer out and then force the OS to put it on the platter, so the data is
|
||||
@@ -264,24 +266,25 @@ static bool FlushFileToDisk(FILE* file) {
|
||||
|
||||
// Replace `targetPath` with `tempPath` in one step. Either the old or the new contents
|
||||
// survive a crash; there is no window where the target is truncated or partial.
|
||||
static bool AtomicReplaceHostFile(const char* who, const std::string& tempPath,
|
||||
const std::string& targetPath) {
|
||||
static bool AtomicReplaceHostFile(const char* who, const std::filesystem::path& tempPath,
|
||||
const std::filesystem::path& targetPath) {
|
||||
#ifdef _WIN32
|
||||
if (MoveFileExA(tempPath.c_str(), targetPath.c_str(),
|
||||
if (MoveFileExW(tempPath.c_str(), targetPath.c_str(),
|
||||
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
|
||||
return true;
|
||||
}
|
||||
LogNandError(who, "ERROR: MoveFileEx('%s' -> '%s') failed (err=%lu)",
|
||||
tempPath.c_str(), targetPath.c_str(), static_cast<unsigned long>(GetLastError()));
|
||||
HostPathText(tempPath).c_str(), HostPathText(targetPath).c_str(),
|
||||
static_cast<unsigned long>(GetLastError()));
|
||||
return false;
|
||||
#else
|
||||
if (std::rename(tempPath.c_str(), targetPath.c_str()) != 0) {
|
||||
if (!NandRename(tempPath, targetPath)) {
|
||||
LogNandError(who, "ERROR: rename('%s' -> '%s') failed",
|
||||
tempPath.c_str(), targetPath.c_str());
|
||||
HostPathText(tempPath).c_str(), HostPathText(targetPath).c_str());
|
||||
return false;
|
||||
}
|
||||
// Durably record the directory entry so the rename itself survives a crash.
|
||||
const std::string directory = std::filesystem::path(targetPath).parent_path().string();
|
||||
const std::string directory = targetPath.parent_path().string();
|
||||
const int dirFd = open(directory.c_str(), O_RDONLY);
|
||||
if (dirFd >= 0) {
|
||||
fsync(dirFd);
|
||||
@@ -293,23 +296,24 @@ static bool AtomicReplaceHostFile(const char* who, const std::string& tempPath,
|
||||
|
||||
// Remove a scratch file left behind by a previous run that died between safe open and
|
||||
// safe close. Its contents are worthless: the original was never replaced.
|
||||
bool DiscardStaleSafeTemp(const std::string& tempPath) {
|
||||
bool DiscardStaleSafeTemp(const std::filesystem::path& tempPath) {
|
||||
if (!PathExists(tempPath)) {
|
||||
return true;
|
||||
}
|
||||
LogNandWarning("nand-shadow", "WARNING: discarding stale scratch file '%s' from a previous run",
|
||||
tempPath.c_str());
|
||||
if (std::remove(tempPath.c_str()) == 0) {
|
||||
HostPathText(tempPath).c_str());
|
||||
if (NandRemove(tempPath)) {
|
||||
return true;
|
||||
}
|
||||
LogNandError("nand-shadow", "FAILED to remove stale scratch file '%s'", tempPath.c_str());
|
||||
LogNandError("nand-shadow", "FAILED to remove stale scratch file '%s'",
|
||||
HostPathText(tempPath).c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// True when any live handle already refers to `hostPath`, either directly or as the
|
||||
// commit target of a shadow. Used to keep shadow writes from hiding data behind a second
|
||||
// handle on the same file.
|
||||
bool IsHostPathOpen(const std::string& hostPath) {
|
||||
bool IsHostPathOpen(const std::filesystem::path& hostPath) {
|
||||
std::lock_guard<std::mutex> lock(g_fdMutex);
|
||||
for (const auto& entry : g_fileHandles) {
|
||||
if (entry.second.path == hostPath || entry.second.safeCommitPath == hostPath) {
|
||||
@@ -324,8 +328,8 @@ bool IsHostPathOpen(const std::string& hostPath) {
|
||||
// dropped and the original is left exactly as it was, and the error is returned so the
|
||||
// guest's close call fails instead of silently reporting success.
|
||||
int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError) {
|
||||
std::string tempPath;
|
||||
std::string commitPath;
|
||||
std::filesystem::path tempPath;
|
||||
std::filesystem::path commitPath;
|
||||
FILE* file = nullptr;
|
||||
int32_t mode = 0;
|
||||
|
||||
@@ -358,7 +362,7 @@ int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError) {
|
||||
|
||||
if (!needsCommit) {
|
||||
if (!flushed) {
|
||||
LogNandError(who, "ERROR: flush of '%s' failed", tempPath.c_str());
|
||||
LogNandError(who, "ERROR: flush of '%s' failed", HostPathText(tempPath).c_str());
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
return NAND_RESULT_OK;
|
||||
@@ -366,13 +370,13 @@ int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError) {
|
||||
|
||||
if (!flushed) {
|
||||
LogNandError(who, "ERROR: flush of '%s' failed, discarding it and leaving '%s' untouched",
|
||||
tempPath.c_str(), commitPath.c_str());
|
||||
std::remove(tempPath.c_str());
|
||||
HostPathText(tempPath).c_str(), HostPathText(commitPath).c_str());
|
||||
NandRemove(tempPath);
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
|
||||
if (!AtomicReplaceHostFile(who, tempPath, commitPath)) {
|
||||
std::remove(tempPath.c_str());
|
||||
NandRemove(tempPath);
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
|
||||
@@ -394,7 +398,7 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
|
||||
return NAND_RESULT_INVALID;
|
||||
}
|
||||
|
||||
const std::string hostPath = TranslateNandPath(path);
|
||||
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||
if (hostPath.empty()) {
|
||||
LogNandError("NANDSafeOpen", "FAILED to translate path '%s'", path);
|
||||
return NAND_RESULT_INVALID;
|
||||
@@ -407,12 +411,13 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
|
||||
if (mode == 1) {
|
||||
// Read-only safe open reads the original in place; the library builds no scratch
|
||||
// copy for this case.
|
||||
FILE* file = std::fopen(hostPath.c_str(), "rb");
|
||||
FILE* file = NandFopen(hostPath, "rb");
|
||||
if (!file && IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) {
|
||||
file = std::fopen(hostPath.c_str(), "rb");
|
||||
file = NandFopen(hostPath, "rb");
|
||||
}
|
||||
if (!file) {
|
||||
LogNandError("NANDSafeOpen", "FAILED to open '%s' for reading", hostPath.c_str());
|
||||
LogNandError("NANDSafeOpen", "FAILED to open '%s' for reading",
|
||||
HostPathText(hostPath).c_str());
|
||||
return NAND_RESULT_NOEXISTS;
|
||||
}
|
||||
|
||||
@@ -425,15 +430,16 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
|
||||
// Write modes. The library queries the attributes of the original first, so a safe
|
||||
// open of a file that does not exist fails instead of creating one.
|
||||
if (!PathExists(hostPath)) {
|
||||
LogNandError("NANDSafeOpen", "FAILED: '%s' does not exist, safe open never creates it", hostPath.c_str());
|
||||
LogNandError("NANDSafeOpen", "FAILED: '%s' does not exist, safe open never creates it",
|
||||
HostPathText(hostPath).c_str());
|
||||
return NAND_RESULT_NOEXISTS;
|
||||
}
|
||||
if (IsDirectory(hostPath)) {
|
||||
LogNandError("NANDSafeOpen", "FAILED: '%s' is a directory", hostPath.c_str());
|
||||
LogNandError("NANDSafeOpen", "FAILED: '%s' is a directory", HostPathText(hostPath).c_str());
|
||||
return NAND_RESULT_INVALID;
|
||||
}
|
||||
|
||||
const std::string tempPath = SafeTempPathFor(hostPath);
|
||||
const std::filesystem::path tempPath = SafeTempPathFor(hostPath);
|
||||
if (!DiscardStaleSafeTemp(tempPath)) {
|
||||
return NAND_RESULT_ACCESS;
|
||||
}
|
||||
@@ -445,15 +451,17 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
|
||||
std::filesystem::copy_options::overwrite_existing, ec);
|
||||
if (ec) {
|
||||
LogNandError("NANDSafeOpen", "FAILED to seed scratch file '%s' from '%s': %s",
|
||||
tempPath.c_str(), hostPath.c_str(), ec.message().c_str());
|
||||
std::remove(tempPath.c_str());
|
||||
HostPathText(tempPath).c_str(), HostPathText(hostPath).c_str(),
|
||||
ec.message().c_str());
|
||||
NandRemove(tempPath);
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
|
||||
FILE* file = std::fopen(tempPath.c_str(), "r+b");
|
||||
FILE* file = NandFopen(tempPath, "r+b");
|
||||
if (!file) {
|
||||
LogNandError("NANDSafeOpen", "FAILED to open scratch file '%s'", tempPath.c_str());
|
||||
std::remove(tempPath.c_str());
|
||||
LogNandError("NANDSafeOpen", "FAILED to open scratch file '%s'",
|
||||
HostPathText(tempPath).c_str());
|
||||
NandRemove(tempPath);
|
||||
return NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// ============================================================================
|
||||
|
||||
// Base path for the host Wii NAND directory (resolved at runtime).
|
||||
static std::string g_dolphinWiiBase;
|
||||
static std::filesystem::path g_dolphinWiiBase;
|
||||
static std::once_flag g_dolphinWiiBaseOnce;
|
||||
|
||||
// ============================================================================
|
||||
@@ -47,7 +47,7 @@ std::map<int32_t, FileHandle> g_fileHandles;
|
||||
static int32_t g_nextFd = 100; // Start at 100 to avoid confusion with stdio fds
|
||||
std::mutex g_fdMutex;
|
||||
|
||||
int32_t AllocateFd(const std::string& path, FILE* file, int32_t mode) {
|
||||
int32_t AllocateFd(const std::filesystem::path& path, FILE* file, int32_t mode) {
|
||||
std::lock_guard<std::mutex> lock(g_fdMutex);
|
||||
int32_t fd = g_nextFd++;
|
||||
g_fileHandles[fd] = {file, path, mode, 0};
|
||||
@@ -88,29 +88,50 @@ std::string CurrentNandDataDir() {
|
||||
return path;
|
||||
}
|
||||
|
||||
const std::string& GetNandBasePath() {
|
||||
const std::filesystem::path& GetNandBasePath() {
|
||||
std::call_once(g_dolphinWiiBaseOnce, []() {
|
||||
g_dolphinWiiBase = RuntimeNandPath::DiscoverNandRootString();
|
||||
g_dolphinWiiBase = RuntimeNandPath::DiscoverNandRootPath();
|
||||
});
|
||||
|
||||
return g_dolphinWiiBase;
|
||||
}
|
||||
|
||||
static std::string BuildHostNandPath(std::string wiiPathStr) {
|
||||
std::string hostPath = GetNandBasePath();
|
||||
for (char& c : wiiPathStr) {
|
||||
if (c == '/') {
|
||||
#ifdef _WIN32
|
||||
c = '\\';
|
||||
#endif
|
||||
}
|
||||
}
|
||||
std::string HostPathText(const std::filesystem::path& path) {
|
||||
return RuntimeConfigFile::PathToUtf8(path);
|
||||
}
|
||||
|
||||
if (!wiiPathStr.empty() && (wiiPathStr[0] == '\\' || wiiPathStr[0] == '/')) {
|
||||
hostPath += wiiPathStr;
|
||||
} else {
|
||||
hostPath += "\\";
|
||||
hostPath += wiiPathStr;
|
||||
FILE* NandFopen(const std::filesystem::path& path, const char* mode) {
|
||||
#ifdef _WIN32
|
||||
const std::wstring wideMode(mode, mode + std::strlen(mode));
|
||||
return _wfopen(path.c_str(), wideMode.c_str());
|
||||
#else
|
||||
return std::fopen(path.c_str(), mode);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool NandRemove(const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
return std::filesystem::remove(path, ec) && !ec;
|
||||
}
|
||||
|
||||
bool NandRename(const std::filesystem::path& from, const std::filesystem::path& to) {
|
||||
std::error_code ec;
|
||||
std::filesystem::rename(from, to, ec);
|
||||
return !ec;
|
||||
}
|
||||
|
||||
// Guest paths are absolute and already lexically resolved against the NAND root, so
|
||||
// they are appended as relative components instead of replacing the root.
|
||||
static std::filesystem::path BuildHostNandPath(const std::string& wiiPathStr) {
|
||||
std::filesystem::path hostPath = GetNandBasePath();
|
||||
size_t cursor = 0;
|
||||
while (cursor < wiiPathStr.size()) {
|
||||
const size_t slash = wiiPathStr.find('/', cursor);
|
||||
const size_t end = slash == std::string::npos ? wiiPathStr.size() : slash;
|
||||
if (end != cursor) {
|
||||
hostPath /= wiiPathStr.substr(cursor, end - cursor);
|
||||
}
|
||||
cursor = end + 1;
|
||||
}
|
||||
return hostPath;
|
||||
}
|
||||
@@ -169,7 +190,7 @@ static std::string NormalizeAbsoluteWiiPath(const char* wiiPath) {
|
||||
struct RiivolutionSaveRedirect {
|
||||
bool enabled = false;
|
||||
bool clone = false;
|
||||
std::string hostDirectory;
|
||||
std::filesystem::path hostDirectory;
|
||||
};
|
||||
|
||||
static std::once_flag g_riivolutionSaveRedirectOnce;
|
||||
@@ -187,7 +208,7 @@ static const RiivolutionSaveRedirect& GetRiivolutionSaveRedirect() {
|
||||
}
|
||||
g_riivolutionSaveRedirect.enabled = true;
|
||||
g_riivolutionSaveRedirect.clone = redirect->clone;
|
||||
g_riivolutionSaveRedirect.hostDirectory = redirect->hostDirectory.string();
|
||||
g_riivolutionSaveRedirect.hostDirectory = redirect->hostDirectory;
|
||||
// Riivolution creates the redirect folder if it does not exist.
|
||||
CreateDirectoryPath(g_riivolutionSaveRedirect.hostDirectory);
|
||||
});
|
||||
@@ -195,8 +216,8 @@ static const RiivolutionSaveRedirect& GetRiivolutionSaveRedirect() {
|
||||
return g_riivolutionSaveRedirect;
|
||||
}
|
||||
|
||||
static void CloneRiivolutionSaveIfNeeded(const std::string& sourceHostPath,
|
||||
const std::string& redirectedHostPath,
|
||||
static void CloneRiivolutionSaveIfNeeded(const std::filesystem::path& sourceHostPath,
|
||||
const std::filesystem::path& redirectedHostPath,
|
||||
const RiivolutionSaveRedirect& redirect) {
|
||||
if (!redirect.clone || PathExists(redirectedHostPath) || !PathExists(sourceHostPath)) {
|
||||
return;
|
||||
@@ -209,11 +230,13 @@ static void CloneRiivolutionSaveIfNeeded(const std::string& sourceHostPath,
|
||||
std::filesystem::copy_options::skip_existing, ec);
|
||||
if (ec) {
|
||||
LogNandWarning("RiivolutionSave", "WARNING: failed to clone '%s' -> '%s': %s",
|
||||
sourceHostPath.c_str(), redirectedHostPath.c_str(), ec.message().c_str());
|
||||
HostPathText(sourceHostPath).c_str(),
|
||||
HostPathText(redirectedHostPath).c_str(), ec.message().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static bool ResolveRiivolutionSaveHostPath(const std::string& absoluteWiiPath, std::string& outHostPath) {
|
||||
static bool ResolveRiivolutionSaveHostPath(const std::string& absoluteWiiPath,
|
||||
std::filesystem::path& outHostPath) {
|
||||
const RiivolutionSaveRedirect& redirect = GetRiivolutionSaveRedirect();
|
||||
if (!redirect.enabled) {
|
||||
return false;
|
||||
@@ -232,23 +255,23 @@ static bool ResolveRiivolutionSaveHostPath(const std::string& absoluteWiiPath, s
|
||||
relative = absoluteWiiPath.substr(dataDir.size() + 1);
|
||||
}
|
||||
|
||||
std::filesystem::path redirected(redirect.hostDirectory);
|
||||
std::filesystem::path redirected = redirect.hostDirectory;
|
||||
if (!relative.empty()) {
|
||||
redirected /= std::filesystem::path(relative);
|
||||
}
|
||||
|
||||
outHostPath = redirected.string();
|
||||
outHostPath = redirected;
|
||||
CloneRiivolutionSaveIfNeeded(BuildHostNandPath(absoluteWiiPath), outHostPath, redirect);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string TranslateNandPath(const char* wiiPath) {
|
||||
std::filesystem::path TranslateNandPath(const char* wiiPath) {
|
||||
std::string wiiPathStr = NormalizeAbsoluteWiiPath(wiiPath);
|
||||
if (wiiPathStr.empty()) {
|
||||
return "";
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string redirectedHostPath;
|
||||
std::filesystem::path redirectedHostPath;
|
||||
if (ResolveRiivolutionSaveHostPath(wiiPathStr, redirectedHostPath)) {
|
||||
return redirectedHostPath;
|
||||
}
|
||||
@@ -266,7 +289,8 @@ struct U8Node {
|
||||
uint32_t size;
|
||||
};
|
||||
|
||||
static bool ExtractFromU8(const std::string& archivePath, const char* targetName, std::vector<uint8_t>& outData) {
|
||||
static bool ExtractFromU8(const std::filesystem::path& archivePath, const char* targetName,
|
||||
std::vector<uint8_t>& outData) {
|
||||
std::ifstream file(archivePath, std::ios::binary);
|
||||
if (!file) {
|
||||
return false;
|
||||
@@ -352,13 +376,13 @@ static bool ExtractFromU8(const std::string& archivePath, const char* targetName
|
||||
// as an argument and the path predicate below hard-codes the same name.
|
||||
static constexpr char kFaceLibResourceName[] = "RFL_Res.dat";
|
||||
|
||||
bool SeedFaceLibResource(const std::string& hostPath) {
|
||||
bool SeedFaceLibResource(const std::filesystem::path& hostPath) {
|
||||
std::vector<uint8_t> payload;
|
||||
if (const auto dvdRoot = RuntimeConfigFile::ResolvedDvdRoot(); !dvdRoot.empty()) {
|
||||
const auto arcPath = dvdRoot / "files" / "contents" / "RFLRes01.arc";
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(arcPath, ec)) {
|
||||
ExtractFromU8(arcPath.string(), kFaceLibResourceName, payload);
|
||||
ExtractFromU8(arcPath, kFaceLibResourceName, payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,12 +395,12 @@ bool SeedFaceLibResource(const std::string& hostPath) {
|
||||
|
||||
std::ofstream out(hostPath, std::ios::binary);
|
||||
if (!out) {
|
||||
LogNandError("FaceLibSeed", "Failed to create %s", hostPath.c_str());
|
||||
LogNandError("FaceLibSeed", "Failed to create %s", HostPathText(hostPath).c_str());
|
||||
return false;
|
||||
}
|
||||
out.write(reinterpret_cast<const char*>(payload.data()), static_cast<std::streamsize>(payload.size()));
|
||||
if (!out) {
|
||||
LogNandError("FaceLibSeed", "Failed to write %s", hostPath.c_str());
|
||||
LogNandError("FaceLibSeed", "Failed to write %s", HostPathText(hostPath).c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -388,45 +412,33 @@ bool IsFaceLibResourcePath(const char* path) {
|
||||
}
|
||||
|
||||
// Create directories recursively
|
||||
bool CreateDirectoryPath(const std::string& path) {
|
||||
bool CreateDirectoryPath(const std::filesystem::path& path) {
|
||||
if (path.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(path, ec);
|
||||
return !ec || std::filesystem::is_directory(path);
|
||||
return !ec || std::filesystem::is_directory(path, ec);
|
||||
}
|
||||
|
||||
// Check if a path exists
|
||||
bool PathExists(const std::string& path) {
|
||||
#ifdef _WIN32
|
||||
return GetFileAttributesA(path.c_str()) != INVALID_FILE_ATTRIBUTES;
|
||||
#else
|
||||
return access(path.c_str(), F_OK) == 0;
|
||||
#endif
|
||||
bool PathExists(const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
return std::filesystem::exists(path, ec) && !ec;
|
||||
}
|
||||
|
||||
// Check if path is a directory
|
||||
bool IsDirectory(const std::string& path) {
|
||||
#ifdef _WIN32
|
||||
DWORD attrs = GetFileAttributesA(path.c_str());
|
||||
return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY);
|
||||
#else
|
||||
struct stat st;
|
||||
return stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode);
|
||||
#endif
|
||||
bool IsDirectory(const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
return std::filesystem::is_directory(path, ec) && !ec;
|
||||
}
|
||||
|
||||
bool CreateParentDirectories(const std::string& path) {
|
||||
size_t lastSlash = path.rfind('\\');
|
||||
if (lastSlash == std::string::npos) {
|
||||
lastSlash = path.rfind('/');
|
||||
}
|
||||
if (lastSlash == std::string::npos) {
|
||||
bool CreateParentDirectories(const std::filesystem::path& path) {
|
||||
if (!path.has_parent_path()) {
|
||||
return false;
|
||||
}
|
||||
CreateDirectoryPath(path.substr(0, lastSlash));
|
||||
CreateDirectoryPath(path.parent_path());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,15 +29,10 @@
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
#include <io.h>
|
||||
#include <windows.h>
|
||||
#define mkdir(path, mode) _mkdir(path)
|
||||
#define access _access
|
||||
#define F_OK 0
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <sys/types.h>
|
||||
@@ -67,19 +62,19 @@ void LogNandWarning(const char* func, const char* fmt, ...);
|
||||
|
||||
struct FileHandle {
|
||||
FILE* file = nullptr;
|
||||
std::string path;
|
||||
std::filesystem::path path;
|
||||
int32_t mode = 0; // 1=read, 2=write, 3=read/write
|
||||
uint32_t position = 0;
|
||||
// Non-empty only for write-mode NANDSafeOpen handles. `path` then points at the
|
||||
// sibling scratch file the guest is writing into, and this is the original file it
|
||||
// atomically replaces on NANDSafeClose.
|
||||
std::string safeCommitPath;
|
||||
std::filesystem::path safeCommitPath;
|
||||
};
|
||||
|
||||
extern std::map<int32_t, FileHandle> g_fileHandles;
|
||||
extern std::mutex g_fdMutex;
|
||||
|
||||
int32_t AllocateFd(const std::string& path, FILE* file, int32_t mode);
|
||||
int32_t AllocateFd(const std::filesystem::path& path, FILE* file, int32_t mode);
|
||||
FileHandle* GetHandle(int32_t fd);
|
||||
void CloseFd(int32_t fd);
|
||||
|
||||
@@ -89,18 +84,27 @@ void CloseFd(int32_t fd);
|
||||
|
||||
uint32_t CurrentMkwTitleIdLo();
|
||||
std::string CurrentNandDataDir();
|
||||
const std::string& GetNandBasePath();
|
||||
std::string TranslateNandPath(const char* wiiPath);
|
||||
const std::filesystem::path& GetNandBasePath();
|
||||
std::filesystem::path TranslateNandPath(const char* wiiPath);
|
||||
|
||||
bool CreateDirectoryPath(const std::string& path);
|
||||
bool PathExists(const std::string& path);
|
||||
bool IsDirectory(const std::string& path);
|
||||
bool CreateDirectoryPath(const std::filesystem::path& path);
|
||||
bool PathExists(const std::filesystem::path& path);
|
||||
bool IsDirectory(const std::filesystem::path& path);
|
||||
|
||||
// Create the directory that contains `path`. False when `path` has no directory
|
||||
// component, i.e. there was nothing to create.
|
||||
bool CreateParentDirectories(const std::string& path);
|
||||
bool CreateParentDirectories(const std::filesystem::path& path);
|
||||
|
||||
bool SeedFaceLibResource(const std::string& hostPath);
|
||||
// Host paths keep their native encoding end to end; these are the only places a NAND
|
||||
// path is narrowed, and they narrow to UTF-8 for display.
|
||||
std::string HostPathText(const std::filesystem::path& path);
|
||||
|
||||
// fopen takes an ANSI-codepage name on Windows, which cannot express every path.
|
||||
FILE* NandFopen(const std::filesystem::path& path, const char* mode);
|
||||
bool NandRemove(const std::filesystem::path& path);
|
||||
bool NandRename(const std::filesystem::path& from, const std::filesystem::path& to);
|
||||
|
||||
bool SeedFaceLibResource(const std::filesystem::path& hostPath);
|
||||
bool IsFaceLibResourcePath(const char* path);
|
||||
|
||||
// ============================================================================
|
||||
@@ -178,9 +182,9 @@ enum ISFSResult {
|
||||
int32_t ISFS_OpenLib_Initialize(CpuContext* ctx);
|
||||
|
||||
// Shadow-write machinery, defined with the NANDSafeOpen/NANDSafeClose section below.
|
||||
std::string SafeTempPathFor(const std::string& hostPath);
|
||||
bool DiscardStaleSafeTemp(const std::string& tempPath);
|
||||
bool IsHostPathOpen(const std::string& hostPath);
|
||||
std::filesystem::path SafeTempPathFor(const std::filesystem::path& hostPath);
|
||||
bool DiscardStaleSafeTemp(const std::filesystem::path& tempPath);
|
||||
bool IsHostPathOpen(const std::filesystem::path& hostPath);
|
||||
int32_t CommitAndCloseFd(const char* who, int32_t fd, bool missingFdIsError);
|
||||
|
||||
// Synchronous NAND library entry points (defined in nand_api.cpp); the async
|
||||
|
||||
@@ -329,7 +329,7 @@ extern "C" int32_t NAND_IOS_Open_HLE(uint32_t pathPtr, uint32_t mode) {
|
||||
}
|
||||
|
||||
// It's a NAND file path
|
||||
std::string hostPath = TranslateNandPath(path);
|
||||
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||
|
||||
// Seed FaceLib resources before the existence check so every open mode can
|
||||
// still find them on a fresh managed NAND.
|
||||
@@ -346,16 +346,16 @@ extern "C" int32_t NAND_IOS_Open_HLE(uint32_t pathPtr, uint32_t mode) {
|
||||
if (mode == 2 || mode == 3) {
|
||||
if (!PathExists(hostPath)) {
|
||||
LogNandWarning("IOS_Open", "'%s' does not exist; open mode %u never creates it",
|
||||
hostPath.c_str(), mode);
|
||||
HostPathText(hostPath).c_str(), mode);
|
||||
return ISFS_ENOENT;
|
||||
}
|
||||
fopenMode = "r+b"; // Write-only opens still need read for seeks
|
||||
}
|
||||
|
||||
FILE* file = std::fopen(hostPath.c_str(), fopenMode);
|
||||
FILE* file = NandFopen(hostPath, fopenMode);
|
||||
|
||||
if (!file) {
|
||||
LogNandError("IOS_Open", "FAILED to open '%s'", hostPath.c_str());
|
||||
LogNandError("IOS_Open", "FAILED to open '%s'", HostPathText(hostPath).c_str());
|
||||
return ISFS_ENOENT;
|
||||
}
|
||||
|
||||
@@ -516,14 +516,9 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
|
||||
return ISFS_EINVAL;
|
||||
}
|
||||
const char* path = (const char*)Memory::GetPointer(inBufPtr + 6);
|
||||
std::string hostPath = TranslateNandPath(path);
|
||||
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||
|
||||
if (CreateDirectoryPath(hostPath)) {
|
||||
#ifdef _WIN32
|
||||
_mkdir(hostPath.c_str());
|
||||
#else
|
||||
mkdir(hostPath.c_str(), 0755);
|
||||
#endif
|
||||
return ISFS_OK;
|
||||
}
|
||||
return ISFS_EIO;
|
||||
@@ -534,16 +529,11 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
|
||||
return ISFS_EINVAL;
|
||||
}
|
||||
const char* path = (const char*)Memory::GetPointer(inBufPtr);
|
||||
std::string hostPath = TranslateNandPath(path);
|
||||
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||
|
||||
if (IsDirectory(hostPath)) {
|
||||
#ifdef _WIN32
|
||||
if (RemoveDirectoryA(hostPath.c_str())) return ISFS_OK;
|
||||
#else
|
||||
if (rmdir(hostPath.c_str()) == 0) return ISFS_OK;
|
||||
#endif
|
||||
} else {
|
||||
if (std::remove(hostPath.c_str()) == 0) return ISFS_OK;
|
||||
// fs::remove refuses a non-empty directory, matching rmdir.
|
||||
if (NandRemove(hostPath)) {
|
||||
return ISFS_OK;
|
||||
}
|
||||
return ISFS_ENOENT;
|
||||
}
|
||||
@@ -553,7 +543,7 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
|
||||
return ISFS_EINVAL;
|
||||
}
|
||||
const char* path = (const char*)Memory::GetPointer(inBufPtr);
|
||||
std::string hostPath = TranslateNandPath(path);
|
||||
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||
|
||||
if (!PathExists(hostPath)) {
|
||||
return ISFS_ENOENT;
|
||||
@@ -582,11 +572,11 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
|
||||
return ISFS_EINVAL;
|
||||
}
|
||||
const char* path = (const char*)Memory::GetPointer(inBufPtr + 6);
|
||||
std::string hostPath = TranslateNandPath(path);
|
||||
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||
CreateParentDirectories(hostPath);
|
||||
|
||||
// Create empty file
|
||||
FILE* f = std::fopen(hostPath.c_str(), "wb");
|
||||
FILE* f = NandFopen(hostPath, "wb");
|
||||
if (f) {
|
||||
std::fclose(f);
|
||||
return ISFS_OK;
|
||||
@@ -606,10 +596,10 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
|
||||
}
|
||||
const char* srcPath = (const char*)Memory::GetPointer(inBufPtr);
|
||||
const char* dstPath = (const char*)Memory::GetPointer(inBufPtr + 0x40);
|
||||
std::string srcHost = TranslateNandPath(srcPath);
|
||||
std::string dstHost = TranslateNandPath(dstPath);
|
||||
const std::filesystem::path srcHost = TranslateNandPath(srcPath);
|
||||
const std::filesystem::path dstHost = TranslateNandPath(dstPath);
|
||||
|
||||
if (std::rename(srcHost.c_str(), dstHost.c_str()) == 0) {
|
||||
if (NandRename(srcHost, dstHost)) {
|
||||
return ISFS_OK;
|
||||
}
|
||||
return ISFS_EIO;
|
||||
@@ -805,11 +795,11 @@ int32_t ISFS_OpenLib_Initialize(CpuContext* ctx) {
|
||||
g_isfsInitialized = true;
|
||||
|
||||
// Create the title data directory if it doesn't exist
|
||||
char titlePath[256];
|
||||
const std::string& base = GetNandBasePath();
|
||||
std::snprintf(titlePath, sizeof(titlePath), "%s\\title\\%08x\\%08x\\data",
|
||||
base.c_str(), kNandTitleIdHi, CurrentMkwTitleIdLo());
|
||||
CreateDirectoryPath(titlePath);
|
||||
char titleId[32];
|
||||
std::snprintf(titleId, sizeof(titleId), "%08x", kNandTitleIdHi);
|
||||
char gameId[32];
|
||||
std::snprintf(gameId, sizeof(gameId), "%08x", CurrentMkwTitleIdLo());
|
||||
CreateDirectoryPath(GetNandBasePath() / "title" / titleId / gameId / "data");
|
||||
|
||||
if (!ctx) {
|
||||
return ISFS_OK;
|
||||
@@ -899,6 +889,74 @@ REGISTER_NATIVE_FUNCTION_AS(0x80169BCC, ISFS_OpenLib_HLE_80169BCC, "ISFS_OpenLib
|
||||
// IOS_Ioctlv HLE - Vector Ioctl for complex ISFS operations
|
||||
// ============================================================================
|
||||
|
||||
static int32_t HandleIsfsReadDir(uint32_t numIn, uint32_t numOut, uint32_t vectorPtr) {
|
||||
const bool countOnly = (numIn == 1 && numOut == 1);
|
||||
if (!countOnly && !(numIn == 2 && numOut == 2)) {
|
||||
LogNandWarning("IOS_Ioctlv", "READDIR unsupported vector shape numIn=%u numOut=%u",
|
||||
numIn, numOut);
|
||||
return ISFS_EINVAL;
|
||||
}
|
||||
|
||||
const IosVector pathVec = ReadIosVector(vectorPtr, 0);
|
||||
const std::string wiiPath = ReadGuestCString(pathVec.address, 64);
|
||||
if (wiiPath.empty()) {
|
||||
return ISFS_EINVAL;
|
||||
}
|
||||
const std::filesystem::path hostPath = TranslateNandPath(wiiPath.c_str());
|
||||
if (!IsDirectory(hostPath)) {
|
||||
return ISFS_ENOENT;
|
||||
}
|
||||
|
||||
// NAND names are at most 12 characters; longer host names cannot exist on
|
||||
// a real NAND (this also hides *.nandsafe.tmp write shadows).
|
||||
constexpr size_t kMaxNandNameLength = 12;
|
||||
std::vector<std::string> names;
|
||||
std::error_code ec;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(hostPath, ec)) {
|
||||
std::string name = HostPathText(entry.path().filename());
|
||||
if (name.empty() || name.size() > kMaxNandNameLength) {
|
||||
continue;
|
||||
}
|
||||
names.push_back(std::move(name));
|
||||
}
|
||||
std::sort(names.begin(), names.end());
|
||||
|
||||
if (countOnly) {
|
||||
const IosVector countOut = ReadIosVector(vectorPtr, 1);
|
||||
if (countOut.size < 4 || !Memory::Contains(countOut.address, 4)) {
|
||||
return ISFS_EINVAL;
|
||||
}
|
||||
Memory::Write32(countOut.address, static_cast<uint32_t>(names.size()));
|
||||
return ISFS_OK;
|
||||
}
|
||||
|
||||
const IosVector maxVec = ReadIosVector(vectorPtr, 1);
|
||||
const IosVector namesOut = ReadIosVector(vectorPtr, 2);
|
||||
const IosVector countOut = ReadIosVector(vectorPtr, 3);
|
||||
if (maxVec.size < 4 || !Memory::Contains(maxVec.address, 4) ||
|
||||
countOut.size < 4 || !Memory::Contains(countOut.address, 4) ||
|
||||
!IsValidGuestRange(namesOut.address, namesOut.size)) {
|
||||
return ISFS_EINVAL;
|
||||
}
|
||||
const uint32_t maxCount = Memory::Read32(maxVec.address);
|
||||
|
||||
constexpr uint32_t kEntryWindow = 13; // 12 chars + terminator
|
||||
uint32_t cursor = 0;
|
||||
uint32_t written = 0;
|
||||
for (const std::string& name : names) {
|
||||
if (written >= maxCount || cursor + kEntryWindow > namesOut.size) {
|
||||
break;
|
||||
}
|
||||
uint8_t* out = Memory::GetPointer(namesOut.address + cursor, kEntryWindow);
|
||||
std::memset(out, 0, kEntryWindow);
|
||||
std::memcpy(out, name.data(), name.size());
|
||||
cursor += static_cast<uint32_t>(name.size()) + 1;
|
||||
++written;
|
||||
}
|
||||
Memory::Write32(countOut.address, written);
|
||||
return ISFS_OK;
|
||||
}
|
||||
|
||||
extern "C" int32_t NAND_IOS_Ioctlv_HLE(
|
||||
uint32_t fd,
|
||||
uint32_t cmd,
|
||||
@@ -919,6 +977,16 @@ extern "C" int32_t NAND_IOS_Ioctlv_HLE(
|
||||
return HandleDolphinIoctlv(cmd, numIn, numOut, vectorPtr);
|
||||
}
|
||||
|
||||
if (fd == ISFS_DEV_FD) {
|
||||
if (!vectorPtr || !Memory::Contains(vectorPtr, static_cast<size_t>(numIn + numOut) * 8u)) {
|
||||
return ISFS_EINVAL;
|
||||
}
|
||||
if (cmd == ISFS_IOCTL_READDIR) {
|
||||
return HandleIsfsReadDir(numIn, numOut, vectorPtr);
|
||||
}
|
||||
return ISFS_OK;
|
||||
}
|
||||
|
||||
if (fd == ES_DEV_FD) {
|
||||
if (!vectorPtr || !Memory::Contains(vectorPtr, static_cast<size_t>(numIn + numOut) * 8u)) {
|
||||
return ISFS_EINVAL;
|
||||
|
||||
@@ -74,8 +74,18 @@ std::string RiivoGameId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
// Every narrow path string here is UTF-8, including the ones the XML halves of
|
||||
// resolved paths are concatenated with.
|
||||
using RuntimeConfigFile::PathFromUtf8;
|
||||
using RuntimeConfigFile::PathToUtf8;
|
||||
|
||||
std::string RiivoGenericText(const fs::path& path) {
|
||||
const std::u8string text = path.generic_u8string();
|
||||
return std::string(text.begin(), text.end());
|
||||
}
|
||||
|
||||
std::string RiivoComparablePath(const fs::path& path) {
|
||||
std::string text = path.lexically_normal().generic_string();
|
||||
std::string text = RiivoGenericText(path.lexically_normal());
|
||||
#ifdef _WIN32
|
||||
RuntimeHle::LowerInPlace(text);
|
||||
#endif
|
||||
@@ -86,6 +96,8 @@ void RiivoAddRoot(std::vector<RuntimeRiivolution::Overlay>& overlays, fs::path r
|
||||
const char* source) {
|
||||
std::error_code ec;
|
||||
if (!fs::is_directory(root, ec)) {
|
||||
RT_LOG(RT_TAG_RIIVOLUTION) << "rejected overlay root (" << (source ? source : "unknown")
|
||||
<< "): " << PathToUtf8(root) << " is not a reachable directory" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -104,7 +116,7 @@ void RiivoAddRoot(std::vector<RuntimeRiivolution::Overlay>& overlays, fs::path r
|
||||
}
|
||||
|
||||
RT_LOG(RT_TAG_RIIVOLUTION) << "overlay root (" << (source ? source : "unknown")
|
||||
<< "): " << normalized.string() << std::endl;
|
||||
<< "): " << PathToUtf8(normalized) << std::endl;
|
||||
overlays.push_back({std::move(normalized), std::nullopt});
|
||||
}
|
||||
|
||||
@@ -133,7 +145,7 @@ std::vector<RuntimeRiivolution::Overlay> RiivoDiscoverRoots() {
|
||||
}
|
||||
|
||||
for (const auto& root : RecompMod::DvdOverlayRoots()) {
|
||||
RiivoAddRoot(overlays, fs::path(root), "recomp mod manifest");
|
||||
RiivoAddRoot(overlays, root, "recomp mod manifest");
|
||||
}
|
||||
|
||||
return overlays;
|
||||
@@ -154,7 +166,7 @@ std::optional<RiivoXmlSet> RiivoFindXmls(const fs::path& overlayRoot) {
|
||||
// <sd>/RetroRewind6), so externals resolve against the root's parent.
|
||||
const std::string& configured = RecompMod::RiivolutionXml();
|
||||
if (!configured.empty()) {
|
||||
const fs::path configuredXml = overlayRoot / fs::path(configured);
|
||||
const fs::path configuredXml = overlayRoot / PathFromUtf8(configured);
|
||||
if (fs::is_regular_file(configuredXml, ec)) {
|
||||
return RiivoXmlSet{overlayRoot.parent_path(), {configuredXml}};
|
||||
}
|
||||
@@ -211,7 +223,7 @@ void RiivoCollectMappings(const RiivolutionContract::Patch& patch, const std::st
|
||||
++set.skippedExternals;
|
||||
continue;
|
||||
}
|
||||
const fs::path hostFile(*resolved);
|
||||
const fs::path hostFile = PathFromUtf8(*resolved);
|
||||
if (!fs::is_regular_file(hostFile, ec)) {
|
||||
++set.skippedExternals;
|
||||
continue;
|
||||
@@ -227,7 +239,7 @@ void RiivoCollectMappings(const RiivolutionContract::Patch& patch, const std::st
|
||||
++set.skippedExternals;
|
||||
continue;
|
||||
}
|
||||
const fs::path hostFolder(*resolved);
|
||||
const fs::path hostFolder = PathFromUtf8(*resolved);
|
||||
if (!fs::is_directory(hostFolder, ec)) {
|
||||
++set.skippedExternals;
|
||||
continue;
|
||||
@@ -246,7 +258,7 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
|
||||
}
|
||||
|
||||
const std::string gameId = RiivoGameId();
|
||||
const std::string sdRootGeneric = xmlSet->sdRoot.generic_string();
|
||||
const std::string sdRootGeneric = RiivoGenericText(xmlSet->sdRoot);
|
||||
const auto manifestSelections = RiivoManifestSelections();
|
||||
|
||||
// Dolphin-compatible remembered choices; a recomp.yml pin overrides them.
|
||||
@@ -261,19 +273,20 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
|
||||
for (const fs::path& xmlFile : xmlSet->xmlFiles) {
|
||||
const auto text = RiivoReadFile(xmlFile);
|
||||
if (!text) {
|
||||
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: cannot read " << xmlFile.string() << std::endl;
|
||||
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: cannot read " << PathToUtf8(xmlFile)
|
||||
<< std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto disc = RiivolutionContract::ParseString(*text);
|
||||
if (!disc) {
|
||||
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: " << xmlFile.string()
|
||||
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: " << PathToUtf8(xmlFile)
|
||||
<< " is not a valid Riivolution XML (version 1 wiidisc); ignoring it"
|
||||
<< std::endl;
|
||||
continue;
|
||||
}
|
||||
if (!disc->IsValidForGame(gameId, std::nullopt, std::nullopt)) {
|
||||
RT_LOG(RT_TAG_RIIVOLUTION) << xmlFile.string() << ": not valid for " << gameId
|
||||
RT_LOG(RT_TAG_RIIVOLUTION) << PathToUtf8(xmlFile) << ": not valid for " << gameId
|
||||
<< ", skipped" << std::endl;
|
||||
continue;
|
||||
}
|
||||
@@ -284,7 +297,7 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
|
||||
RiivolutionContract::ApplySelections(*disc, manifestSelections);
|
||||
|
||||
const auto activePatches = disc->GeneratePatches(gameId);
|
||||
const std::string xmlDirGeneric = xmlFile.parent_path().generic_string();
|
||||
const std::string xmlDirGeneric = RiivoGenericText(xmlFile.parent_path());
|
||||
|
||||
const size_t before = set.mappings.size();
|
||||
for (const auto& patch : activePatches) {
|
||||
@@ -297,9 +310,10 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
|
||||
if (const auto resolvedSave = RiivolutionContract::MakeAbsoluteFromRelative(
|
||||
sdRootGeneric, xmlDirGeneric, savegame->external)) {
|
||||
state.saveRedirect =
|
||||
RuntimeRiivolution::SaveRedirect{fs::path(*resolvedSave), savegame->clone};
|
||||
RuntimeRiivolution::SaveRedirect{PathFromUtf8(*resolvedSave),
|
||||
savegame->clone};
|
||||
RT_LOG(RT_TAG_RIIVOLUTION) << "savegame redirect: "
|
||||
<< state.saveRedirect->hostDirectory.string()
|
||||
<< PathToUtf8(state.saveRedirect->hostDirectory)
|
||||
<< (savegame->clone ? " (clone)" : "") << std::endl;
|
||||
}
|
||||
}
|
||||
@@ -308,11 +322,11 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
|
||||
// A pack whose XML parses but activates nothing is the most confusing
|
||||
// failure this layer has: the game boots, plays, and quietly shows
|
||||
// vanilla content. Always say what happened.
|
||||
RT_LOG(RT_TAG_RIIVOLUTION) << xmlFile.string() << ": " << activePatches.size()
|
||||
RT_LOG(RT_TAG_RIIVOLUTION) << PathToUtf8(xmlFile) << ": " << activePatches.size()
|
||||
<< " active patch(es), " << (set.mappings.size() - before) << " mapping(s)"
|
||||
<< std::endl;
|
||||
if (activePatches.empty()) {
|
||||
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: " << xmlFile.string()
|
||||
RT_LOG(RT_TAG_RIIVOLUTION) << "WARNING: " << PathToUtf8(xmlFile)
|
||||
<< " has no enabled options for " << gameId
|
||||
<< "; check the riivolution option selections (recomp.yml) or "
|
||||
<< sdRootGeneric << "/riivolution/config/" << gameId.substr(0, 4) << ".xml"
|
||||
@@ -321,7 +335,7 @@ std::optional<RuntimeRiivolution::PatchSet> RiivoLoadPatchSet(const fs::path& ov
|
||||
}
|
||||
|
||||
if (set.skippedExternals != 0) {
|
||||
RT_LOG(RT_TAG_RIIVOLUTION) << overlayRoot.string() << ": skipped "
|
||||
RT_LOG(RT_TAG_RIIVOLUTION) << PathToUtf8(overlayRoot) << ": skipped "
|
||||
<< set.skippedExternals << " mapping(s) whose external path does not exist"
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
+35
-11
@@ -47,6 +47,7 @@
|
||||
#include "system_bridge.h"
|
||||
#include "ppc_runtime.h"
|
||||
#include "aurora_events.h"
|
||||
#include "wup028_adapter.h"
|
||||
#include "fiber_manager.h"
|
||||
#include "hle_stubs.h"
|
||||
#include "runtime_config.h"
|
||||
@@ -394,7 +395,7 @@ void InitializeProcessTranscript(int argc, char** argv) {
|
||||
}
|
||||
|
||||
const std::filesystem::path path = GetRunLogDirectory() / "console.log";
|
||||
state.file.open(path.string(), std::ios::out | std::ios::trunc | std::ios::binary);
|
||||
state.file.open(path, std::ios::out | std::ios::trunc | std::ios::binary);
|
||||
if (!state.file) {
|
||||
return;
|
||||
}
|
||||
@@ -570,14 +571,31 @@ std::string FormatHostStackTrace(unsigned framesToSkip) {
|
||||
for (USHORT i = 0; i < captured; ++i) {
|
||||
const DWORD64 addr = reinterpret_cast<DWORD64>(frames[i]);
|
||||
HMODULE module = nullptr;
|
||||
char modulePath[MAX_PATH] = "?";
|
||||
std::string modulePath = "?";
|
||||
DWORD64 moduleBase = 0;
|
||||
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCSTR>(frames[i]),
|
||||
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(frames[i]),
|
||||
&module) != 0 &&
|
||||
module != nullptr) {
|
||||
moduleBase = reinterpret_cast<DWORD64>(module);
|
||||
(void)GetModuleFileNameA(module, modulePath, MAX_PATH);
|
||||
std::wstring modulePathBuffer(MAX_PATH, L'\0');
|
||||
for (;;) {
|
||||
const DWORD length =
|
||||
GetModuleFileNameW(module, modulePathBuffer.data(), static_cast<DWORD>(modulePathBuffer.size()));
|
||||
if (length == 0) {
|
||||
break;
|
||||
}
|
||||
if (length < modulePathBuffer.size()) {
|
||||
modulePathBuffer.resize(length);
|
||||
modulePath = RuntimeConfigFile::PathToUtf8(std::filesystem::path(modulePathBuffer));
|
||||
break;
|
||||
}
|
||||
// Truncated; retry with a larger buffer up to the extended path limit.
|
||||
if (modulePathBuffer.size() >= 32768) {
|
||||
break;
|
||||
}
|
||||
modulePathBuffer.resize(modulePathBuffer.size() * 2);
|
||||
}
|
||||
}
|
||||
|
||||
const char* symbolName = "?";
|
||||
@@ -641,7 +659,7 @@ void WriteFatalLogImpl(std::string_view reason, std::string_view extraDetails =
|
||||
std::string fileName = "crash_";
|
||||
fileName.append(reason);
|
||||
fileName.append(".txt");
|
||||
std::ofstream out((runDirectory / fileName).string(), std::ios::out | std::ios::trunc);
|
||||
std::ofstream out(runDirectory / fileName, std::ios::out | std::ios::trunc);
|
||||
if (!out) {
|
||||
return;
|
||||
}
|
||||
@@ -680,11 +698,12 @@ void WriteFatalLogImpl(std::string_view reason, std::string_view extraDetails =
|
||||
// are large.
|
||||
static std::atomic_bool s_memorySnapshotWritten{false};
|
||||
if (!s_memorySnapshotWritten.exchange(true, std::memory_order_acq_rel)) {
|
||||
SystemBridge::WriteGuestMemorySnapshot(out, (runDirectory / "mem1.bin").string().c_str());
|
||||
SystemBridge::WriteGuestMemorySnapshot(out, runDirectory / "mem1.bin");
|
||||
}
|
||||
|
||||
out.flush();
|
||||
RT_LOG(RT_TAG_RUNTIME) << "crash artifacts written to " << runDirectory.string() << std::endl;
|
||||
RT_LOG(RT_TAG_RUNTIME) << "crash artifacts written to "
|
||||
<< RuntimeConfigFile::PathToUtf8(runDirectory) << std::endl;
|
||||
}
|
||||
|
||||
void SetRuntimeExitCodeImpl(int code) {
|
||||
@@ -1183,10 +1202,11 @@ int RuntimeMain(int argc, char** argv) {
|
||||
std::filesystem::create_directories(rendererCacheDirectory, rendererPathError);
|
||||
if (rendererPathError) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "Unable to create renderer cache directory "
|
||||
<< rendererCacheDirectory << ": " << rendererPathError.message() << std::endl;
|
||||
<< RuntimeConfigFile::PathToUtf8(rendererCacheDirectory) << ": "
|
||||
<< rendererPathError.message() << std::endl;
|
||||
}
|
||||
const std::string auroraUserPath = applicationDataDirectory.string();
|
||||
const std::string auroraCachePath = rendererCacheDirectory.string();
|
||||
const std::string auroraUserPath = RuntimeConfigFile::PathToUtf8(applicationDataDirectory);
|
||||
const std::string auroraCachePath = RuntimeConfigFile::PathToUtf8(rendererCacheDirectory);
|
||||
auroraConfig.userPath = auroraUserPath.c_str();
|
||||
auroraConfig.cachePath = auroraCachePath.c_str();
|
||||
auroraConfig.logCallback = &RuntimeAuroraLogCallback;
|
||||
@@ -1252,6 +1272,7 @@ int RuntimeMain(int argc, char** argv) {
|
||||
}
|
||||
aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait);
|
||||
GxGuestWrite::InstallAuroraHooks();
|
||||
Wup028Adapter::Initialize();
|
||||
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
|
||||
auroraInfo.windowSize.native_fb_height);
|
||||
settings_overlay::InitializeRuntimeSettings();
|
||||
@@ -1293,6 +1314,7 @@ int RuntimeMain(int argc, char** argv) {
|
||||
// Shutdown fiber system
|
||||
Fiber::GuestFiberManager::Shutdown();
|
||||
WindowPlacementPersistence::Flush(true);
|
||||
Wup028Adapter::Shutdown();
|
||||
aurora_shutdown();
|
||||
SetRuntimeExitCodeImpl(0);
|
||||
ShutdownProcessTranscript();
|
||||
@@ -1310,6 +1332,7 @@ int RuntimeMain(int argc, char** argv) {
|
||||
SetRuntimeExitCodeImpl(1);
|
||||
Fiber::GuestFiberManager::Shutdown();
|
||||
WindowPlacementPersistence::Flush(true);
|
||||
Wup028Adapter::Shutdown();
|
||||
aurora_shutdown();
|
||||
ShutdownProcessTranscript();
|
||||
return 1;
|
||||
@@ -1321,6 +1344,7 @@ int RuntimeMain(int argc, char** argv) {
|
||||
SetRuntimeExitCodeImpl(1);
|
||||
Fiber::GuestFiberManager::Shutdown();
|
||||
WindowPlacementPersistence::Flush(true);
|
||||
Wup028Adapter::Shutdown();
|
||||
aurora_shutdown();
|
||||
ShutdownProcessTranscript();
|
||||
return 1;
|
||||
|
||||
@@ -33,8 +33,8 @@ std::vector<RecompMod::InitializerFn>& PostRelInitializers() {
|
||||
return initializers;
|
||||
}
|
||||
|
||||
std::vector<std::string>& OverlayRoots() {
|
||||
static std::vector<std::string> roots;
|
||||
std::vector<std::filesystem::path>& OverlayRoots() {
|
||||
static std::vector<std::filesystem::path> roots;
|
||||
return roots;
|
||||
}
|
||||
|
||||
@@ -279,17 +279,17 @@ void RegisterDvdOverlayRoot(std::string root) {
|
||||
// against the executable directory instead.
|
||||
const std::filesystem::path base =
|
||||
RuntimeConfigFile::ExecutableDirectory().value_or(std::filesystem::current_path());
|
||||
root = RuntimeConfigFile::ResolveRelativeTo(base, root).string();
|
||||
std::filesystem::path resolved = RuntimeConfigFile::ResolveRelativeTo(base, root);
|
||||
|
||||
std::lock_guard<std::mutex> lock(ModMutex());
|
||||
auto& roots = OverlayRoots();
|
||||
const auto it = std::find(roots.begin(), roots.end(), root);
|
||||
const auto it = std::find(roots.begin(), roots.end(), resolved);
|
||||
if (it == roots.end()) {
|
||||
roots.push_back(std::move(root));
|
||||
roots.push_back(std::move(resolved));
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<std::string>& DvdOverlayRoots() {
|
||||
const std::vector<std::filesystem::path>& DvdOverlayRoots() {
|
||||
return OverlayRoots();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "settings_overlay.h"
|
||||
#include "wup028_adapter.h"
|
||||
#include "audio_backend.h"
|
||||
#include "controller_mapping_wizard.h"
|
||||
#include "game_graphics_options.h"
|
||||
#include "music_attenuation.h"
|
||||
#include "runtime_config.h"
|
||||
@@ -310,6 +312,34 @@ void ApplyConfiguredMappings() {
|
||||
}
|
||||
}
|
||||
|
||||
void DrawGameCubeAdapterInfo() {
|
||||
ImGui::Separator();
|
||||
if (!ImGui::BeginMenu("GameCube adapter info")) return;
|
||||
|
||||
const auto adapter = Wup028Adapter::GetInfo();
|
||||
const char* state = adapter.state == Wup028Adapter::ConnectionState::Connected
|
||||
? "Connected"
|
||||
: adapter.state == Wup028Adapter::ConnectionState::DriverError ? "Driver error"
|
||||
: "Searching";
|
||||
ImGui::Text("Status: %s", state);
|
||||
if (!adapter.deviceName.empty()) {
|
||||
ImGui::Text("Device: %s", adapter.deviceName.c_str());
|
||||
}
|
||||
ImGui::TextWrapped("%s", adapter.detail.c_str());
|
||||
if (adapter.state == Wup028Adapter::ConnectionState::Connected) {
|
||||
ImGui::Text("Poll rate: %.1f reports/s", adapter.pollRateHz);
|
||||
ImGui::Text("Endpoints: IN 0x%02X, OUT 0x%02X", adapter.inputEndpoint, adapter.outputEndpoint);
|
||||
for (size_t port = 0; port < adapter.ports.size(); ++port) {
|
||||
const uint8_t type = adapter.portStatus[port] & 0x30;
|
||||
const char* typeName = type == 0x10 ? "wired" : type == 0x20 ? "wireless" : "none";
|
||||
ImGui::Text("Adapter port %u: %s (type %s, raw 0x%02X)", static_cast<unsigned>(port + 1),
|
||||
adapter.ports[port] ? "Controller connected" : "Empty", typeName,
|
||||
adapter.portStatus[port]);
|
||||
}
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
void DrawControllerSettings() {
|
||||
for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
||||
const std::string label = "Port " + std::to_string(port + 1);
|
||||
@@ -320,20 +350,60 @@ void DrawControllerSettings() {
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
const uint32_t selectedGamePort = static_cast<uint32_t>(g_controllerPort);
|
||||
const int adapterAssignment = Wup028Adapter::GetPortAssignment(selectedGamePort);
|
||||
if (adapterAssignment >= 0) {
|
||||
ImGui::Text("Assigned: GameCube adapter port %d", adapterAssignment + 1);
|
||||
} else {
|
||||
const char* currentName = PADGetName(selectedGamePort);
|
||||
ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None");
|
||||
}
|
||||
if (ImGui::BeginMenu("Assign GameCube adapter port")) {
|
||||
if (ImGui::MenuItem("None", nullptr, adapterAssignment < 0)) {
|
||||
Wup028Adapter::SetPortAssignment(selectedGamePort, -1);
|
||||
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, -1);
|
||||
}
|
||||
const auto adapter = Wup028Adapter::GetInfo();
|
||||
for (int physicalPort = 0; physicalPort < PAD_CHANMAX; ++physicalPort) {
|
||||
const std::string label = "Adapter port " + std::to_string(physicalPort + 1) +
|
||||
(adapter.ports[static_cast<size_t>(physicalPort)] ? " (connected)" : " (empty)");
|
||||
if (ImGui::MenuItem(label.c_str(), nullptr, adapterAssignment == physicalPort)) {
|
||||
for (uint32_t gamePort = 0; gamePort < PAD_CHANMAX; ++gamePort) {
|
||||
if (gamePort != selectedGamePort && Wup028Adapter::GetPortAssignment(gamePort) == physicalPort) {
|
||||
RuntimeConfigFile::SetGameCubeAdapterPort(gamePort, -1);
|
||||
}
|
||||
}
|
||||
PADClearPort(selectedGamePort);
|
||||
Wup028Adapter::SetPortAssignment(selectedGamePort, physicalPort);
|
||||
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, physicalPort);
|
||||
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
|
||||
}
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (ImGui::MenuItem("Unassign controller")) {
|
||||
PADClearPort(selectedGamePort);
|
||||
Wup028Adapter::SetPortAssignment(selectedGamePort, -1);
|
||||
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, -1);
|
||||
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
|
||||
}
|
||||
ImGui::Separator();
|
||||
controller_mapping_wizard::DrawSetupList();
|
||||
const uint32_t controllerCount = PADCount();
|
||||
if (controllerCount == 0) {
|
||||
ImGui::TextDisabled("No controller connected");
|
||||
DrawGameCubeAdapterInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
const char* currentName = PADGetName(static_cast<uint32_t>(g_controllerPort));
|
||||
ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None");
|
||||
if (ImGui::BeginMenu("Assign connected controller")) {
|
||||
for (uint32_t index = 0; index < controllerCount; ++index) {
|
||||
const char* name = PADGetNameForControllerIndex(index);
|
||||
ImGui::PushID(static_cast<int>(index));
|
||||
if (ImGui::MenuItem(name != nullptr ? name : "Unknown controller")) {
|
||||
PADSetPortForIndex(index, static_cast<uint32_t>(g_controllerPort));
|
||||
Wup028Adapter::SetPortAssignment(selectedGamePort, -1);
|
||||
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, -1);
|
||||
PADSetPortForIndex(index, selectedGamePort);
|
||||
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
|
||||
ApplyConfiguredMappings();
|
||||
}
|
||||
@@ -346,6 +416,7 @@ void DrawControllerSettings() {
|
||||
PADButtonMapping* mappings = PADGetButtonMappings(static_cast<uint32_t>(g_controllerPort), &mappingCount);
|
||||
if (mappings == nullptr || mappingCount != PAD_BUTTON_COUNT) {
|
||||
ImGui::TextDisabled("Assign a controller to edit its buttons");
|
||||
DrawGameCubeAdapterInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -485,7 +556,7 @@ void DrawControllerSettings() {
|
||||
ImGui::TextUnformatted(kControllerButtons[i].label);
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
DrawGameCubeAdapterInfo();
|
||||
}
|
||||
|
||||
void DrawAudioSettings() {
|
||||
@@ -840,6 +911,7 @@ void PersistDisplayModeIfChanged() {
|
||||
} // namespace
|
||||
|
||||
void InitializeRuntimeSettings() noexcept {
|
||||
controller_mapping_wizard::LoadPersistedMappings();
|
||||
ApplyConfiguredMappings();
|
||||
AudioBackend::Instance().SetMasterVolume(static_cast<float>(g_audioVolumePercent) / 100.0f);
|
||||
AudioBackend::Instance().SetMuted(g_audioMuted);
|
||||
@@ -872,6 +944,7 @@ void HandleEvents(const AuroraEvent* events) noexcept {
|
||||
if (ev->type != AURORA_SDL_EVENT) {
|
||||
continue;
|
||||
}
|
||||
controller_mapping_wizard::HandleSdlEvent(ev->sdl);
|
||||
if (IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) {
|
||||
SetTopBarVisible(!g_topBarVisible);
|
||||
}
|
||||
@@ -893,6 +966,10 @@ void Draw() noexcept {
|
||||
}
|
||||
DrawFpsOverlay();
|
||||
DrawTopBar();
|
||||
controller_mapping_wizard::Draw();
|
||||
// The wizard captures raw presses; keep them out of the game even when the
|
||||
// top bar is hidden mid-setup.
|
||||
PADBlockInput(g_topBarVisible || controller_mapping_wizard::IsActive());
|
||||
DrawStartupScreen();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "memory.h"
|
||||
#include "ppc_runtime.h"
|
||||
#include "recomp_mod_loader.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
#include "runtime_product.h"
|
||||
#include "timebase_contract.h"
|
||||
@@ -409,14 +410,14 @@ void SystemBridge::Initialize() {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "Executed " << count << " static constructors." << std::endl;
|
||||
}
|
||||
|
||||
void SystemBridge::WriteGuestMemorySnapshot(std::ostream& os, const char* mem1Path) {
|
||||
void SystemBridge::WriteGuestMemorySnapshot(std::ostream& os, const std::filesystem::path& mem1Path) {
|
||||
constexpr uint32_t kMem1Base = 0x80000000u;
|
||||
constexpr uint32_t kMem1Size = 0x01800000u;
|
||||
if (Memory::Contains(kMem1Base, kMem1Size)) {
|
||||
std::ofstream dump(mem1Path, std::ios::binary | std::ios::trunc);
|
||||
dump.write(reinterpret_cast<const char*>(Memory::GetPointer(kMem1Base, kMem1Size)),
|
||||
kMem1Size);
|
||||
os << "[runtime] MEM1 snapshot written to " << mem1Path
|
||||
os << "[runtime] MEM1 snapshot written to " << RuntimeConfigFile::PathToUtf8(mem1Path)
|
||||
<< (dump.good() ? "" : " (write failed)") << std::endl;
|
||||
}
|
||||
constexpr uint32_t kMem2Base = 0x90000000u;
|
||||
@@ -424,11 +425,12 @@ void SystemBridge::WriteGuestMemorySnapshot(std::ostream& os, const char* mem1Pa
|
||||
if (!Memory::Contains(kMem2Base, mem2Size)) {
|
||||
continue;
|
||||
}
|
||||
const std::string mem2Path = std::string(mem1Path) + ".mem2";
|
||||
std::filesystem::path mem2Path = mem1Path;
|
||||
mem2Path += ".mem2";
|
||||
std::ofstream dump(mem2Path, std::ios::binary | std::ios::trunc);
|
||||
dump.write(reinterpret_cast<const char*>(Memory::GetPointer(kMem2Base, mem2Size)),
|
||||
mem2Size);
|
||||
os << "[runtime] MEM2 snapshot written to " << mem2Path
|
||||
os << "[runtime] MEM2 snapshot written to " << RuntimeConfigFile::PathToUtf8(mem2Path)
|
||||
<< (dump.good() ? "" : " (write failed)") << std::endl;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,450 @@
|
||||
#include "wup028_adapter.h"
|
||||
|
||||
#include "runtime_log.h"
|
||||
#include "runtime_config.h"
|
||||
|
||||
#include <dolphin/pad.h>
|
||||
#include <windows.h>
|
||||
#include <initguid.h>
|
||||
#include <setupapi.h>
|
||||
#include <usb.h>
|
||||
#include <usbiodef.h>
|
||||
#include <winusb.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cwctype>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace Wup028Adapter {
|
||||
namespace {
|
||||
|
||||
constexpr uint16_t kNintendoVendor = 0x057e;
|
||||
constexpr uint16_t kAdapterProduct = 0x0337;
|
||||
constexpr size_t kReportSize = 37;
|
||||
constexpr auto kInputReportTimeout = std::chrono::milliseconds(500);
|
||||
|
||||
std::mutex g_mutex;
|
||||
std::array<PADStatus, PAD_CHANMAX> g_statuses{};
|
||||
std::array<uint8_t, PAD_CHANMAX> g_rumble{};
|
||||
std::array<int8_t, PAD_CHANMAX> g_portAssignments{{-1, -1, -1, -1}};
|
||||
std::thread g_worker;
|
||||
std::atomic_bool g_stop{false};
|
||||
std::atomic_bool g_running{false};
|
||||
std::atomic_bool g_connected{false};
|
||||
AdapterInfo g_info;
|
||||
|
||||
struct Device {
|
||||
HANDLE file = INVALID_HANDLE_VALUE;
|
||||
HANDLE event = nullptr;
|
||||
WINUSB_INTERFACE_HANDLE usb = nullptr;
|
||||
UCHAR inputPipe = 0;
|
||||
UCHAR outputPipe = 0;
|
||||
|
||||
~Device() { Close(); }
|
||||
void Close() {
|
||||
if (usb != nullptr) WinUsb_Free(usb);
|
||||
if (event != nullptr) CloseHandle(event);
|
||||
if (file != INVALID_HANDLE_VALUE) CloseHandle(file);
|
||||
usb = nullptr;
|
||||
event = nullptr;
|
||||
file = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
};
|
||||
|
||||
bool Transfer(Device& device, bool input, UCHAR pipe, UCHAR* data, ULONG size, ULONG& transferred,
|
||||
DWORD timeoutMs, bool* timedOut = nullptr) {
|
||||
if (timedOut != nullptr) *timedOut = false;
|
||||
ResetEvent(device.event);
|
||||
OVERLAPPED operation{};
|
||||
operation.hEvent = device.event;
|
||||
const BOOL started = input ? WinUsb_ReadPipe(device.usb, pipe, data, size, &transferred, &operation)
|
||||
: WinUsb_WritePipe(device.usb, pipe, data, size, &transferred, &operation);
|
||||
if (started) return true;
|
||||
if (GetLastError() != ERROR_IO_PENDING) return false;
|
||||
const DWORD wait = WaitForSingleObject(device.event, timeoutMs);
|
||||
if (wait == WAIT_OBJECT_0) {
|
||||
return WinUsb_GetOverlappedResult(device.usb, &operation, &transferred, FALSE);
|
||||
}
|
||||
CancelIoEx(device.file, &operation);
|
||||
WaitForSingleObject(device.event, INFINITE);
|
||||
WinUsb_GetOverlappedResult(device.usb, &operation, &transferred, FALSE);
|
||||
if (timedOut != nullptr && wait == WAIT_TIMEOUT) *timedOut = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
struct DeviceMatch {
|
||||
std::wstring path;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
std::string WideToUtf8(const wchar_t* value) {
|
||||
if (value == nullptr || *value == L'\0') return {};
|
||||
const int size = WideCharToMultiByte(CP_UTF8, 0, value, -1, nullptr, 0, nullptr, nullptr);
|
||||
if (size <= 1) return {};
|
||||
std::string result(static_cast<size_t>(size), '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, value, -1, result.data(), size, nullptr, nullptr);
|
||||
result.resize(static_cast<size_t>(size - 1));
|
||||
return result;
|
||||
}
|
||||
|
||||
DeviceMatch FindAdapter() {
|
||||
HDEVINFO devices = SetupDiGetClassDevsW(&GUID_DEVINTERFACE_USB_DEVICE, nullptr, nullptr,
|
||||
DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
|
||||
if (devices == INVALID_HANDLE_VALUE) return {};
|
||||
|
||||
DeviceMatch result;
|
||||
for (DWORD index = 0;; ++index) {
|
||||
SP_DEVICE_INTERFACE_DATA iface{sizeof(iface)};
|
||||
if (!SetupDiEnumDeviceInterfaces(devices, nullptr, &GUID_DEVINTERFACE_USB_DEVICE, index, &iface)) break;
|
||||
|
||||
DWORD required = 0;
|
||||
SetupDiGetDeviceInterfaceDetailW(devices, &iface, nullptr, 0, &required, nullptr);
|
||||
if (required < sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W)) continue;
|
||||
std::vector<uint8_t> storage(required);
|
||||
auto* detail = reinterpret_cast<SP_DEVICE_INTERFACE_DETAIL_DATA_W*>(storage.data());
|
||||
detail->cbSize = sizeof(*detail);
|
||||
SP_DEVINFO_DATA deviceInfo{sizeof(deviceInfo)};
|
||||
if (!SetupDiGetDeviceInterfaceDetailW(devices, &iface, detail, required, nullptr, &deviceInfo)) continue;
|
||||
|
||||
std::wstring lower(detail->DevicePath);
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||
[](wchar_t c) { return static_cast<wchar_t>(std::towlower(c)); });
|
||||
if (lower.find(L"vid_057e") != std::wstring::npos && lower.find(L"pid_0337") != std::wstring::npos) {
|
||||
result.path = detail->DevicePath;
|
||||
std::array<wchar_t, 256> description{};
|
||||
if (SetupDiGetDeviceRegistryPropertyW(devices, &deviceInfo, SPDRP_FRIENDLYNAME, nullptr,
|
||||
reinterpret_cast<BYTE*>(description.data()),
|
||||
static_cast<DWORD>(description.size() * sizeof(wchar_t)), nullptr) ||
|
||||
SetupDiGetDeviceRegistryPropertyW(devices, &deviceInfo, SPDRP_DEVICEDESC, nullptr,
|
||||
reinterpret_cast<BYTE*>(description.data()),
|
||||
static_cast<DWORD>(description.size() * sizeof(wchar_t)), nullptr)) {
|
||||
result.name = WideToUtf8(description.data());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
SetupDiDestroyDeviceInfoList(devices);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Open(Device& device, std::string& name, std::string& error) {
|
||||
const auto match = FindAdapter();
|
||||
if (match.path.empty()) {
|
||||
error = "No VID 057E / PID 0337 adapter is present";
|
||||
return false;
|
||||
}
|
||||
name = match.name.empty() ? "WUP-028-compatible adapter" : match.name;
|
||||
device.file = CreateFileW(match.path.c_str(), GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, nullptr);
|
||||
if (device.file == INVALID_HANDLE_VALUE || !WinUsb_Initialize(device.file, &device.usb)) {
|
||||
error = "WinUSB could not open the adapter (Windows error " + std::to_string(GetLastError()) + ")";
|
||||
device.Close();
|
||||
return false;
|
||||
}
|
||||
device.event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
if (device.event == nullptr) {
|
||||
error = "Could not create the WinUSB transfer event";
|
||||
device.Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
USB_INTERFACE_DESCRIPTOR descriptor{};
|
||||
if (!WinUsb_QueryInterfaceSettings(device.usb, 0, &descriptor)) {
|
||||
error = "WinUSB could not query the adapter interface";
|
||||
device.Close();
|
||||
return false;
|
||||
}
|
||||
for (UCHAR index = 0; index < descriptor.bNumEndpoints; ++index) {
|
||||
WINUSB_PIPE_INFORMATION pipe{};
|
||||
if (!WinUsb_QueryPipe(device.usb, 0, index, &pipe)) continue;
|
||||
if ((pipe.PipeType == UsbdPipeTypeInterrupt || pipe.PipeType == UsbdPipeTypeBulk) &&
|
||||
USB_ENDPOINT_DIRECTION_IN(pipe.PipeId) && device.inputPipe == 0) {
|
||||
device.inputPipe = pipe.PipeId;
|
||||
} else if ((pipe.PipeType == UsbdPipeTypeInterrupt || pipe.PipeType == UsbdPipeTypeBulk) &&
|
||||
USB_ENDPOINT_DIRECTION_OUT(pipe.PipeId) && device.outputPipe == 0) {
|
||||
device.outputPipe = pipe.PipeId;
|
||||
}
|
||||
}
|
||||
if (device.inputPipe == 0 || device.outputPipe == 0) {
|
||||
error = "The adapter has no usable input/output endpoint";
|
||||
device.Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
UCHAR command = 0x13; // Enable the adapter's 37-byte input stream.
|
||||
ULONG written = 0;
|
||||
if (!Transfer(device, false, device.outputPipe, &command, 1, written, 1000) || written != 1) {
|
||||
error = "The adapter rejected input initialization on endpoint 0x";
|
||||
const char hex[] = "0123456789ABCDEF";
|
||||
error += hex[device.outputPipe >> 4];
|
||||
error += hex[device.outputPipe & 15];
|
||||
device.Close();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int8_t Axis(uint8_t raw) {
|
||||
constexpr int kCenter = 128;
|
||||
constexpr int kCenterTolerance = 10;
|
||||
if (raw >= kCenter - kCenterTolerance && raw <= kCenter + kCenterTolerance) return 0;
|
||||
return static_cast<int8_t>(std::clamp(static_cast<int>(raw) - kCenter, -128, 127));
|
||||
}
|
||||
|
||||
PADStatus DecodePort(const uint8_t* p) {
|
||||
PADStatus out{};
|
||||
// The WUP-028 protocol uses type 1 for wired pads and type 2 for
|
||||
// WaveBird/wireless receivers. Testing only bit 0x10 drops every type-2
|
||||
// controller and makes otherwise valid ports appear empty.
|
||||
if ((p[0] & 0x30) == 0) {
|
||||
out.err = PAD_ERR_NO_CONTROLLER;
|
||||
return out;
|
||||
}
|
||||
if (p[1] & 0x01) out.button |= PAD_BUTTON_A;
|
||||
if (p[1] & 0x02) out.button |= PAD_BUTTON_B;
|
||||
if (p[1] & 0x04) out.button |= PAD_BUTTON_X;
|
||||
if (p[1] & 0x08) out.button |= PAD_BUTTON_Y;
|
||||
if (p[1] & 0x10) out.button |= PAD_BUTTON_LEFT;
|
||||
if (p[1] & 0x20) out.button |= PAD_BUTTON_RIGHT;
|
||||
if (p[1] & 0x40) out.button |= PAD_BUTTON_DOWN;
|
||||
if (p[1] & 0x80) out.button |= PAD_BUTTON_UP;
|
||||
if (p[2] & 0x01) out.button |= PAD_BUTTON_START;
|
||||
if (p[2] & 0x02) out.button |= PAD_TRIGGER_Z;
|
||||
if (p[2] & 0x04) out.button |= PAD_TRIGGER_R;
|
||||
if (p[2] & 0x08) out.button |= PAD_TRIGGER_L;
|
||||
out.stickX = Axis(p[3]);
|
||||
out.stickY = Axis(p[4]);
|
||||
out.substickX = Axis(p[5]);
|
||||
out.substickY = Axis(p[6]);
|
||||
out.triggerL = p[7];
|
||||
out.triggerR = p[8];
|
||||
out.err = PAD_ERR_NONE;
|
||||
return out;
|
||||
}
|
||||
|
||||
bool SendRumble(Device& device, const std::array<uint8_t, PAD_CHANMAX>& motors) {
|
||||
std::array<UCHAR, 5> report{{0x11, motors[0], motors[1], motors[2], motors[3]}};
|
||||
ULONG written = 0;
|
||||
return Transfer(device, false, device.outputPipe, report.data(), report.size(), written, 1000) &&
|
||||
written == report.size();
|
||||
}
|
||||
|
||||
bool RefreshInputStream(Device& device) {
|
||||
UCHAR command = 0x13;
|
||||
ULONG written = 0;
|
||||
return Transfer(device, false, device.outputPipe, &command, 1, written, 1000) && written == 1;
|
||||
}
|
||||
|
||||
void ClearConnectedPorts(const char* reason) {
|
||||
std::lock_guard lock(g_mutex);
|
||||
g_rumble.fill(0);
|
||||
for (size_t port = 0; port < g_info.ports.size(); ++port) {
|
||||
if (g_info.ports[port]) {
|
||||
g_info.ports[port] = false;
|
||||
++g_info.portChangeSequence[port];
|
||||
RT_LOG(RT_TAG_RUNTIME) << "GameCube adapter port " << (port + 1)
|
||||
<< " controller disconnected (" << reason << ")" << std::endl;
|
||||
}
|
||||
g_statuses[port] = {};
|
||||
g_statuses[port].err = PAD_ERR_NO_CONTROLLER;
|
||||
g_info.portStatus[port] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Worker() {
|
||||
std::string lastError;
|
||||
while (!g_stop.load(std::memory_order_acquire)) {
|
||||
Device device;
|
||||
std::string name;
|
||||
std::string error;
|
||||
if (!Open(device, name, error)) {
|
||||
g_connected.store(false, std::memory_order_release);
|
||||
{
|
||||
std::lock_guard lock(g_mutex);
|
||||
g_info.state = error.starts_with("No VID") ? ConnectionState::Searching : ConnectionState::DriverError;
|
||||
g_info.deviceName = name;
|
||||
g_info.detail = error;
|
||||
g_info.pollRateHz = 0.0f;
|
||||
g_info.inputEndpoint = 0;
|
||||
g_info.outputEndpoint = 0;
|
||||
g_info.ports.fill(false);
|
||||
g_info.portStatus.fill(0);
|
||||
}
|
||||
if (error != lastError && !error.starts_with("No VID")) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "GameCube adapter: " << error << std::endl;
|
||||
}
|
||||
lastError = error;
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
continue;
|
||||
}
|
||||
RT_LOG(RT_TAG_RUNTIME) << name << " connected (input endpoint 0x" << std::hex
|
||||
<< static_cast<unsigned>(device.inputPipe) << ", output endpoint 0x"
|
||||
<< static_cast<unsigned>(device.outputPipe) << std::dec << ")" << std::endl;
|
||||
lastError.clear();
|
||||
g_connected.store(true, std::memory_order_release);
|
||||
{
|
||||
std::lock_guard lock(g_mutex);
|
||||
g_info.state = ConnectionState::Connected;
|
||||
g_info.deviceName = name;
|
||||
g_info.detail = "Receiving native GameCube reports";
|
||||
g_info.inputEndpoint = device.inputPipe;
|
||||
g_info.outputEndpoint = device.outputPipe;
|
||||
}
|
||||
std::array<uint8_t, PAD_CHANMAX> sentRumble{};
|
||||
auto rateStart = std::chrono::steady_clock::now();
|
||||
uint32_t rateReports = 0;
|
||||
std::array<bool, PAD_CHANMAX> reportedPorts{};
|
||||
auto lastReport = std::chrono::steady_clock::now();
|
||||
|
||||
while (!g_stop.load(std::memory_order_acquire)) {
|
||||
std::array<UCHAR, kReportSize> report{};
|
||||
ULONG read = 0;
|
||||
bool timedOut = false;
|
||||
if (!Transfer(device, true, device.inputPipe, report.data(), report.size(), read, 100, &timedOut)) {
|
||||
if (timedOut && std::chrono::steady_clock::now() - lastReport < kInputReportTimeout) continue;
|
||||
break;
|
||||
}
|
||||
if (read != report.size() || report[0] != 0x21) {
|
||||
if (std::chrono::steady_clock::now() - lastReport >= kInputReportTimeout) break;
|
||||
continue;
|
||||
}
|
||||
lastReport = std::chrono::steady_clock::now();
|
||||
++rateReports;
|
||||
std::array<PADStatus, PAD_CHANMAX> decoded{};
|
||||
for (size_t port = 0; port < decoded.size(); ++port) decoded[port] = DecodePort(report.data() + 1 + port * 9);
|
||||
std::array<uint8_t, PAD_CHANMAX> desired{};
|
||||
std::array<int8_t, PAD_CHANMAX> transitions{};
|
||||
bool refreshStream = false;
|
||||
{
|
||||
std::lock_guard lock(g_mutex);
|
||||
g_statuses = decoded;
|
||||
desired = g_rumble;
|
||||
for (size_t port = 0; port < decoded.size(); ++port) {
|
||||
g_info.portStatus[port] = report[1 + port * 9];
|
||||
const bool present = decoded[port].err == PAD_ERR_NONE;
|
||||
if (present != reportedPorts[port]) {
|
||||
reportedPorts[port] = present;
|
||||
g_info.ports[port] = present;
|
||||
++g_info.portChangeSequence[port];
|
||||
transitions[port] = present ? 1 : -1;
|
||||
}
|
||||
}
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const float seconds = std::chrono::duration<float>(now - rateStart).count();
|
||||
if (seconds >= 1.0f) {
|
||||
g_info.pollRateHz = static_cast<float>(rateReports) / seconds;
|
||||
rateReports = 0;
|
||||
rateStart = now;
|
||||
refreshStream = true;
|
||||
}
|
||||
}
|
||||
for (size_t port = 0; port < transitions.size(); ++port) {
|
||||
if (transitions[port] != 0) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "GameCube adapter port " << (port + 1) << " controller "
|
||||
<< (transitions[port] > 0 ? "connected" : "disconnected") << std::endl;
|
||||
}
|
||||
}
|
||||
if (desired != sentRumble) {
|
||||
if (!SendRumble(device, desired)) break;
|
||||
sentRumble = desired;
|
||||
}
|
||||
if (refreshStream && !RefreshInputStream(device)) break;
|
||||
}
|
||||
// Do not leave a motor latched on when stopping or abandoning this handle.
|
||||
SendRumble(device, {});
|
||||
g_connected.store(false, std::memory_order_release);
|
||||
ClearConnectedPorts("adapter unavailable");
|
||||
{
|
||||
std::lock_guard lock(g_mutex);
|
||||
g_info.state = ConnectionState::Searching;
|
||||
g_info.detail = "Adapter disconnected; waiting for reconnect";
|
||||
g_info.pollRateHz = 0.0f;
|
||||
}
|
||||
if (!g_stop.load(std::memory_order_acquire)) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "WUP-028 GameCube adapter disconnected; waiting for reconnect" << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Initialize() {
|
||||
bool expected = false;
|
||||
if (!g_running.compare_exchange_strong(expected, true)) return;
|
||||
for (auto& status : g_statuses) status.err = PAD_ERR_NO_CONTROLLER;
|
||||
for (size_t gamePort = 0; gamePort < g_portAssignments.size(); ++gamePort) {
|
||||
g_portAssignments[gamePort] = static_cast<int8_t>(RuntimeConfigFile::GameCubeAdapterPort(gamePort));
|
||||
}
|
||||
g_stop.store(false, std::memory_order_release);
|
||||
g_worker = std::thread(Worker);
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
if (!g_running.exchange(false)) return;
|
||||
g_stop.store(true, std::memory_order_release);
|
||||
if (g_worker.joinable()) g_worker.join();
|
||||
g_connected.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool Read(std::array<PADStatus, 4>& statuses) {
|
||||
if (!g_connected.load(std::memory_order_acquire)) return false;
|
||||
std::lock_guard lock(g_mutex);
|
||||
for (auto& status : statuses) status.err = PAD_ERR_NO_CONTROLLER;
|
||||
for (size_t gamePort = 0; gamePort < statuses.size(); ++gamePort) {
|
||||
const int physicalPort = g_portAssignments[gamePort];
|
||||
if (physicalPort >= 0) statuses[gamePort] = g_statuses[static_cast<size_t>(physicalPort)];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetPortAssignment(uint32_t gamePort, int physicalPort) {
|
||||
if (gamePort >= g_portAssignments.size() || physicalPort < -1 || physicalPort >= PAD_CHANMAX) return;
|
||||
std::lock_guard lock(g_mutex);
|
||||
const int oldPhysicalPort = g_portAssignments[gamePort];
|
||||
if (oldPhysicalPort >= 0) g_rumble[static_cast<size_t>(oldPhysicalPort)] = 0;
|
||||
if (physicalPort >= 0) {
|
||||
for (auto& assignment : g_portAssignments) {
|
||||
if (assignment == physicalPort) {
|
||||
assignment = -1;
|
||||
g_rumble[static_cast<size_t>(physicalPort)] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
g_portAssignments[gamePort] = static_cast<int8_t>(physicalPort);
|
||||
}
|
||||
|
||||
int GetPortAssignment(uint32_t gamePort) {
|
||||
if (gamePort >= g_portAssignments.size()) return -1;
|
||||
std::lock_guard lock(g_mutex);
|
||||
return g_portAssignments[gamePort];
|
||||
}
|
||||
|
||||
bool SetRumble(uint32_t port, bool enabled) {
|
||||
if (port >= g_rumble.size() || !g_connected.load(std::memory_order_acquire)) return false;
|
||||
std::lock_guard lock(g_mutex);
|
||||
if (!g_connected.load(std::memory_order_acquire)) return false;
|
||||
const int physicalPort = g_portAssignments[port];
|
||||
if (physicalPort < 0) return false;
|
||||
const size_t adapterPort = static_cast<size_t>(physicalPort);
|
||||
if (g_statuses[adapterPort].err != PAD_ERR_NONE) {
|
||||
g_rumble[adapterPort] = 0;
|
||||
return false;
|
||||
}
|
||||
g_rumble[adapterPort] = enabled ? 1 : 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
AdapterInfo GetInfo() {
|
||||
std::lock_guard lock(g_mutex);
|
||||
return g_info;
|
||||
}
|
||||
|
||||
} // namespace Wup028Adapter
|
||||
@@ -1,145 +1,77 @@
|
||||
|
||||
# Wiicompiled Static Recompiler
|
||||
|
||||
|
||||
|
||||
The translator parses GameCube/Wii DOL files, decodes PowerPC instructions, lifts them through
|
||||
|
||||
IR/SSA and type inference, and emits C++ that is compiled into a native executable together with
|
||||
|
||||
a runtime in `runtime/`. Project-specific paths and addresses are supplied through a versioned
|
||||
|
||||
YAML manifest, so the translator itself contains no game-specific data.
|
||||
|
||||
|
||||
|
||||
> Translating a DOL does not exempt you from owning the game it came from.
|
||||
|
||||
|
||||
|
||||
## How translating a DOL works
|
||||
|
||||
|
||||
|
||||
A project manifest (YAML) names the input DOL and pins its layout; everything else is derived.
|
||||
|
||||
Translation is four commands:
|
||||
|
||||
|
||||
|
||||
1. **`translate-recursive <entry-point> --project <manifest>`** - walks the call graph from the
|
||||
|
||||
entry point, decodes every reachable function, and emits C++ (plus JSON metadata describing
|
||||
|
||||
what was emitted).
|
||||
|
||||
2. **`generate-data-init --project <manifest>`** - writes the embedded `.data`/`.rodata`/`.sdata`
|
||||
|
||||
section initializer and `RuntimeConfig.h`.
|
||||
|
||||
3. **`emit-build-shards --project <manifest>`** - emits the CMake build graph (`shards.cmake`)
|
||||
|
||||
covering both generated sources and `runtime/src`.
|
||||
|
||||
4. **CMake + Ninja with Clang** compiles `runtime/` plus the generated output into one executable.
|
||||
|
||||
|
||||
|
||||
Discovery is purely recursive from the entry point unless the manifest provides an optional
|
||||
|
||||
`function_map` (one `hexaddr name` per line) that seeds additional function boundaries. Unsupported
|
||||
|
||||
instructions fail translation by default.
|
||||
|
||||
|
||||
|
||||
See `projects/examples/generic-dol.yml` for a minimal manifest driven by `RECOMP_GENERIC_DOL`.
|
||||
|
||||
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
||||
|
||||
| Tool | Notes |
|
||||
|
||||
| --- | --- |
|
||||
|
||||
| .NET 8 SDK | Builds and runs the translator. |
|
||||
|
||||
| CMake ≥ 3.16 and Ninja | Configures and drives the native build. |
|
||||
|
||||
| Clang / LLVM | The shipped build uses LLVM-MinGW targeting `x86-64-v3`. MSVC is not the tested path. |
|
||||
|
||||
|
||||
|
||||
Build the CLI once and invoke the assembly directly:
|
||||
|
||||
|
||||
|
||||
```powershell
|
||||
|
||||
dotnet build translator/src/Translator.Cli/Translator.Cli.csproj -c Release
|
||||
|
||||
$translator = 'translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll'
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Manifest essentials
|
||||
|
||||
|
||||
|
||||
- `inputs.dol.path` - the DOL to translate; optional SHA-256 pinning rejects wrong revisions.
|
||||
|
||||
- `memory.base` / `size` - guest address space.
|
||||
|
||||
- `memory.sda_base` / `sda2_base` - the r13/r2 Small Data Area bases your DOL's boot code installs
|
||||
|
||||
(`lis`/`ori` pairs in `__init_registers`). Required by any command that writes `RuntimeConfig.h`;
|
||||
|
||||
the translator does not guess them.
|
||||
|
||||
- `translation.function_map.path` - optional symbol map used as the discovery oracle.
|
||||
|
||||
- `translation.allow_unsupported_instructions` - off by default; enabling it emits runtime traps
|
||||
|
||||
instead of failing, and such a build can never ship.
|
||||
|
||||
|
||||
|
||||
Relative paths resolve from `workspace_root`, which itself resolves from the manifest directory.
|
||||
|
||||
|
||||
|
||||
## Commands
|
||||
|
||||
|
||||
|
||||
- `info [--project path]`
|
||||
|
||||
- `translate-recursive <address> --project path`
|
||||
|
||||
- `generate-data-init --project path`
|
||||
|
||||
- `emit-base-manifest --project path`
|
||||
|
||||
- `emit-build-shards --project path`
|
||||
|
||||
- `translate-mod --project path [--profile name] ...` - static Kamek/Pulsar module translation
|
||||
|
||||
|
||||
|
||||
Any command prints its own option list with `--help`.
|
||||
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
|
||||
```powershell
|
||||
|
||||
dotnet test translator/Translator.sln -c Release
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user