Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ab77952f4 | |||
| 5d67b229f6 | |||
| 56db6ba641 | |||
| a67069afd3 | |||
| 009697fb97 | |||
| 3f7fed48c9 | |||
| 6eba523d70 | |||
| 8c6c177857 | |||
| be153e0fa0 |
@@ -34,46 +34,3 @@ jobs:
|
|||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
|
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
|
||||||
|
|
||||||
ios-crosscompile:
|
|
||||||
name: iOS arm64 (cross-compile, no Xcode)
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
env:
|
|
||||||
IOS_SDK_DIR: ${{ github.workspace }}/iOS-SDKs/iPhoneOS26.5.sdk
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
# The runner images carry clang up to 18; the iOS toolchain file is tested
|
|
||||||
# against upstream 19, so take it from apt.llvm.org rather than the archive.
|
|
||||||
- name: Install clang 19, lld and ninja
|
|
||||||
run: |
|
|
||||||
wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh
|
|
||||||
chmod +x /tmp/llvm.sh
|
|
||||||
sudo /tmp/llvm.sh 19
|
|
||||||
sudo apt-get install -y lld-19 ninja-build
|
|
||||||
|
|
||||||
# The only Apple piece in the build. Sparse-cloned so the checkout is one
|
|
||||||
# SDK (~50 MB) rather than every SDK the repository carries.
|
|
||||||
- name: Fetch the iPhoneOS SDK
|
|
||||||
run: |
|
|
||||||
git clone --depth 1 --filter=blob:none --sparse \
|
|
||||||
https://github.com/xybp888/iOS-SDKs.git "${{ github.workspace }}/iOS-SDKs"
|
|
||||||
git -C "${{ github.workspace }}/iOS-SDKs" sparse-checkout set --no-cone iPhoneOS26.5.sdk
|
|
||||||
|
|
||||||
# Compile-only: linking a product needs the translated shards, which come
|
|
||||||
# from the user's own Mario Kart Wii dump and are not in this repository.
|
|
||||||
# This proves the runtime's own sources still build for iOS arm64.
|
|
||||||
- name: Configure
|
|
||||||
run: |
|
|
||||||
cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
|
||||||
-DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}/runtime/cmake/ios-linux-toolchain.cmake" \
|
|
||||||
-DIOS_SDK="$IOS_SDK_DIR" \
|
|
||||||
-DMKW_IOS_LLVM_BIN=/usr/lib/llvm-19/bin \
|
|
||||||
-DMKW_BUILD_PRODUCTS=OFF \
|
|
||||||
-DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE \
|
|
||||||
-DAURORA_DAWN_PROVIDER=package
|
|
||||||
|
|
||||||
- name: Compile the runtime sources for iOS
|
|
||||||
run: cmake --build build-ios --target mkw_macos_native_compile
|
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
|
|||||||
|
|
||||||
$manifest = [ordered]@{
|
$manifest = [ordered]@{
|
||||||
SchemaVersion = 2
|
SchemaVersion = 2
|
||||||
ProductVersion = '0.2.27'
|
ProductVersion = '0.2.28'
|
||||||
ExpectedGameId = $pins.GameId
|
ExpectedGameId = $pins.GameId
|
||||||
ExpectedDolSha256 = $pins.DolSha256
|
ExpectedDolSha256 = $pins.DolSha256
|
||||||
ExpectedRelSha256 = $pins.RelSha256
|
ExpectedRelSha256 = $pins.RelSha256
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ assert_file "$ninja_bin" "Portable Ninja"
|
|||||||
assert_file "$cc" "Portable C compiler"
|
assert_file "$cc" "Portable C compiler"
|
||||||
assert_file "$cxx" "Portable C++ compiler"
|
assert_file "$cxx" "Portable C++ compiler"
|
||||||
assert_dir "$aurora_source" "aurora-main source tree"
|
assert_dir "$aurora_source" "aurora-main source tree"
|
||||||
clang_binary=$(normalize "$toolchain_dir/bin/clang-23")
|
clang_binary=$(normalize "$toolchain_dir/bin/clang-22")
|
||||||
assert_file "$clang_binary" "Portable clang driver binary"
|
assert_file "$clang_binary" "Portable clang driver binary"
|
||||||
|
|
||||||
(( parallel > 0 )) || parallel=$(nproc)
|
(( parallel > 0 )) || parallel=$(nproc)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
|
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
|
||||||
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
|
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
|
||||||
<Version>0.2.22</Version>
|
<Version>0.2.28</Version>
|
||||||
<Authors>patchzy</Authors>
|
<Authors>patchzy</Authors>
|
||||||
<Product>WiiCompiled</Product>
|
<Product>WiiCompiled</Product>
|
||||||
<Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description>
|
<Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
|
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
|
||||||
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
|
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
|
||||||
<Version>0.2.22</Version>
|
<Version>0.2.28</Version>
|
||||||
<Authors>patchzy</Authors>
|
<Authors>patchzy</Authors>
|
||||||
<Product>WiiCompiled</Product>
|
<Product>WiiCompiled</Product>
|
||||||
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
|
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ namespace WiiCompiled.Setup.Linux;
|
|||||||
internal static class ProductInfo
|
internal static class ProductInfo
|
||||||
{
|
{
|
||||||
public const string Name = "WiiCompiled";
|
public const string Name = "WiiCompiled";
|
||||||
public const string Version = "0.2.27";
|
public const string Version = "0.2.28";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>One installed product's record inside install-state.json.</summary>
|
/// <summary>One installed product's record inside install-state.json.</summary>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
|
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
|
||||||
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
|
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
|
||||||
<Version>0.2.22</Version>
|
<Version>0.2.28</Version>
|
||||||
<Authors>patchzy</Authors>
|
<Authors>patchzy</Authors>
|
||||||
<Product>WiiCompiled</Product>
|
<Product>WiiCompiled</Product>
|
||||||
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
|
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ internal static class PlatformChecks
|
|||||||
internal static class ProductInfo
|
internal static class ProductInfo
|
||||||
{
|
{
|
||||||
public const string Name = "WiiCompiled";
|
public const string Name = "WiiCompiled";
|
||||||
public const string Version = "0.2.27";
|
public const string Version = "0.2.28";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The setup executable is copied into the installation under this name. It is the launcher and
|
/// The setup executable is copied into the installation under this name. It is the launcher and
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<AssemblyName>WiiCompiled.Setup</AssemblyName>
|
<AssemblyName>WiiCompiled.Setup</AssemblyName>
|
||||||
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
|
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<Version>0.2.27</Version>
|
<Version>0.2.28</Version>
|
||||||
<Authors>patchzy</Authors>
|
<Authors>patchzy</Authors>
|
||||||
<Product>WiiCompiled</Product>
|
<Product>WiiCompiled</Product>
|
||||||
<Description>Command-line installer and launcher for WiiCompiled</Description>
|
<Description>Command-line installer and launcher for WiiCompiled</Description>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ set -euo pipefail
|
|||||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
workspace=$(cd "$script_dir/.." && pwd)
|
workspace=$(cd "$script_dir/.." && pwd)
|
||||||
|
|
||||||
llvm_version=23.1.0
|
llvm_version=22.1.8
|
||||||
cmake_version=4.3.3
|
cmake_version=4.3.3
|
||||||
ninja_version=1.13.2
|
ninja_version=1.13.2
|
||||||
destination="$script_dir/artifacts/portable-tools"
|
destination="$script_dir/artifacts/portable-tools"
|
||||||
@@ -52,13 +52,13 @@ done
|
|||||||
|
|
||||||
case "$arch" in
|
case "$arch" in
|
||||||
x86_64) llvm_release_arch=X64; target_triple=x86_64-unknown-linux-gnu
|
x86_64) llvm_release_arch=X64; target_triple=x86_64-unknown-linux-gnu
|
||||||
llvm_release_sha256=18da30f77f475688a18f7704d23f9f155ae007ed9922dbed6850a9419d9fec8c
|
llvm_release_sha256=df0e1ecf16caf3489a272a5eea4eec9b0d82878f6477fa309504f918a0006384
|
||||||
cmake_release_arch=x86_64
|
cmake_release_arch=x86_64
|
||||||
cmake_sha256=927b2368a946c37269c3a66225ab00544e756459cdd0b5d0da438694fb9ff802
|
cmake_sha256=927b2368a946c37269c3a66225ab00544e756459cdd0b5d0da438694fb9ff802
|
||||||
ninja_asset=ninja-linux.zip
|
ninja_asset=ninja-linux.zip
|
||||||
ninja_sha256=5749cbc4e668273514150a80e387a957f933c6ed3f5f11e03fb30955e2bbead6 ;;
|
ninja_sha256=5749cbc4e668273514150a80e387a957f933c6ed3f5f11e03fb30955e2bbead6 ;;
|
||||||
aarch64) llvm_release_arch=ARM64; target_triple=aarch64-unknown-linux-gnu
|
aarch64) llvm_release_arch=ARM64; target_triple=aarch64-unknown-linux-gnu
|
||||||
llvm_release_sha256=cfb31bfc713ef453248bf5bd026312f838ad6c52c25623e987cb6a340f3050d4
|
llvm_release_sha256=805efad2bb91cb4967fa569e0881d10c0f69c04461cf671cccbae19f547acc34
|
||||||
cmake_release_arch=aarch64
|
cmake_release_arch=aarch64
|
||||||
cmake_sha256=9ea38356dbd3e32e51029a3e09a0f2f8e117ef4fbcaad7a21ffb36409bbd5cb4
|
cmake_sha256=9ea38356dbd3e32e51029a3e09a0f2f8e117ef4fbcaad7a21ffb36409bbd5cb4
|
||||||
ninja_asset=ninja-linux-aarch64.zip
|
ninja_asset=ninja-linux-aarch64.zip
|
||||||
@@ -120,9 +120,9 @@ echo "prepare-portable-tools.sh: pruning to the minimal compile+link toolchain..
|
|||||||
|
|
||||||
# clang: the real driver executable plus the clang/clang++ symlinks CMake/local-build.sh invoke.
|
# clang: the real driver executable plus the clang/clang++ symlinks CMake/local-build.sh invoke.
|
||||||
# Stripped: debug symbols are dead weight for a bundled compiler nobody will debug.
|
# Stripped: debug symbols are dead weight for a bundled compiler nobody will debug.
|
||||||
cp -a "$src/bin/clang-23" "$work/bin/"
|
cp -a "$src/bin/clang-22" "$work/bin/"
|
||||||
strip "$work/bin/clang-23"
|
strip "$work/bin/clang-22"
|
||||||
ln -s clang-23 "$work/bin/clang"
|
ln -s clang-22 "$work/bin/clang"
|
||||||
ln -s clang "$work/bin/clang++"
|
ln -s clang "$work/bin/clang++"
|
||||||
|
|
||||||
# lld: linked via -fuse-ld=lld, which clang resolves by looking for ld.lld next to itself first -
|
# lld: linked via -fuse-ld=lld, which clang resolves by looking for ld.lld next to itself first -
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ Press **F10** while the game window has focus:
|
|||||||
- Internal resolution
|
- Internal resolution
|
||||||
- FPS counter
|
- FPS counter
|
||||||
- Controller assignment for all four ports
|
- Controller assignment for all four ports
|
||||||
- Full per-controller button mapping
|
- Full per-controller button mapping, including the bumpers
|
||||||
|
- Dolphin-syntax input expressions and GCPadNew.ini import
|
||||||
|
- Controller vibration on/off
|
||||||
- Volume, instant mute, and the music ducking toggle
|
- Volume, instant mute, and the music ducking toggle
|
||||||
|
|
||||||
Everything you change is saved to `Config.toml` on the spot and restored next launch.
|
Everything you change is saved to `Config.toml` on the spot and restored next launch.
|
||||||
@@ -56,25 +58,20 @@ Controllers are fed to the game as a GameCube controller.
|
|||||||
Mappings are positional (`south`, `east`, `west`, `north`) rather than Xbox-labelled, so the
|
Mappings are positional (`south`, `east`, `west`, `north`) rather than Xbox-labelled, so the
|
||||||
same config makes sense on Xbox, PlayStation, Nintendo and generic SDL pads alike, and extra
|
same config makes sense on Xbox, PlayStation, Nintendo and generic SDL pads alike, and extra
|
||||||
inputs like paddles, touchpads and share buttons show up when the hardware reports them.
|
inputs like paddles, touchpads and share buttons show up when the hardware reports them.
|
||||||
|
|
||||||
|
**Dolphin-compatible input expressions.**
|
||||||
|
Each GameCube control can carry an expression in Dolphin's input syntax, with the same operators
|
||||||
|
and the same functions.
|
||||||
|
A Dolphin `GCPadNew.ini` can be imported directly from the F10 bar.
|
||||||
|
|
||||||
|
**Vibration toggle.**
|
||||||
|
Force feedback can be turned off for every port at once.
|
||||||
The official Wii U / Switch GameCube adapter (WUP-028) works too; as with Dolphin, on Windows the
|
The official Wii U / Switch GameCube adapter (WUP-028) works too; as with Dolphin, on Windows the
|
||||||
adapter must be switched to the WinUSB driver once (Zadig).
|
adapter must be switched to the WinUSB driver once (Zadig).
|
||||||
|
|
||||||
**Real Wii Remotes over Bluetooth.**
|
**Real Wii Remotes over Bluetooth.**
|
||||||
Pair a Wii Remote with Windows (Settings > Bluetooth > Add device, press 1+2 or SYNC, leave the
|
Pair a Wii Remote with Windows (Settings > Bluetooth > Add device, press 1+2 or SYNC, leave the
|
||||||
PIN empty) and the game reads it as an actual Wii Remote through KPAD: Wii Remote icons and
|
PIN empty)
|
||||||
prompts, Wii Wheel tilt steering, wheelies and tricks all come from the game's own motion code.
|
|
||||||
Nunchuk and Classic Controller are real Wii extensions too: the game gets the Nunchuk's stick,
|
|
||||||
C/Z and accelerometer, and the Classic Controller through `KPADGetUnifiedWpadStatus` with its own
|
|
||||||
layout and icons, so its buttons do what the game says they do and no mapping is involved. Plug an
|
|
||||||
extension in or pull it out mid-game and the game switches control scheme like on the console
|
|
||||||
(the runtime patches SDL's Wii driver, which otherwise loses the remote for good on an extension
|
|
||||||
change). Only the Wii U Pro Controller, which has no Wii-era equivalent, is fed to the game as a
|
|
||||||
GameCube pad with Nintendo's layout. If a remote drops out or was switched on after launch, the
|
|
||||||
runtime keeps rescanning Bluetooth until it comes back (F10 > Controller settings > Wii Remotes). SDL's read of
|
|
||||||
the remote's factory accelerometer calibration often times out over Bluetooth (`console.log`
|
|
||||||
then says "Using fallback accelerometer calibration") and it falls back to a nominal zero point,
|
|
||||||
so the same menu has a one-button calibration (remote flat, buttons up) that removes the small
|
|
||||||
tilt offset some remotes show.
|
|
||||||
|
|
||||||
Known limitations of the Wii Remote path:
|
Known limitations of the Wii Remote path:
|
||||||
- No IR pointer yet: menus are navigated with the D-pad and A (the game treats the remote as
|
- No IR pointer yet: menus are navigated with the D-pad and A (the game treats the remote as
|
||||||
@@ -115,92 +112,6 @@ Wheel Wizard downloads the setup tool from this repo and walks you through insta
|
|||||||
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
|
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
|
||||||
|
|
||||||
|
|
||||||
### iOS
|
|
||||||
|
|
||||||
Requires macOS with Xcode. Build the `WiiCompiled` target for iOS arm64; the build writes
|
|
||||||
`WiiCompiled-unsigned.ipa` next to the app bundle.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
|
||||||
-DCMAKE_SYSTEM_NAME=iOS -DCMAKE_SYSTEM_PROCESSOR=arm64 \
|
|
||||||
-DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_SYSROOT=iphoneos \
|
|
||||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=17.0 \
|
|
||||||
-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH \
|
|
||||||
-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY \
|
|
||||||
-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY \
|
|
||||||
-DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE \
|
|
||||||
-DAURORA_DAWN_PROVIDER=package
|
|
||||||
cmake --build build-ios --target WiiCompiled
|
|
||||||
```
|
|
||||||
|
|
||||||
#### iOS from Linux
|
|
||||||
|
|
||||||
The same .ipa builds on a Linux host with upstream clang and lld; no Theos, xtool or Xcode. Tested
|
|
||||||
on Debian 13 with `clang-19 lld-19 llvm-19 cmake ninja-build git`. The only Apple piece is the
|
|
||||||
iPhoneOS SDK, and [xybp888/iOS-SDKs](https://github.com/xybp888/iOS-SDKs) carries current ones:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git clone --depth 1 --filter=blob:none --sparse https://github.com/xybp888/iOS-SDKs.git /opt/iOS-SDKs
|
|
||||||
git -C /opt/iOS-SDKs sparse-checkout set --no-cone iPhoneOS26.5.sdk
|
|
||||||
```
|
|
||||||
|
|
||||||
Pass that directory as `IOS_SDK` (or copy `iPhoneOS.sdk` out of a Mac's Xcode to `/opt/iPhoneOS.sdk`,
|
|
||||||
the default). The translated shard manifest under `generated/` records absolute paths from the
|
|
||||||
machine that ran the translator, so either translate on the Linux host or symlink that path to your
|
|
||||||
checkout.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
|
||||||
-DCMAKE_TOOLCHAIN_FILE=cmake/ios-linux-toolchain.cmake -DIOS_SDK=/opt/iOS-SDKs/iPhoneOS26.5.sdk \
|
|
||||||
-DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE -DAURORA_DAWN_PROVIDER=package
|
|
||||||
cmake --build build-ios --target WiiCompiled
|
|
||||||
```
|
|
||||||
|
|
||||||
#### iOS from Windows
|
|
||||||
|
|
||||||
The same toolchain file works on Windows with [llvm-mingw](https://github.com/mstorsjo/llvm-mingw),
|
|
||||||
which is what the Windows build already uses. Three things differ from a Linux host:
|
|
||||||
|
|
||||||
- llvm-mingw does not ship `ld64.lld.exe` or `llvm-install-name-tool.exe`; LLVM tools dispatch on
|
|
||||||
their file name, so copy `ld.lld.exe` to `ld64.lld.exe` and `llvm-objcopy.exe` to
|
|
||||||
`llvm-install-name-tool.exe` in its `bin` directory.
|
|
||||||
- Git for Windows checks the SDK repo's symlinks out as small text files unless `core.symlinks`
|
|
||||||
is on (Developer Mode). Either enable that before cloning, or replace each placeholder with a
|
|
||||||
copy of its target; `libSystem.tbd` is one of them and the link fails without it.
|
|
||||||
- Pass the toolchain file as an absolute path, and `MKW_IOS_LLVM_BIN` as the llvm-mingw `bin`
|
|
||||||
directory with forward slashes.
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release `
|
|
||||||
-DCMAKE_TOOLCHAIN_FILE=C:/src/Wiicompiled/runtime/cmake/ios-linux-toolchain.cmake `
|
|
||||||
-DIOS_SDK=C:/iOS-SDKs/iPhoneOS26.5.sdk -DMKW_IOS_LLVM_BIN=C:/llvm-mingw/bin `
|
|
||||||
-DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE -DAURORA_DAWN_PROVIDER=package
|
|
||||||
cmake --build build-ios --target WiiCompiled
|
|
||||||
```
|
|
||||||
|
|
||||||
Install it with AltStore or SideStore, which sign with your own Apple ID. The app needs the
|
|
||||||
`com.apple.developer.kernel.increased-memory-limit` entitlement or it exits at startup;
|
|
||||||
[GetMoreRam](https://github.com/hugeBlack/GetMoreRam) grants it with a free Apple ID.
|
|
||||||
GetMoreRam grants `com.apple.developer.kernel.extended-virtual-addressing` at the same time, which
|
|
||||||
matters on devices with less than 4 GB of RAM: the runtime reserves a 4 GiB guest address space at
|
|
||||||
startup and that is right on the limit iOS allows without it. It makes no difference on the 8 GB
|
|
||||||
devices this was developed on.
|
|
||||||
|
|
||||||
`runtime/cmake/ios/WiiCompiled.entitlements` is a template for signing by hand, with placeholders to
|
|
||||||
replace; the sideloaders build their own and ignore it. It carries both entitlements, so the
|
|
||||||
provisioning profile you sign with needs both capabilities enabled.
|
|
||||||
|
|
||||||
Copy `Config.toml` and an extracted `DATA` directory into the app's Documents folder, using Files on
|
|
||||||
the device or the Finder with it connected. Keep `dvd_root` relative:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[paths]
|
|
||||||
dvd_root = "DATA"
|
|
||||||
```
|
|
||||||
|
|
||||||
Touch controls appear when no gamepad is attached; tap the FPS readout for settings.
|
|
||||||
|
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> Only take builds from this repository's
|
> Only take builds from this repository's
|
||||||
> [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an
|
> [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an
|
||||||
|
|||||||
@@ -229,7 +229,6 @@ s32 PADGetNativeButtonPressed(u32 port);
|
|||||||
PADSignedNativeAxis PADGetNativeAxisPulled(u32 port);
|
PADSignedNativeAxis PADGetNativeAxisPulled(u32 port);
|
||||||
void PADRestoreDefaultMapping(u32 port);
|
void PADRestoreDefaultMapping(u32 port);
|
||||||
void PADBlockInput(bool block);
|
void PADBlockInput(bool block);
|
||||||
bool PADIsInputBlocked(void);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the default controller mapping used.
|
* Set the default controller mapping used.
|
||||||
|
|||||||
@@ -1627,8 +1627,6 @@ void PADBlockInput(const bool block) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PADIsInputBlocked() { return g_blockPAD.load(std::memory_order_acquire); }
|
|
||||||
|
|
||||||
|
|
||||||
SDL_Gamepad* PADGetSDLGamepadForIndex(const u32 index) {
|
SDL_Gamepad* PADGetSDLGamepadForIndex(const u32 index) {
|
||||||
const auto* ctrl = __PADGetControllerForIndex(index);
|
const auto* ctrl = __PADGetControllerForIndex(index);
|
||||||
|
|||||||
@@ -401,6 +401,8 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
controller.m_isGameCube = controller.m_vid == 0x057E && controller.m_pid == 0x0337;
|
controller.m_isGameCube = controller.m_vid == 0x057E && controller.m_pid == 0x0337;
|
||||||
|
const char* serial = SDL_GetGamepadSerial(ctrl);
|
||||||
|
controller.m_gameCubeUseOrdinaryStop = controller.m_isGameCube && serial && "GCP+"sv == serial;
|
||||||
if (controller.m_isGameCube ||
|
if (controller.m_isGameCube ||
|
||||||
(SDL_GetGamepadType(ctrl) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO && controller.m_pid == 0x2073)) {
|
(SDL_GetGamepadType(ctrl) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO && controller.m_pid == 0x2073)) {
|
||||||
controller.m_deadZones.emulateTriggers = false;
|
controller.m_deadZones.emulateTriggers = false;
|
||||||
@@ -481,6 +483,13 @@ bool controller_has_rumble(Uint32 instance) noexcept {
|
|||||||
void controller_rumble(uint32_t instance, uint16_t low_freq_intensity, uint16_t high_freq_intensity,
|
void controller_rumble(uint32_t instance, uint16_t low_freq_intensity, uint16_t high_freq_intensity,
|
||||||
uint16_t duration_ms) noexcept {
|
uint16_t duration_ms) noexcept {
|
||||||
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
|
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
|
||||||
|
// GC Pocket+ has been observed continuing to vibrate after a hard stop;
|
||||||
|
// an ordinary stop cleared it. With GAMECUBE_RUMBLE_BRAKE enabled, SDL
|
||||||
|
// encodes (0, 1) as adapter command 0, whereas (0, 0) sends command 2.
|
||||||
|
// Apply the workaround here so shutdown uses the same stop as PAD calls.
|
||||||
|
if (it->second.m_gameCubeUseOrdinaryStop && low_freq_intensity == 0 && high_freq_intensity == 0) {
|
||||||
|
high_freq_intensity = 1;
|
||||||
|
}
|
||||||
SDL_RumbleGamepad(it->second.m_controller, low_freq_intensity, high_freq_intensity, duration_ms);
|
SDL_RumbleGamepad(it->second.m_controller, low_freq_intensity, high_freq_intensity, duration_ms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ extern Module Log;
|
|||||||
struct GameController {
|
struct GameController {
|
||||||
SDL_Gamepad* m_controller = nullptr;
|
SDL_Gamepad* m_controller = nullptr;
|
||||||
bool m_isGameCube = false;
|
bool m_isGameCube = false;
|
||||||
|
bool m_gameCubeUseOrdinaryStop = false;
|
||||||
Sint32 m_index = -1;
|
Sint32 m_index = -1;
|
||||||
Sint32 m_playerIndex = -1;
|
Sint32 m_playerIndex = -1;
|
||||||
bool m_hasRumble = false;
|
bool m_hasRumble = false;
|
||||||
|
|||||||
@@ -11,9 +11,6 @@
|
|||||||
#include <aurora/event.h>
|
#include <aurora/event.h>
|
||||||
#include <aurora/gfx.h>
|
#include <aurora/gfx.h>
|
||||||
#include <aurora/render_size_limits.hpp>
|
#include <aurora/render_size_limits.hpp>
|
||||||
#if defined(__APPLE__)
|
|
||||||
#include <TargetConditionals.h>
|
|
||||||
#endif
|
|
||||||
#include <SDL3/SDL_error.h>
|
#include <SDL3/SDL_error.h>
|
||||||
#include <SDL3/SDL_events.h>
|
#include <SDL3/SDL_events.h>
|
||||||
#include <SDL3/SDL_keyboard.h>
|
#include <SDL3/SDL_keyboard.h>
|
||||||
@@ -411,21 +408,8 @@ bool create_window(AuroraBackend backend) {
|
|||||||
height = 480;
|
height = 480;
|
||||||
}
|
}
|
||||||
|
|
||||||
Sint32 posX = g_config.hasWindowPosition ? g_config.windowPosX : SDL_WINDOWPOS_CENTERED;
|
const Sint32 posX = g_config.hasWindowPosition ? g_config.windowPosX : SDL_WINDOWPOS_CENTERED;
|
||||||
Sint32 posY = g_config.hasWindowPosition ? g_config.windowPosY : SDL_WINDOWPOS_CENTERED;
|
const Sint32 posY = g_config.hasWindowPosition ? g_config.windowPosY : SDL_WINDOWPOS_CENTERED;
|
||||||
#if defined(__APPLE__) && TARGET_OS_IPHONE
|
|
||||||
// Without this the window falls back to the 1280x960 default on any device
|
|
||||||
// with no saved size.
|
|
||||||
if (const SDL_DisplayID primary = SDL_GetPrimaryDisplay(); primary != 0) {
|
|
||||||
SDL_Rect bounds{};
|
|
||||||
if (SDL_GetDisplayBounds(primary, &bounds) && bounds.w > 0 && bounds.h > 0) {
|
|
||||||
width = bounds.w;
|
|
||||||
height = bounds.h;
|
|
||||||
}
|
|
||||||
posX = SDL_WINDOWPOS_CENTERED_DISPLAY(primary);
|
|
||||||
posY = SDL_WINDOWPOS_CENTERED_DISPLAY(primary);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
const auto props = SDL_CreateProperties();
|
const auto props = SDL_CreateProperties();
|
||||||
TRY(SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, g_config.appName), "Failed to set {}: {}",
|
TRY(SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, g_config.appName), "Failed to set {}: {}",
|
||||||
|
|||||||
@@ -7,13 +7,6 @@ endif()
|
|||||||
|
|
||||||
if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||||
set(MKW_PLATFORM_WINDOWS TRUE)
|
set(MKW_PLATFORM_WINDOWS TRUE)
|
||||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS")
|
|
||||||
# Checked before macOS: APPLE is true for iOS too, so the macOS branch would
|
|
||||||
# otherwise claim an iOS build and select a backend that cannot compile
|
|
||||||
# there (<mach/mach_vm.h> is absent from the iOS SDK) or run there (/tmp is
|
|
||||||
# outside the sandbox).
|
|
||||||
set(MKW_PLATFORM_IOS TRUE)
|
|
||||||
set(MKW_PLATFORM_MACOS TRUE) # shares the Apple arm64 substrate
|
|
||||||
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$")
|
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$")
|
||||||
# The first native macOS target is Apple Silicon. Intel and universal
|
# The first native macOS target is Apple Silicon. Intel and universal
|
||||||
# binaries remain future compatibility work; do not silently claim them.
|
# binaries remain future compatibility work; do not silently claim them.
|
||||||
@@ -22,16 +15,13 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(
|
|||||||
set(MKW_PLATFORM_LINUX TRUE)
|
set(MKW_PLATFORM_LINUX TRUE)
|
||||||
else()
|
else()
|
||||||
message(FATAL_ERROR
|
message(FATAL_ERROR
|
||||||
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux "
|
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS arm64")
|
||||||
"x86_64/aarch64, or Apple Clang on macOS arm64 or iOS arm64")
|
|
||||||
endif()
|
endif()
|
||||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
|
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||||
message(FATAL_ERROR "WiiCompiled only supports Release builds")
|
message(FATAL_ERROR "WiiCompiled only supports Release builds")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
|
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
|
||||||
set(MKW_IOS_BUNDLE_ID_PREFIX "it.tobyfox" CACHE STRING
|
|
||||||
"Reverse-DNS prefix for the iOS bundle identifier")
|
|
||||||
|
|
||||||
# Preprocessor definitions that belong to this project's own code (the runtime,
|
# Preprocessor definitions that belong to this project's own code (the runtime,
|
||||||
# the translated shards and the product glue) and to nothing else. They are
|
# the translated shards and the product glue) and to nothing else. They are
|
||||||
@@ -40,11 +30,6 @@ set(MKW_IOS_BUNDLE_ID_PREFIX "it.tobyfox" CACHE STRING
|
|||||||
# libraries and the products in cmake/PublicProducts.cmake) inherits them while
|
# libraries and the products in cmake/PublicProducts.cmake) inherits them while
|
||||||
# aurora-main and its third-party tree stay unaffected.
|
# aurora-main and its third-party tree stay unaffected.
|
||||||
set(MKW_PROJECT_COMPILE_DEFINITIONS NOMINMAX)
|
set(MKW_PROJECT_COMPILE_DEFINITIONS NOMINMAX)
|
||||||
if(MKW_PLATFORM_IOS)
|
|
||||||
# One project-wide answer to "is this iOS", so sources do not each pull in
|
|
||||||
# TargetConditionals.h and spell out the __APPLE__ && TARGET_OS_IPHONE dance.
|
|
||||||
list(APPEND MKW_PROJECT_COMPILE_DEFINITIONS MKW_PLATFORM_IOS=1)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
|
||||||
@@ -151,16 +136,6 @@ else()
|
|||||||
if(NOT EXISTS ${MKW_AURORA_DIR}/CMakeLists.txt)
|
if(NOT EXISTS ${MKW_AURORA_DIR}/CMakeLists.txt)
|
||||||
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
|
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
|
||||||
endif()
|
endif()
|
||||||
# aurora picks shared third-party libraries whenever BUILD_SHARED_LIBS is
|
|
||||||
# merely undefined, and its extern/ tree calls unset(BUILD_SHARED_LIBS CACHE)
|
|
||||||
# while working around xxhash, which deletes any -D the user passed. Defining
|
|
||||||
# it here, before aurora is added, is what actually sticks across
|
|
||||||
# reconfigures. Apple builds ship a self-contained bundle, so a dylib
|
|
||||||
# resolved from the build tree or from Homebrew cannot be allowed to leak
|
|
||||||
# into the link.
|
|
||||||
if(APPLE)
|
|
||||||
set(BUILD_SHARED_LIBS OFF)
|
|
||||||
endif()
|
|
||||||
set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE)
|
set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE)
|
||||||
if(MKW_PLATFORM_WINDOWS)
|
if(MKW_PLATFORM_WINDOWS)
|
||||||
set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE)
|
set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE)
|
||||||
@@ -274,18 +249,8 @@ if(MKW_PLATFORM_MACOS)
|
|||||||
# mkw_co_init/mkw_co_switch at link time.
|
# mkw_co_init/mkw_co_switch at link time.
|
||||||
enable_language(ASM)
|
enable_language(ASM)
|
||||||
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
||||||
set_source_files_properties("${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S"
|
|
||||||
PROPERTIES LANGUAGE ASM SKIP_UNITY_BUILD_INCLUSION ON SKIP_PRECOMPILE_HEADERS ON)
|
|
||||||
# iOS cannot use the macOS backend: <mach/mach_vm.h> is absent from that SDK
|
|
||||||
# and /tmp is outside the sandbox.
|
|
||||||
if(MKW_PLATFORM_IOS)
|
|
||||||
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
|
|
||||||
else()
|
|
||||||
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_ios.cpp")
|
|
||||||
endif()
|
|
||||||
else()
|
else()
|
||||||
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
|
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
|
||||||
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_ios.cpp")
|
|
||||||
endif()
|
endif()
|
||||||
set(MKW_PLATFORM_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/platform/host_platform.cpp")
|
set(MKW_PLATFORM_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/platform/host_platform.cpp")
|
||||||
set(MKW_BASE_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/base_product.cpp")
|
set(MKW_BASE_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/base_product.cpp")
|
||||||
@@ -312,6 +277,20 @@ target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
|
|||||||
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
|
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
|
||||||
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
|
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
|
||||||
|
|
||||||
|
add_executable(mkw_nand_settings_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_settings_tests.cpp")
|
||||||
|
target_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||||
|
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
|
||||||
|
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
|
||||||
|
|
||||||
|
# The input expression engine is self-contained, so it can be exercised without
|
||||||
|
# linking the runtime or SDL.
|
||||||
|
add_executable(mkw_input_expr_tests
|
||||||
|
"${CMAKE_CURRENT_LIST_DIR}/tests/test_expr.cpp"
|
||||||
|
"${CMAKE_CURRENT_LIST_DIR}/src/input_expr.cpp")
|
||||||
|
target_include_directories(mkw_input_expr_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||||
|
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
|
||||||
|
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
|
||||||
|
|
||||||
# HostContext deliberately keeps the platform-specific context primitive out
|
# HostContext deliberately keeps the platform-specific context primitive out
|
||||||
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
|
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
|
||||||
# refactors cannot silently remove its headers, implementation, or link edge.
|
# refactors cannot silently remove its headers, implementation, or link edge.
|
||||||
@@ -327,11 +306,9 @@ if(MKW_PLATFORM_LINUX)
|
|||||||
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
|
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(MKW_PLATFORM_MACOS AND NOT MKW_PLATFORM_IOS)
|
if(MKW_PLATFORM_MACOS)
|
||||||
# Exercise the Apple Silicon context ABI and the public host-memory
|
# Exercise the Apple Silicon context ABI and the public host-memory
|
||||||
# contracts separately from translated products. Host-only: the flat memory
|
# contracts separately from translated products.
|
||||||
# test pulls in guest_flat_memory_macos.cpp, whose <mach/mach_vm.h> does not
|
|
||||||
# exist in the iOS SDK, and none of these can run on a device anyway.
|
|
||||||
enable_language(ASM)
|
enable_language(ASM)
|
||||||
add_executable(mkw_macos_context_abi_tests
|
add_executable(mkw_macos_context_abi_tests
|
||||||
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
|
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
|
||||||
@@ -401,7 +378,7 @@ else()
|
|||||||
target_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC)
|
target_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC)
|
||||||
target_link_libraries(mkw_macos_native_compile PRIVATE
|
target_link_libraries(mkw_macos_native_compile PRIVATE
|
||||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
|
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
|
||||||
PNG::PNG mkw::pugixml mkw::toml11 mkw::cryptopp)
|
mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||||
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
|
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
|
||||||
endif()
|
endif()
|
||||||
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
|
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
GameCube Button Icons and Controls - Zacksly
|
|
||||||
Licensed under CC BY 3.0 - https://zacksly.itch.io
|
|
||||||
|
|
||||||
The PNG files in this directory are taken unmodified from the "Buttons Outline /
|
|
||||||
White" set of that pack, at 256px. They are drawn tinted and partly transparent
|
|
||||||
at runtime, which is a rendering choice and not an edit to the artwork.
|
|
||||||
|
|
||||||
Full licence: http://creativecommons.org/licenses/by/3.0/
|
|
||||||
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
@@ -77,17 +77,10 @@ add_library(mkw_runtime_common OBJECT ${SOURCES})
|
|||||||
mkw_configure_object_target(mkw_runtime_common)
|
mkw_configure_object_target(mkw_runtime_common)
|
||||||
target_compile_features(mkw_runtime_common PRIVATE cxx_std_20)
|
target_compile_features(mkw_runtime_common PRIVATE cxx_std_20)
|
||||||
target_compile_definitions(mkw_runtime_common PRIVATE
|
target_compile_definitions(mkw_runtime_common PRIVATE
|
||||||
|
SDL_MAIN_HANDLED
|
||||||
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
|
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
|
||||||
if(NOT MKW_PLATFORM_IOS)
|
|
||||||
# iOS is the one target where SDL must own main(): its entry point is what
|
|
||||||
# drives UIApplicationMain. Defining this makes <SDL3/SDL_main.h> a no-op,
|
|
||||||
# so the app links a bare main() that UIKit never calls.
|
|
||||||
target_compile_definitions(mkw_runtime_common PRIVATE SDL_MAIN_HANDLED)
|
|
||||||
endif()
|
|
||||||
target_link_libraries(mkw_runtime_common PRIVATE
|
target_link_libraries(mkw_runtime_common PRIVATE
|
||||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||||
# The touch overlay decodes its button artwork at startup.
|
|
||||||
target_link_libraries(mkw_runtime_common PRIVATE PNG::PNG)
|
|
||||||
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
|
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||||
if(MKW_PLATFORM_WINDOWS)
|
if(MKW_PLATFORM_WINDOWS)
|
||||||
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
|
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
|
||||||
@@ -199,10 +192,7 @@ function(mkw_configure_product target)
|
|||||||
"${MKW_RUNTIME_SOURCE_DIR}/.."
|
"${MKW_RUNTIME_SOURCE_DIR}/.."
|
||||||
"${MKW_RUNTIME_SOURCE_DIR}/../aurora-main/include")
|
"${MKW_RUNTIME_SOURCE_DIR}/../aurora-main/include")
|
||||||
target_compile_definitions(${target} PRIVATE
|
target_compile_definitions(${target} PRIVATE
|
||||||
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION TARGET_PC)
|
SDL_MAIN_HANDLED _DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION TARGET_PC)
|
||||||
if(NOT MKW_PLATFORM_IOS)
|
|
||||||
target_compile_definitions(${target} PRIVATE SDL_MAIN_HANDLED)
|
|
||||||
endif()
|
|
||||||
target_compile_features(${target} PRIVATE cxx_std_20)
|
target_compile_features(${target} PRIVATE cxx_std_20)
|
||||||
mkw_apply_common_compile_options(${target})
|
mkw_apply_common_compile_options(${target})
|
||||||
# The dispatch-table and registration shards compile inside the product target itself and
|
# The dispatch-table and registration shards compile inside the product target itself and
|
||||||
@@ -231,26 +221,6 @@ function(mkw_configure_product target)
|
|||||||
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
|
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(MKW_PLATFORM_IOS AND NOT CMAKE_HOST_APPLE)
|
|
||||||
set(shim "${MKW_RUNTIME_SOURCE_DIR}/src/platform/ios/compiler_rt_shim.c")
|
|
||||||
target_sources(${target} PRIVATE "${shim}")
|
|
||||||
set_source_files_properties("${shim}" PROPERTIES SKIP_PRECOMPILE_HEADERS ON SKIP_UNITY_BUILD_INCLUSION ON)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(MKW_PLATFORM_IOS)
|
|
||||||
# CMake bundles iOS targets with its own default Info.plist, whose
|
|
||||||
# CFBundleIdentifier is empty and which carries none of the iOS keys, so
|
|
||||||
# iOS refuses to install the result. Supply a real one.
|
|
||||||
set_target_properties(${target} PROPERTIES
|
|
||||||
MACOSX_BUNDLE TRUE
|
|
||||||
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_LIST_DIR}/ios/Info.plist.in"
|
|
||||||
MACOSX_BUNDLE_EXECUTABLE_NAME "${target}"
|
|
||||||
MACOSX_BUNDLE_BUNDLE_NAME "${target}"
|
|
||||||
MACOSX_BUNDLE_GUI_IDENTIFIER "${MKW_IOS_BUNDLE_ID_PREFIX}.wiicompiled"
|
|
||||||
MACOSX_BUNDLE_BUNDLE_VERSION "1"
|
|
||||||
MACOSX_BUNDLE_SHORT_VERSION_STRING "1.0")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(MKW_PLATFORM_WINDOWS)
|
if(MKW_PLATFORM_WINDOWS)
|
||||||
target_link_libraries(${target} PRIVATE
|
target_link_libraries(${target} PRIVATE
|
||||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
|
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
|
||||||
@@ -293,19 +263,6 @@ function(mkw_configure_product target)
|
|||||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory
|
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
"${MKW_WII_BOOTSTRAP_SOURCE_DIR}" "$<TARGET_FILE_DIR:${target}>/wii_bootstrap")
|
"${MKW_WII_BOOTSTRAP_SOURCE_DIR}" "$<TARGET_FILE_DIR:${target}>/wii_bootstrap")
|
||||||
|
|
||||||
# Touch control artwork. Only the touch build reads these, but they are copied
|
|
||||||
# everywhere the other assets are so the bundle layout stays uniform.
|
|
||||||
set(MKW_TOUCH_ASSET_DIR "${MKW_RUNTIME_SOURCE_DIR}/assets/touch")
|
|
||||||
if(NOT EXISTS "${MKW_TOUCH_ASSET_DIR}/a.png")
|
|
||||||
message(FATAL_ERROR "Missing touch control artwork: ${MKW_TOUCH_ASSET_DIR}")
|
|
||||||
endif()
|
|
||||||
# Cleared first: copy_directory merges, so a removed icon would otherwise
|
|
||||||
# linger in the bundle and get shipped.
|
|
||||||
add_custom_command(TARGET ${target} POST_BUILD
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "$<TARGET_FILE_DIR:${target}>/touch"
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
|
||||||
"${MKW_TOUCH_ASSET_DIR}" "$<TARGET_FILE_DIR:${target}>/touch")
|
|
||||||
|
|
||||||
set(MKW_DSP_COEFFICIENT_ROM "${MKW_RUNTIME_SOURCE_DIR}/assets/dsp/dsp_coef.bin")
|
set(MKW_DSP_COEFFICIENT_ROM "${MKW_RUNTIME_SOURCE_DIR}/assets/dsp/dsp_coef.bin")
|
||||||
if(NOT EXISTS "${MKW_DSP_COEFFICIENT_ROM}")
|
if(NOT EXISTS "${MKW_DSP_COEFFICIENT_ROM}")
|
||||||
message(FATAL_ERROR "Missing Wii DSP coefficient ROM: ${MKW_DSP_COEFFICIENT_ROM}")
|
message(FATAL_ERROR "Missing Wii DSP coefficient ROM: ${MKW_DSP_COEFFICIENT_ROM}")
|
||||||
@@ -329,25 +286,6 @@ function(mkw_configure_product target)
|
|||||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
"${MKW_INITIAL_PIPELINE_CACHE}"
|
"${MKW_INITIAL_PIPELINE_CACHE}"
|
||||||
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db")
|
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db")
|
||||||
|
|
||||||
if(MKW_PLATFORM_IOS)
|
|
||||||
# An .ipa is a zip with the bundle under Payload/. Left unsigned: the
|
|
||||||
# sideloaders people actually install with (AltStore, SideStore) sign on
|
|
||||||
# the device with the user's own Apple ID, and a signature applied here
|
|
||||||
# would only be replaced. WiiCompiled.entitlements beside this file names
|
|
||||||
# the one entitlement the runtime needs, for whatever does the signing.
|
|
||||||
add_custom_command(TARGET ${target} POST_BUILD
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "$<TARGET_FILE_DIR:${target}>/../ipa"
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:${target}>/../ipa/Payload"
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
|
||||||
"$<TARGET_BUNDLE_DIR:${target}>"
|
|
||||||
"$<TARGET_FILE_DIR:${target}>/../ipa/Payload/${target}.app"
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E chdir "$<TARGET_FILE_DIR:${target}>/../ipa"
|
|
||||||
${CMAKE_COMMAND} -E tar cf "${CMAKE_BINARY_DIR}/${target}-unsigned.ipa" --format=zip Payload
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "$<TARGET_FILE_DIR:${target}>/../ipa"
|
|
||||||
COMMENT "Packaging ${target}-unsigned.ipa")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}" ${MKW_BASE_REGISTRATION_SOURCES})
|
add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}" ${MKW_BASE_REGISTRATION_SOURCES})
|
||||||
@@ -381,11 +319,6 @@ endif()
|
|||||||
# tuning rather than leaving target-specific performance on the table.
|
# tuning rather than leaving target-specific performance on the table.
|
||||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||||
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3)
|
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3)
|
||||||
elseif(MKW_PLATFORM_IOS)
|
|
||||||
# iOS 17 runs on the A12 and later, and the build host is never the device;
|
|
||||||
# -mcpu=native there would tune for whatever Mac (or Linux box) did the
|
|
||||||
# compile, and upstream clang rejects it when cross-compiling.
|
|
||||||
set(MKW_BASELINE_ARCH_FLAG -mcpu=apple-a12)
|
|
||||||
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
|
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
|
||||||
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
|
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
|
||||||
else()
|
else()
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
# Cross-compile the iOS arm64 products from a Linux host.
|
|
||||||
#
|
|
||||||
# Needs upstream clang and lld (Debian 13: clang-19 lld-19 llvm-19) and an
|
|
||||||
# iPhoneOS SDK, either copied out of Xcode or sparse-cloned from
|
|
||||||
# github.com/xybp888/iOS-SDKs. Nothing from Theos or xtool. See README.md,
|
|
||||||
# "iOS from Linux".
|
|
||||||
#
|
|
||||||
# cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
|
||||||
# -DCMAKE_TOOLCHAIN_FILE=cmake/ios-linux-toolchain.cmake \
|
|
||||||
# -DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE -DAURORA_DAWN_PROVIDER=package
|
|
||||||
|
|
||||||
set(IOS_SDK "/opt/iPhoneOS.sdk" CACHE PATH "iPhoneOS SDK copied from Xcode")
|
|
||||||
set(MKW_IOS_LLVM_BIN "/usr/lib/llvm-19/bin" CACHE PATH "Directory holding clang, ld64.lld and llvm-ar")
|
|
||||||
set(MKW_IOS_CLANG_RT "" CACHE FILEPATH
|
|
||||||
"Optional: Xcode's libclang_rt.ios.a. Left empty, src/platform/ios/compiler_rt_shim.c covers what the runtime needs")
|
|
||||||
list(APPEND CMAKE_TRY_COMPILE_PLATFORM_VARIABLES IOS_SDK MKW_IOS_LLVM_BIN MKW_IOS_CLANG_RT)
|
|
||||||
|
|
||||||
set(CMAKE_SYSTEM_NAME iOS)
|
|
||||||
set(CMAKE_SYSTEM_PROCESSOR arm64)
|
|
||||||
set(CMAKE_OSX_ARCHITECTURES arm64 CACHE STRING "")
|
|
||||||
set(CMAKE_OSX_DEPLOYMENT_TARGET 17.0 CACHE STRING "")
|
|
||||||
set(CMAKE_OSX_SYSROOT "${IOS_SDK}" CACHE PATH "")
|
|
||||||
set(CMAKE_SYSROOT "${IOS_SDK}")
|
|
||||||
|
|
||||||
set(x "${CMAKE_HOST_EXECUTABLE_SUFFIX}")
|
|
||||||
set(CMAKE_C_COMPILER "${MKW_IOS_LLVM_BIN}/clang${x}")
|
|
||||||
set(CMAKE_CXX_COMPILER "${MKW_IOS_LLVM_BIN}/clang++${x}")
|
|
||||||
set(CMAKE_OBJC_COMPILER "${MKW_IOS_LLVM_BIN}/clang${x}")
|
|
||||||
set(CMAKE_OBJCXX_COMPILER "${MKW_IOS_LLVM_BIN}/clang++${x}")
|
|
||||||
set(CMAKE_ASM_COMPILER "${MKW_IOS_LLVM_BIN}/clang${x}")
|
|
||||||
set(CMAKE_AR "${MKW_IOS_LLVM_BIN}/llvm-ar${x}" CACHE FILEPATH "")
|
|
||||||
set(CMAKE_RANLIB "${MKW_IOS_LLVM_BIN}/llvm-ranlib${x}" CACHE FILEPATH "")
|
|
||||||
set(CMAKE_STRIP "${MKW_IOS_LLVM_BIN}/llvm-strip${x}" CACHE FILEPATH "")
|
|
||||||
# Not every LLVM distribution ships these (llvm-mingw does not); nothing in
|
|
||||||
# this build needs them, but CMake's Darwin support likes to know where they are.
|
|
||||||
foreach(tool INSTALL_NAME_TOOL:install-name-tool LIPO:lipo OTOOL:otool)
|
|
||||||
string(REPLACE ":" ";" tool "${tool}")
|
|
||||||
list(GET tool 0 var)
|
|
||||||
list(GET tool 1 exe)
|
|
||||||
if(EXISTS "${MKW_IOS_LLVM_BIN}/llvm-${exe}${x}")
|
|
||||||
set(CMAKE_${var} "${MKW_IOS_LLVM_BIN}/llvm-${exe}${x}" CACHE FILEPATH "")
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
set(CMAKE_LINKER "${MKW_IOS_LLVM_BIN}/ld64.lld${x}" CACHE FILEPATH "")
|
|
||||||
|
|
||||||
foreach(lang C CXX OBJC OBJCXX ASM)
|
|
||||||
set(CMAKE_${lang}_COMPILER_TARGET arm64-apple-ios17.0)
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
# Apple's clang searches the SDK's SubFrameworks implicitly and UIKit's own
|
|
||||||
# headers import from there.
|
|
||||||
foreach(lang C CXX OBJC OBJCXX)
|
|
||||||
set(CMAKE_${lang}_FLAGS_INIT "-iframework ${IOS_SDK}/System/Library/SubFrameworks")
|
|
||||||
endforeach()
|
|
||||||
# The SDK carries its own libc++ headers and Apple's clang uses those. Upstream
|
|
||||||
# clang prefers a libc++ shipped beside itself when there is one (llvm-mingw
|
|
||||||
# does), which pairs the wrong C++ library with Darwin's C headers.
|
|
||||||
foreach(lang CXX OBJCXX)
|
|
||||||
string(APPEND CMAKE_${lang}_FLAGS_INIT " -nostdinc++ -isystem ${IOS_SDK}/usr/include/c++/v1")
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
set(CMAKE_EXE_LINKER_FLAGS_INIT "-fuse-ld=lld ${MKW_IOS_CLANG_RT}")
|
|
||||||
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-fuse-ld=lld ${MKW_IOS_CLANG_RT}")
|
|
||||||
set(CMAKE_MODULE_LINKER_FLAGS_INIT "-fuse-ld=lld ${MKW_IOS_CLANG_RT}")
|
|
||||||
string(STRIP "${CMAKE_EXE_LINKER_FLAGS_INIT}" CMAKE_EXE_LINKER_FLAGS_INIT)
|
|
||||||
|
|
||||||
set(CMAKE_FIND_ROOT_PATH "${IOS_SDK}")
|
|
||||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
|
||||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
|
||||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
|
||||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleExecutable</key><string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
|
|
||||||
<key>CFBundleIdentifier</key><string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
|
|
||||||
<key>CFBundleName</key><string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
|
|
||||||
<key>CFBundleDisplayName</key><string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
|
|
||||||
<key>CFBundleVersion</key><string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
|
|
||||||
<key>CFBundleShortVersionString</key><string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
|
|
||||||
<key>CFBundlePackageType</key><string>APPL</string>
|
|
||||||
<key>LSRequiresIPhoneOS</key><true/>
|
|
||||||
<key>MinimumOSVersion</key><string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
|
|
||||||
<key>UIDeviceFamily</key><array><integer>1</integer><integer>2</integer></array>
|
|
||||||
<key>UILaunchScreen</key><dict/>
|
|
||||||
<key>UIRequiresFullScreen</key><true/>
|
|
||||||
<key>CADisableMinimumFrameDuration</key><true/>
|
|
||||||
<key>UIFileSharingEnabled</key><true/>
|
|
||||||
<key>LSSupportsOpeningDocumentsInPlace</key><true/>
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>application-identifier</key><string>CHANGEME.com.example.wiicompiled</string>
|
|
||||||
<key>com.apple.developer.team-identifier</key><string>CHANGEME</string>
|
|
||||||
<key>get-task-allow</key><true/>
|
|
||||||
<key>com.apple.developer.kernel.increased-memory-limit</key><true/>
|
|
||||||
<key>com.apple.developer.kernel.extended-virtual-addressing</key><true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,38 +1,28 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "runtime_config.h"
|
#include "nand_path.h"
|
||||||
|
#include "nand_settings.h"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cctype>
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
|
||||||
#include <fstream>
|
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <optional>
|
|
||||||
#include <random>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace RuntimeConsoleIdentity {
|
namespace RuntimeConsoleIdentity {
|
||||||
|
|
||||||
struct Identity {
|
struct Identity {
|
||||||
std::string serial;
|
std::string serial;
|
||||||
|
std::string productCode;
|
||||||
|
std::string area;
|
||||||
|
std::string gameRegion;
|
||||||
std::array<uint8_t, 6> mac;
|
std::array<uint8_t, 6> mac;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline bool IsValidSerial(const std::string& serial) {
|
|
||||||
return serial.size() == 9 &&
|
|
||||||
serial != "000000000" &&
|
|
||||||
std::all_of(serial.begin(), serial.end(),
|
|
||||||
[](unsigned char value) { return std::isdigit(value) != 0; });
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Identity FromSerial(std::string serial) {
|
inline Identity FromSerial(std::string serial) {
|
||||||
// Keep Nintendo's Wii OUI. The suffix is derived from the persisted serial
|
// Keep Nintendo's Wii OUI. The suffix is derived from the NAND serial
|
||||||
// so every API exposes one coherent, stable virtual-console identity.
|
// so every API exposes one coherent, stable virtual-console identity.
|
||||||
uint32_t hash = 2166136261u;
|
uint32_t hash = 2166136261u;
|
||||||
for (const unsigned char value : serial) {
|
for (const unsigned char value : serial) {
|
||||||
@@ -46,6 +36,7 @@ inline Identity FromSerial(std::string serial) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
std::move(serial),
|
std::move(serial),
|
||||||
|
{}, {}, {},
|
||||||
{
|
{
|
||||||
0x00,
|
0x00,
|
||||||
0x09,
|
0x09,
|
||||||
@@ -57,83 +48,23 @@ inline Identity FromSerial(std::string serial) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
inline std::optional<std::string> ReadSerial(const std::filesystem::path& path) {
|
inline Identity LoadFromNand() {
|
||||||
std::ifstream input(path);
|
const auto root = RuntimeNandPath::DiscoverNandRootPath();
|
||||||
std::string line;
|
const auto settings = RuntimeNandSettings::Read(root);
|
||||||
if (!input || !std::getline(input, line)) {
|
if (!settings || !RuntimeNandSettings::HasIdentity(*settings)) {
|
||||||
return std::nullopt;
|
RuntimeNandPath::FailNandRoot(
|
||||||
|
"NAND setting.txt is missing or has invalid console identity fields (SERNO, CODE, AREA, GAME)",
|
||||||
|
root / "title/00000001/00000002/data/setting.txt");
|
||||||
}
|
}
|
||||||
constexpr std::string_view prefix = "serial=";
|
Identity identity = FromSerial(settings->at("SERNO"));
|
||||||
if (line.rfind(prefix, 0) != 0) {
|
identity.productCode = settings->at("CODE");
|
||||||
return std::nullopt;
|
identity.area = settings->at("AREA");
|
||||||
}
|
identity.gameRegion = settings->at("GAME");
|
||||||
std::string serial = line.substr(prefix.size());
|
return identity;
|
||||||
if (!IsValidSerial(serial)) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
return serial;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool WriteSerial(const std::filesystem::path& path, const std::string& serial) {
|
|
||||||
std::error_code ec;
|
|
||||||
std::filesystem::create_directories(path.parent_path(), ec);
|
|
||||||
if (ec) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::filesystem::path temporary = path;
|
|
||||||
temporary += ".tmp";
|
|
||||||
{
|
|
||||||
std::ofstream output(temporary, std::ios::trunc);
|
|
||||||
if (!output) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
output << "serial=" << serial << '\n';
|
|
||||||
output.close();
|
|
||||||
if (!output) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::filesystem::rename(temporary, path, ec);
|
|
||||||
if (!ec) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
std::filesystem::remove(temporary, ec);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::string GenerateSerial() {
|
|
||||||
std::random_device entropy;
|
|
||||||
std::seed_seq seed{
|
|
||||||
entropy(),
|
|
||||||
entropy(),
|
|
||||||
entropy(),
|
|
||||||
entropy(),
|
|
||||||
};
|
|
||||||
std::mt19937 generator(seed);
|
|
||||||
std::uniform_int_distribution<uint32_t> distribution(100000000u, 999999999u);
|
|
||||||
return std::to_string(distribution(generator));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Identity LoadOrCreate(const std::filesystem::path& path) {
|
|
||||||
if (const auto serial = ReadSerial(path)) {
|
|
||||||
return FromSerial(*serial);
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string generated = GenerateSerial();
|
|
||||||
if (WriteSerial(path, generated)) {
|
|
||||||
return FromSerial(generated);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remain operational in a read-only environment. This fallback matches
|
|
||||||
// Dolphin's deterministic serial while keeping the same valid identity shape.
|
|
||||||
return FromSerial("123456789");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline const Identity& Current() {
|
inline const Identity& Current() {
|
||||||
static const Identity identity =
|
static const Identity identity = LoadFromNand();
|
||||||
LoadOrCreate(RuntimeConfigFile::ApplicationDataDirectory() / "ConsoleIdentity.txt");
|
|
||||||
return identity;
|
return identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// The single vocabulary shared by everything that has to turn a Config.toml
|
||||||
|
// controller name into a real button: the F10 settings bar, the macro engine,
|
||||||
|
// and the startup mapping pass. Keeping one table here means a name that the
|
||||||
|
// settings bar offers is always a name the config parser accepts, and vice
|
||||||
|
// versa; the two used to drift because each side carried its own copy.
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include <SDL3/SDL_gamepad.h>
|
||||||
|
#include <dolphin/pad.h>
|
||||||
|
|
||||||
|
namespace ControllerNames {
|
||||||
|
|
||||||
|
// A GameCube button as the game sees it, with the Config.toml key that selects
|
||||||
|
// it. Order matches RuntimeConfigFile::kControllerButtonKeys.
|
||||||
|
struct GameCubeButtonItem {
|
||||||
|
const char* configKey;
|
||||||
|
const char* label;
|
||||||
|
PADButton padButton;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr std::array<GameCubeButtonItem, PAD_BUTTON_COUNT> kGameCubeButtons = {{
|
||||||
|
{"a", "A", PAD_BUTTON_A},
|
||||||
|
{"b", "B", PAD_BUTTON_B},
|
||||||
|
{"x", "X", PAD_BUTTON_X},
|
||||||
|
{"y", "Y", PAD_BUTTON_Y},
|
||||||
|
{"start", "Start", PAD_BUTTON_START},
|
||||||
|
{"z", "Z", PAD_TRIGGER_Z},
|
||||||
|
{"l", "L", PAD_TRIGGER_L},
|
||||||
|
{"r", "R", PAD_TRIGGER_R},
|
||||||
|
{"up", "D-pad Up", PAD_BUTTON_UP},
|
||||||
|
{"down", "D-pad Down", PAD_BUTTON_DOWN},
|
||||||
|
{"left", "D-pad Left", PAD_BUTTON_LEFT},
|
||||||
|
{"right", "D-pad Right", PAD_BUTTON_RIGHT},
|
||||||
|
}};
|
||||||
|
|
||||||
|
// A physical button on the host pad. Names are positional (south/east/...)
|
||||||
|
// rather than Xbox-labelled so one config reads the same on any hardware.
|
||||||
|
struct NativeButtonItem {
|
||||||
|
const char* configName;
|
||||||
|
const char* label;
|
||||||
|
uint32_t nativeButton;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr std::array<NativeButtonItem, SDL_GAMEPAD_BUTTON_COUNT + 1> kNativeButtons = {{
|
||||||
|
{"unmapped", "Unmapped / analog trigger", PAD_NATIVE_BUTTON_INVALID},
|
||||||
|
{"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH},
|
||||||
|
{"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST},
|
||||||
|
{"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST},
|
||||||
|
{"north", "North (Y / Triangle)", SDL_GAMEPAD_BUTTON_NORTH},
|
||||||
|
{"back", "Back / Select / Create", SDL_GAMEPAD_BUTTON_BACK},
|
||||||
|
{"guide", "Guide / Home / PS", SDL_GAMEPAD_BUTTON_GUIDE},
|
||||||
|
{"start", "Start / Options", SDL_GAMEPAD_BUTTON_START},
|
||||||
|
{"left_stick", "Left stick click (L3)", SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
||||||
|
{"right_stick", "Right stick click (R3)", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
|
||||||
|
{"left_shoulder", "Left bumper (LB / L1)", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
||||||
|
{"right_shoulder", "Right bumper (RB / R1)", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
||||||
|
{"dpad_up", "D-pad Up", SDL_GAMEPAD_BUTTON_DPAD_UP},
|
||||||
|
{"dpad_down", "D-pad Down", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
||||||
|
{"dpad_left", "D-pad Left", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
|
||||||
|
{"dpad_right", "D-pad Right", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||||
|
{"misc1", "Misc 1 / Share / Mic", SDL_GAMEPAD_BUTTON_MISC1},
|
||||||
|
{"right_paddle1", "Right paddle 1", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
|
||||||
|
{"left_paddle1", "Left paddle 1", SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
|
||||||
|
{"right_paddle2", "Right paddle 2", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
|
||||||
|
{"left_paddle2", "Left paddle 2", SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
|
||||||
|
{"touchpad", "Touchpad click", SDL_GAMEPAD_BUTTON_TOUCHPAD},
|
||||||
|
{"misc2", "Misc 2", SDL_GAMEPAD_BUTTON_MISC2},
|
||||||
|
{"misc3", "Misc 3 / GC L click", SDL_GAMEPAD_BUTTON_MISC3},
|
||||||
|
{"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4},
|
||||||
|
{"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5},
|
||||||
|
{"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6},
|
||||||
|
}};
|
||||||
|
|
||||||
|
inline std::string TrimToken(std::string_view token) {
|
||||||
|
const size_t begin = token.find_first_not_of(" \t");
|
||||||
|
if (begin == std::string_view::npos) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const size_t end = token.find_last_not_of(" \t");
|
||||||
|
return std::string(token.substr(begin, end - begin + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const NativeButtonItem* FindNativeButton(std::string_view configName) {
|
||||||
|
const std::string name = TrimToken(configName);
|
||||||
|
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
|
||||||
|
[&](const NativeButtonItem& item) { return name == item.configName; });
|
||||||
|
return it == kNativeButtons.end() ? nullptr : &*it;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls back to the "unmapped" entry so callers always have a label to draw.
|
||||||
|
inline const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) {
|
||||||
|
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
|
||||||
|
[&](const NativeButtonItem& item) { return nativeButton == item.nativeButton; });
|
||||||
|
return it == kNativeButtons.end() ? kNativeButtons.front() : *it;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const GameCubeButtonItem* FindGameCubeButton(std::string_view configKey) {
|
||||||
|
const std::string key = TrimToken(configKey);
|
||||||
|
const auto it = std::find_if(kGameCubeButtons.begin(), kGameCubeButtons.end(),
|
||||||
|
[&](const GameCubeButtonItem& item) { return key == item.configKey; });
|
||||||
|
return it == kGameCubeButtons.end() ? nullptr : &*it;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "up" or "up,a" -> the OR of those GC button bits. Unknown names are skipped so
|
||||||
|
// a typo costs one button instead of the whole macro.
|
||||||
|
inline uint16_t GameCubeMaskFromKeys(std::string_view keys) {
|
||||||
|
uint16_t mask = 0;
|
||||||
|
size_t begin = 0;
|
||||||
|
while (begin <= keys.size()) {
|
||||||
|
const size_t comma = keys.find(',', begin);
|
||||||
|
const std::string_view token =
|
||||||
|
keys.substr(begin, comma == std::string_view::npos ? std::string_view::npos : comma - begin);
|
||||||
|
if (const GameCubeButtonItem* item = FindGameCubeButton(token)) {
|
||||||
|
mask |= static_cast<uint16_t>(item->padButton);
|
||||||
|
}
|
||||||
|
if (comma == std::string_view::npos) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
begin = comma + 1;
|
||||||
|
}
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string GameCubeKeysFromMask(uint16_t mask) {
|
||||||
|
std::string keys;
|
||||||
|
for (const auto& item : kGameCubeButtons) {
|
||||||
|
if ((mask & static_cast<uint16_t>(item.padButton)) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!keys.empty()) {
|
||||||
|
keys += ',';
|
||||||
|
}
|
||||||
|
keys += item.configKey;
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string GameCubeLabelsFromMask(uint16_t mask) {
|
||||||
|
std::string labels;
|
||||||
|
for (const auto& item : kGameCubeButtons) {
|
||||||
|
if ((mask & static_cast<uint16_t>(item.padButton)) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!labels.empty()) {
|
||||||
|
labels += " + ";
|
||||||
|
}
|
||||||
|
labels += item.label;
|
||||||
|
}
|
||||||
|
return labels.empty() ? std::string("None") : labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ControllerNames
|
||||||
@@ -15,17 +15,6 @@ namespace GuestFlat {
|
|||||||
// of a global.
|
// of a global.
|
||||||
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
|
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
|
||||||
inline constexpr size_t kGuestPageSize = 0x1000;
|
inline constexpr size_t kGuestPageSize = 0x1000;
|
||||||
#ifdef MKW_PLATFORM_IOS
|
|
||||||
// No fixed base works on every device: probing an iPhone 17 Pro and an iPad
|
|
||||||
// Pro M5 gave disjoint sets of free 4 GiB windows (448 GiB only, versus 12-48
|
|
||||||
// GiB), and the extended-virtual-addressing entitlement changes neither. So the
|
|
||||||
// base is whatever the kernel hands out at Initialize().
|
|
||||||
//
|
|
||||||
// Note that memory_access.h routes every flat access to the checked path on
|
|
||||||
// Apple, so this has no reader yet. It is groundwork, not a hot path.
|
|
||||||
extern uint8_t* g_flatGuestBase;
|
|
||||||
#define MKW_FLAT_GUEST_BASE (GuestFlat::g_flatGuestBase)
|
|
||||||
#else
|
|
||||||
#if defined(__x86_64__)
|
#if defined(__x86_64__)
|
||||||
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
|
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
|
||||||
// placement.
|
// placement.
|
||||||
@@ -51,10 +40,7 @@ inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0010'0000'0000ull;
|
|||||||
#error "guest_flat_memory.h has no fixed flat guest base chosen for this architecture"
|
#error "guest_flat_memory.h has no fixed flat guest base chosen for this architecture"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load
|
|
||||||
// of a global.
|
|
||||||
#define MKW_FLAT_GUEST_BASE (reinterpret_cast<uint8_t*>(GuestFlat::kFixedFlatGuestBase))
|
#define MKW_FLAT_GUEST_BASE (reinterpret_cast<uint8_t*>(GuestFlat::kFixedFlatGuestBase))
|
||||||
#endif
|
|
||||||
|
|
||||||
enum class Backing {
|
enum class Backing {
|
||||||
Owned,
|
Owned,
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Per-port expression bindings for the GameCube controls, plus import of a
|
||||||
|
// Dolphin GCPadNew.ini.
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <dolphin/pad.h>
|
||||||
|
|
||||||
|
namespace InputBindings {
|
||||||
|
|
||||||
|
// The controls an expression can drive, in Dolphin's own naming so an
|
||||||
|
// imported config maps across without translation.
|
||||||
|
struct ControlInfo {
|
||||||
|
const char* dolphinName;
|
||||||
|
const char* label;
|
||||||
|
uint16_t padButton; // 0 for the analog-only controls below
|
||||||
|
int analog; // 0 none, 1 trigger L, 2 trigger R
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr std::array<ControlInfo, 14> kControls = {{
|
||||||
|
{"Buttons/A", "A", PAD_BUTTON_A, 0},
|
||||||
|
{"Buttons/B", "B", PAD_BUTTON_B, 0},
|
||||||
|
{"Buttons/X", "X", PAD_BUTTON_X, 0},
|
||||||
|
{"Buttons/Y", "Y", PAD_BUTTON_Y, 0},
|
||||||
|
{"Buttons/Z", "Z", PAD_TRIGGER_Z, 0},
|
||||||
|
{"Buttons/Start", "Start", PAD_BUTTON_START, 0},
|
||||||
|
{"D-Pad/Up", "D-pad Up", PAD_BUTTON_UP, 0},
|
||||||
|
{"D-Pad/Down", "D-pad Down", PAD_BUTTON_DOWN, 0},
|
||||||
|
{"D-Pad/Left", "D-pad Left", PAD_BUTTON_LEFT, 0},
|
||||||
|
{"D-Pad/Right", "D-pad Right", PAD_BUTTON_RIGHT, 0},
|
||||||
|
{"Triggers/L", "L", PAD_TRIGGER_L, 1},
|
||||||
|
{"Triggers/R", "R", PAD_TRIGGER_R, 2},
|
||||||
|
{"Triggers/L-Analog", "L analog", 0, 1},
|
||||||
|
{"Triggers/R-Analog", "R analog", 0, 2},
|
||||||
|
}};
|
||||||
|
|
||||||
|
void Reload() noexcept;
|
||||||
|
|
||||||
|
// The pad library has PADBlockInput but no matching query, so the settings
|
||||||
|
// overlay reports its own state here.
|
||||||
|
void SetInputBlocked(bool blocked) noexcept;
|
||||||
|
bool InputBlocked() noexcept;
|
||||||
|
|
||||||
|
// Mix expression output into a freshly read status set. Call once per guest
|
||||||
|
// PADRead, after every other input source has been merged.
|
||||||
|
void Apply(PADStatus* statuses) noexcept;
|
||||||
|
|
||||||
|
std::string GetExpression(uint32_t port, size_t control) noexcept;
|
||||||
|
// Returns false and fills error if the text does not parse; the binding is
|
||||||
|
// left unchanged in that case.
|
||||||
|
bool SetExpression(uint32_t port, size_t control, const std::string& text, std::string& error) noexcept;
|
||||||
|
|
||||||
|
// True while the control's expression is above the press threshold.
|
||||||
|
bool IsActive(uint32_t port, size_t control) noexcept;
|
||||||
|
|
||||||
|
// The default Dolphin config location on Windows, then next to the executable.
|
||||||
|
std::string DefaultDolphinConfigPath() noexcept;
|
||||||
|
|
||||||
|
// Imports [GCPad<padIndex>] into the given port. Returns the number of controls
|
||||||
|
// imported, or -1 on failure with error filled.
|
||||||
|
int ImportDolphinConfig(const std::string& path, int padIndex, uint32_t port,
|
||||||
|
std::string& summary, std::string& error) noexcept;
|
||||||
|
|
||||||
|
} // namespace InputBindings
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Dolphin-compatible input expressions.
|
||||||
|
//
|
||||||
|
// Values are doubles in Dolphin's ControlState style; a control counts as
|
||||||
|
// pressed above kConditionThreshold. Timing matches Dolphin: wall-clock
|
||||||
|
// seconds on a steady clock, so an expression copied from GCPadNew.ini
|
||||||
|
// behaves the same here as it does there.
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace InputExpr {
|
||||||
|
|
||||||
|
inline constexpr double kConditionThreshold = 0.5;
|
||||||
|
|
||||||
|
// Resolves a backtick-quoted input name to its current value.
|
||||||
|
using InputSource = std::function<double(const std::string&)>;
|
||||||
|
|
||||||
|
struct Node;
|
||||||
|
|
||||||
|
class Expression {
|
||||||
|
public:
|
||||||
|
Expression();
|
||||||
|
~Expression();
|
||||||
|
Expression(Expression&&) noexcept;
|
||||||
|
Expression& operator=(Expression&&) noexcept;
|
||||||
|
|
||||||
|
// Returns false and fills error on a syntax problem.
|
||||||
|
static bool Parse(const std::string& text, Expression& out, std::string& error);
|
||||||
|
|
||||||
|
bool Empty() const { return m_root == nullptr; }
|
||||||
|
double Evaluate(const InputSource& source) const;
|
||||||
|
|
||||||
|
// Input names the expression references, for diagnostics.
|
||||||
|
std::vector<std::string> ReferencedInputs() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<Node> m_root;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parses a Dolphin GCPadNew.ini and returns the expression text for each
|
||||||
|
// control of the requested pad, keyed by Dolphin's own control names
|
||||||
|
// ("Buttons/A", "D-Pad/Up", "Triggers/L", ...). Returns false if the file
|
||||||
|
// cannot be read or the section is missing.
|
||||||
|
bool ReadDolphinConfig(const std::filesystem::path& path, int padIndex,
|
||||||
|
std::vector<std::pair<std::string, std::string>>& controls,
|
||||||
|
std::string& deviceName, std::string& error);
|
||||||
|
|
||||||
|
} // namespace InputExpr
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace RuntimeNandSettings {
|
||||||
|
|
||||||
|
using Settings = std::map<std::string, std::string>;
|
||||||
|
|
||||||
|
// Wii setting.txt is a 256-byte buffer encrypted with a rotating XOR key.
|
||||||
|
inline std::optional<Settings> Read(const std::filesystem::path& nandRoot) {
|
||||||
|
std::ifstream input(nandRoot / "title/00000001/00000002/data/setting.txt",
|
||||||
|
std::ios::binary);
|
||||||
|
std::array<uint8_t, 256> bytes{};
|
||||||
|
if (!input.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
uint32_t key = 0x73B5DBFAu;
|
||||||
|
std::string decoded;
|
||||||
|
for (const uint8_t byte : bytes) {
|
||||||
|
const char value = static_cast<char>(byte ^ static_cast<uint8_t>(key));
|
||||||
|
key = (key << 1) | (key >> 31);
|
||||||
|
if (value == '\0') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (value != '\r') {
|
||||||
|
decoded += value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Settings settings;
|
||||||
|
for (size_t start = 0; start < decoded.size();) {
|
||||||
|
const size_t end = decoded.find('\n', start);
|
||||||
|
const std::string line = decoded.substr(start, end - start);
|
||||||
|
const size_t equals = line.find('=');
|
||||||
|
if (equals != std::string::npos && equals != 0) {
|
||||||
|
settings.emplace(line.substr(0, equals), line.substr(equals + 1));
|
||||||
|
}
|
||||||
|
if (end == std::string::npos) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
start = end + 1;
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool HasIdentity(const Settings& settings) {
|
||||||
|
const auto serial = settings.find("SERNO");
|
||||||
|
if (serial == settings.end() || serial->second.empty() || serial->second.size() > 9 ||
|
||||||
|
serial->second.find_first_not_of("0123456789") != std::string::npos ||
|
||||||
|
serial->second.find_first_not_of('0') == std::string::npos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const auto& field : {std::pair{"CODE", 5u}, {"AREA", 3u}, {"GAME", 2u}}) {
|
||||||
|
const auto value = settings.find(field.first);
|
||||||
|
if (value == settings.end() || value->second.empty() ||
|
||||||
|
value->second.size() > field.second) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace RuntimeNandSettings
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <map>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -89,6 +90,8 @@ struct RuntimeUserConfig {
|
|||||||
// comma-separated SDL-style physical button names ("south", or
|
// comma-separated SDL-style physical button names ("south", or
|
||||||
// "dpad_up,left_shoulder") as values; pressing either bound button counts.
|
// "dpad_up,left_shoulder") as values; pressing either bound button counts.
|
||||||
std::array<std::optional<std::string>, 12> controllerButtons;
|
std::array<std::optional<std::string>, 12> controllerButtons;
|
||||||
|
std::optional<bool> rumbleEnabled;
|
||||||
|
std::map<std::string, std::string> controllerExpressions;
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace RuntimeConfigFile {
|
namespace RuntimeConfigFile {
|
||||||
@@ -407,6 +410,17 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
|||||||
FindConfigValue<std::string>(document, "controller", buttonKeys[index]);
|
FindConfigValue<std::string>(document, "controller", buttonKeys[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.rumbleEnabled = FindConfigValue<bool>(document, "controller", "rumble");
|
||||||
|
|
||||||
|
if (const auto* section = document.contains("controller") ? &document.at("controller") : nullptr;
|
||||||
|
section != nullptr && section->is_table()) {
|
||||||
|
for (const auto& [key, value] : section->as_table()) {
|
||||||
|
if (key.rfind("expr_", 0) == 0 && value.is_string()) {
|
||||||
|
config.controllerExpressions[key] = value.as_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
|
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
|
||||||
config.windowPosX = FindConfigInt(document, "video", "window_x");
|
config.windowPosX = FindConfigInt(document, "video", "window_x");
|
||||||
config.windowPosY = FindConfigInt(document, "video", "window_y");
|
config.windowPosY = FindConfigInt(document, "video", "window_y");
|
||||||
@@ -677,6 +691,25 @@ inline bool SetControllerButton(size_t index, std::string value) {
|
|||||||
return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value));
|
return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline std::string ControllerExpression(const std::string& key) {
|
||||||
|
const auto it = Get().controllerExpressions.find(key);
|
||||||
|
return it == Get().controllerExpressions.end() ? std::string() : it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool SetControllerExpression(const std::string& key, const std::string& value) {
|
||||||
|
Mutable().controllerExpressions[key] = value;
|
||||||
|
return WriteSetting("controller", key, FormatString(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool RumbleEnabled(bool fallback = true) {
|
||||||
|
return Get().rumbleEnabled.value_or(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool SetRumbleEnabled(bool value) {
|
||||||
|
Mutable().rumbleEnabled = value;
|
||||||
|
return WriteSetting("controller", "rumble", value ? "true" : "false");
|
||||||
|
}
|
||||||
|
|
||||||
inline bool SetAudioVolume(float value) {
|
inline bool SetAudioVolume(float value) {
|
||||||
value = std::clamp(value, 0.0f, 1.0f);
|
value = std::clamp(value, 0.0f, 1.0f);
|
||||||
Mutable().audioVolume = value;
|
Mutable().audioVolume = value;
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ void InitializeRuntimeSettings() noexcept;
|
|||||||
// Draw the F10 settings bar before each Aurora present.
|
// Draw the F10 settings bar before each Aurora present.
|
||||||
void HandleEvents(const AuroraEvent* events) noexcept;
|
void HandleEvents(const AuroraEvent* events) noexcept;
|
||||||
void Draw() noexcept;
|
void Draw() noexcept;
|
||||||
void ToggleTopBar() noexcept;
|
|
||||||
bool TopBarVisible() noexcept;
|
|
||||||
bool FpsOverlayBounds(float* minX, float* minY, float* maxX, float* maxY) noexcept;
|
|
||||||
bool StartupScreenVisible() noexcept;
|
bool StartupScreenVisible() noexcept;
|
||||||
void NotifyStrapInputAccepted() noexcept;
|
void NotifyStrapInputAccepted() noexcept;
|
||||||
void AdvancePresentedFrame() noexcept;
|
void AdvancePresentedFrame() noexcept;
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <filesystem>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include <imgui.h>
|
|
||||||
|
|
||||||
// Icons are CC BY 3.0 by Zacksly - see runtime/assets/touch/ATTRIBUTION.txt.
|
|
||||||
namespace TouchArt {
|
|
||||||
|
|
||||||
// By file stem, e.g. "a" or "d-pad". Null when the file is missing; callers
|
|
||||||
// fall back to drawing the shape.
|
|
||||||
ImTextureID Get(const char* name);
|
|
||||||
|
|
||||||
// Call once, after aurora is up.
|
|
||||||
void Preload();
|
|
||||||
|
|
||||||
} // namespace TouchArt
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
struct PADStatus;
|
|
||||||
|
|
||||||
// On-screen touch controls. Read() fills a status array the caller merges over
|
|
||||||
// aurora's PADRead result.
|
|
||||||
//
|
|
||||||
// Polls SDL's finger list directly rather than using ImGui widgets: steering,
|
|
||||||
// accelerating and firing happen at once, and ImGui's SDL backend collapses
|
|
||||||
// touch to a single emulated mouse.
|
|
||||||
namespace TouchPad {
|
|
||||||
|
|
||||||
// True when the host has a touch screen and no physical pad is driving port 0.
|
|
||||||
bool IsActive();
|
|
||||||
|
|
||||||
// Port 0 only. Returns false when touch is not driving input, leaving the
|
|
||||||
// caller's existing statuses untouched.
|
|
||||||
bool Read(std::array<PADStatus, 4>& statuses);
|
|
||||||
|
|
||||||
// Draws the control overlay. Must be called inside a live ImGui frame.
|
|
||||||
void Draw();
|
|
||||||
|
|
||||||
|
|
||||||
} // namespace TouchPad
|
|
||||||
@@ -1,20 +1,5 @@
|
|||||||
#include "discord_presence.h"
|
#include "discord_presence.h"
|
||||||
|
|
||||||
|
|
||||||
#ifdef MKW_PLATFORM_IOS
|
|
||||||
|
|
||||||
// iOS has no Discord client for the IPC socket to reach, so the whole thing is
|
|
||||||
// stubbed out rather than retrying a connection that cannot succeed.
|
|
||||||
namespace DiscordPresence {
|
|
||||||
void Initialize(const std::string&, const std::string&) {}
|
|
||||||
void SetClient(const std::string&) {}
|
|
||||||
void SetActivity(Activity) {}
|
|
||||||
void Reset() {}
|
|
||||||
void Shutdown() {}
|
|
||||||
} // namespace DiscordPresence
|
|
||||||
|
|
||||||
#else
|
|
||||||
|
|
||||||
#include "runtime_log.h"
|
#include "runtime_log.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -479,5 +464,3 @@ void Shutdown() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} // namespace DiscordPresence
|
} // namespace DiscordPresence
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -256,8 +256,12 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr
|
|||||||
gf.cpuContext.srr0 = entryPoint;
|
gf.cpuContext.srr0 = entryPoint;
|
||||||
|
|
||||||
// The host stack models only translated host calls; the guest stack starts
|
// The host stack models only translated host calls; the guest stack starts
|
||||||
// at stackBase in the CPU context above.
|
// at stackBase in the CPU context above. 64 KiB is too small for deep
|
||||||
constexpr size_t kHostStackSize = 64 * 1024;
|
// translated/HLE call chains (notably NW4R's sound worker), and on macOS
|
||||||
|
// it can exhaust the guarded coroutine stack as unrelated host work (such
|
||||||
|
// as a window resize) adds a little more nesting. Keep enough headroom for
|
||||||
|
// those chains while the guest stack remains separately bounded.
|
||||||
|
constexpr size_t kHostStackSize = 1024 * 1024;
|
||||||
gf.fiber = HostContext::Create(kHostStackSize, FiberProc,
|
gf.fiber = HostContext::Create(kHostStackSize, FiberProc,
|
||||||
reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr)));
|
reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr)));
|
||||||
|
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
#include "guest_flat_memory.h"
|
|
||||||
|
|
||||||
// iOS variant of the macOS backend. <mach/mach_vm.h> is not in the iOS SDK, so
|
|
||||||
// the vm_* calls from <mach/mach.h> are used instead, and the backing store is
|
|
||||||
// an anonymous mapping rather than a file in /tmp, which the sandbox denies.
|
|
||||||
|
|
||||||
#include <mach/mach.h>
|
|
||||||
#include <mach/mach_error.h>
|
|
||||||
#include <mach/vm_map.h>
|
|
||||||
#include <sys/mman.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cerrno>
|
|
||||||
#include <cstring>
|
|
||||||
#include <string>
|
|
||||||
#include <mutex>
|
|
||||||
#include <stdexcept>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace GuestFlat {
|
|
||||||
|
|
||||||
uint8_t* g_flatGuestBase = nullptr;
|
|
||||||
|
|
||||||
bool g_requiresCheckedAccess = false;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
struct Mapping { uint32_t base; uint64_t size; uint8_t* host; };
|
|
||||||
std::mutex g_mutex;
|
|
||||||
std::vector<Mapping> g_mappings;
|
|
||||||
std::vector<RegionRequest> g_layout;
|
|
||||||
uint8_t* g_base = nullptr;
|
|
||||||
bool g_active = false;
|
|
||||||
|
|
||||||
uint64_t Offset(const RegionRequest& r) {
|
|
||||||
if (r.backing == Backing::Mem1) return r.base & 0x1fffffffu;
|
|
||||||
if (r.backing == Backing::Mem2) return (r.base & 0x1fffffffu) - 0x10000000u;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
bool Same(const std::vector<RegionRequest>& a, const std::vector<RegionRequest>& b) {
|
|
||||||
return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(),
|
|
||||||
[](const auto& x, const auto& y) { return x.base == y.base && x.size == y.size && x.backing == y.backing; });
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
bool IsActive() { return g_active; }
|
|
||||||
|
|
||||||
void Initialize(const std::vector<RegionRequest>& regions) {
|
|
||||||
std::lock_guard lock(g_mutex);
|
|
||||||
g_requiresCheckedAccess = static_cast<size_t>(getpagesize()) > kGuestPageSize;
|
|
||||||
if (g_active) {
|
|
||||||
if (!Same(g_layout, regions)) throw std::runtime_error("flat guest layout cannot be remapped");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The free window differs per device, so let the kernel place it.
|
|
||||||
vm_address_t address = 0;
|
|
||||||
if (vm_allocate(mach_task_self(), &address, static_cast<vm_size_t>(kGuestSpaceSize),
|
|
||||||
VM_FLAGS_ANYWHERE) != KERN_SUCCESS) {
|
|
||||||
throw std::runtime_error("unable to reserve a 4 GiB iOS guest address space; a device with\n"
|
|
||||||
"less than 4 GB of RAM may need the extended-virtual-addressing entitlement");
|
|
||||||
}
|
|
||||||
g_base = reinterpret_cast<uint8_t*>(address);
|
|
||||||
|
|
||||||
struct Store { Backing kind; uint32_t owned; uint64_t size; uint8_t* mem; };
|
|
||||||
std::vector<Store> stores;
|
|
||||||
|
|
||||||
// Initialize() is called inside a try that reports a dialog rather than
|
|
||||||
// aborting, so a throw here has to give the reservation back.
|
|
||||||
struct Rollback {
|
|
||||||
vm_address_t reservation;
|
|
||||||
std::vector<Store>* stores;
|
|
||||||
bool armed = true;
|
|
||||||
~Rollback() {
|
|
||||||
if (!armed) return;
|
|
||||||
if (stores != nullptr) {
|
|
||||||
for (const auto& s : *stores) {
|
|
||||||
if (s.mem != nullptr) munmap(s.mem, static_cast<size_t>(s.size));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (reservation != 0) {
|
|
||||||
vm_deallocate(mach_task_self(), reservation,
|
|
||||||
static_cast<vm_size_t>(kGuestSpaceSize));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} rollback{address, &stores};
|
|
||||||
for (const auto& r : regions) {
|
|
||||||
if (!r.size) continue;
|
|
||||||
const uint32_t owned = r.backing == Backing::Owned ? r.base : 0;
|
|
||||||
auto it = std::find_if(stores.begin(), stores.end(),
|
|
||||||
[&](const Store& s) { return s.kind == r.backing && s.owned == owned; });
|
|
||||||
const uint64_t need = Offset(r) + r.size;
|
|
||||||
if (it == stores.end()) stores.push_back({r.backing, owned, need, nullptr});
|
|
||||||
else it->size = std::max(it->size, need);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto& s : stores) {
|
|
||||||
void* mem = mmap(nullptr, static_cast<size_t>(s.size), PROT_READ | PROT_WRITE,
|
|
||||||
MAP_PRIVATE | MAP_ANON, -1, 0);
|
|
||||||
if (mem == MAP_FAILED) throw std::runtime_error("unable to create iOS guest backing store");
|
|
||||||
s.mem = static_cast<uint8_t*>(mem);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& r : regions) {
|
|
||||||
if (!r.size) continue;
|
|
||||||
const uint32_t owned = r.backing == Backing::Owned ? r.base : 0;
|
|
||||||
const auto& s = *std::find_if(stores.begin(), stores.end(),
|
|
||||||
[&](const Store& x) { return x.kind == r.backing && x.owned == owned; });
|
|
||||||
uint8_t* host = s.mem + Offset(r);
|
|
||||||
|
|
||||||
// Alias the same physical pages into the reservation. VM_FLAGS_OVERWRITE
|
|
||||||
// replaces the placeholder in place; copy=false is what makes this a
|
|
||||||
// shared alias rather than a copy-on-write duplicate.
|
|
||||||
vm_address_t target = reinterpret_cast<vm_address_t>(g_base + r.base);
|
|
||||||
vm_prot_t cur = VM_PROT_NONE, max = VM_PROT_NONE;
|
|
||||||
const kern_return_t kr = vm_remap(
|
|
||||||
mach_task_self(), &target, static_cast<vm_size_t>(r.size), 0,
|
|
||||||
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE, mach_task_self(),
|
|
||||||
reinterpret_cast<vm_address_t>(host), FALSE, &cur, &max, VM_INHERIT_SHARE);
|
|
||||||
if (kr != KERN_SUCCESS) {
|
|
||||||
throw std::runtime_error(std::string("vm_remap failed for the iOS guest alias: ") +
|
|
||||||
mach_error_string(kr));
|
|
||||||
}
|
|
||||||
if (target != reinterpret_cast<vm_address_t>(g_base + r.base)) {
|
|
||||||
throw std::runtime_error("vm_remap placed the iOS guest alias at the wrong address");
|
|
||||||
}
|
|
||||||
if (mprotect(g_base + r.base, static_cast<size_t>(r.size), PROT_READ | PROT_WRITE) != 0) {
|
|
||||||
throw std::runtime_error(std::string("mprotect failed on the iOS guest alias: ") +
|
|
||||||
std::strerror(errno));
|
|
||||||
}
|
|
||||||
g_mappings.push_back({r.base, r.size, host});
|
|
||||||
}
|
|
||||||
|
|
||||||
g_layout = regions;
|
|
||||||
// Publish the base only now. Until the aliases are mapped the reservation is
|
|
||||||
// an unbacked placeholder, and the header advertises this pointer as the base
|
|
||||||
// for every translated access - so a caller that trusted it early would fault.
|
|
||||||
rollback.armed = false;
|
|
||||||
g_flatGuestBase = g_base;
|
|
||||||
g_active = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t* HostPointer(uint32_t a) {
|
|
||||||
for (const auto& m : g_mappings)
|
|
||||||
if (a >= m.base && uint64_t(a - m.base) < m.size) return m.host + (a - m.base);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
void ProtectDeferredRange(uint32_t, size_t) {}
|
|
||||||
void UnprotectDeferredRange(uint32_t, size_t) {}
|
|
||||||
void RegisterExecutableRange(uint32_t, uint32_t) {}
|
|
||||||
FaultCounters Counters() { return {}; }
|
|
||||||
void LogFaultSummary() noexcept {}
|
|
||||||
bool HandleAccessViolation(void*, bool) noexcept { return false; }
|
|
||||||
} // namespace GuestFlat
|
|
||||||
@@ -1,21 +1,75 @@
|
|||||||
#include "hle_stubs.h"
|
#include "hle_stubs.h"
|
||||||
#include "touch_pad.h"
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include "memory.h"
|
#include "memory.h"
|
||||||
#include "hle/controller_status_contract.h"
|
#include "hle/controller_status_contract.h"
|
||||||
|
#include "input_bindings.h"
|
||||||
#include "wii_remote_input.h"
|
#include "wii_remote_input.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
|
#include <SDL3/SDL_gamepad.h>
|
||||||
#include <dolphin/pad.h>
|
#include <dolphin/pad.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
std::atomic<bool> g_rumbleEnabled{true};
|
||||||
|
|
||||||
|
bool NativeButtonHeld(SDL_Gamepad* gamepad, uint32_t nativeButton) {
|
||||||
|
if (gamepad == nullptr || nativeButton == PAD_NATIVE_BUTTON_INVALID ||
|
||||||
|
nativeButton >= SDL_GAMEPAD_BUTTON_COUNT) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(nativeButton));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A digital button bound to L or R has no analog travel of its own. On real
|
||||||
|
// hardware the click only engages at full depression, so report a full pull.
|
||||||
|
void FillTriggersHeldByButtons(PADStatus* statuses) {
|
||||||
|
if (InputBindings::InputBlocked()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
|
||||||
|
if (statuses[port].err != PAD_ERR_NONE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const s32 index = PADGetIndexForPort(port);
|
||||||
|
if (index < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SDL_Gamepad* gamepad = PADGetSDLGamepadForIndex(static_cast<u32>(index));
|
||||||
|
if (gamepad == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto scan = [&](PADButtonMapping* mappings, u32 count) {
|
||||||
|
if (mappings == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (u32 i = 0; i < count; ++i) {
|
||||||
|
const PADButtonMapping& mapping = mappings[i];
|
||||||
|
if (mapping.padButton != PAD_TRIGGER_L && mapping.padButton != PAD_TRIGGER_R) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!NativeButtonHeld(gamepad, mapping.nativeButton)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (mapping.padButton == PAD_TRIGGER_L) {
|
||||||
|
statuses[port].triggerLeft = 255;
|
||||||
|
} else {
|
||||||
|
statuses[port].triggerRight = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
u32 count = 0;
|
||||||
|
scan(PADGetButtonMappings(port, &count), count);
|
||||||
|
count = 0;
|
||||||
|
scan(PADGetAltButtonMappings(port, &count), count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void WritePadStatus(uint32_t base, const PADStatus& status) {
|
void WritePadStatus(uint32_t base, const PADStatus& status) {
|
||||||
const auto guestStatus = PadStatusContract::Encode({
|
const auto guestStatus = PadStatusContract::Encode({
|
||||||
status.button,
|
status.button,
|
||||||
@@ -35,6 +89,11 @@ void WritePadStatus(uint32_t base, const PADStatus& status) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" void PAD_HLE_SetRumbleEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
g_rumbleEnabled.store(enabled, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" uint32_t PAD__Init_HLE()
|
extern "C" uint32_t PAD__Init_HLE()
|
||||||
{
|
{
|
||||||
return PADInit() ? 1u : 0u;
|
return PADInit() ? 1u : 0u;
|
||||||
@@ -57,15 +116,8 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
|
|||||||
// between "connected" and "no controller" every time the overlay toggles.
|
// between "connected" and "no controller" every time the overlay toggles.
|
||||||
WiiRemoteInput::HideRemotesFromPad(statuses, PAD_CHANMAX);
|
WiiRemoteInput::HideRemotesFromPad(statuses, PAD_CHANMAX);
|
||||||
|
|
||||||
// On-screen controls drive port 0 unless a physical pad is connected.
|
FillTriggersHeldByButtons(statuses);
|
||||||
// TouchPad::Read makes that call itself; the err field is no proxy for it,
|
InputBindings::Apply(statuses);
|
||||||
// since keyboard bindings report PAD_ERR_NONE with no controller attached.
|
|
||||||
std::array<PADStatus, PAD_CHANMAX> touchStatuses{};
|
|
||||||
#ifdef MKW_PLATFORM_IOS
|
|
||||||
if (!PADIsInputBlocked() && TouchPad::Read(touchStatuses)) {
|
|
||||||
statuses[0] = touchStatuses[0];
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (uint32_t i = 0; i < PAD_CHANMAX; ++i) {
|
for (uint32_t i = 0; i < PAD_CHANMAX; ++i) {
|
||||||
@@ -94,6 +146,9 @@ PPC_NATIVE_OVERRIDE(801AF1E4, PAD__Recalibrate_HLE, uint32_t, (uint32_t mask), (
|
|||||||
|
|
||||||
extern "C" void PAD__ControlMotor_HLE(int32_t chan, uint32_t command)
|
extern "C" void PAD__ControlMotor_HLE(int32_t chan, uint32_t command)
|
||||||
{
|
{
|
||||||
|
if (command == PAD_MOTOR_RUMBLE && !g_rumbleEnabled.load(std::memory_order_relaxed)) {
|
||||||
|
command = PAD_MOTOR_STOP;
|
||||||
|
}
|
||||||
PADControlMotor(chan, command);
|
PADControlMotor(chan, command);
|
||||||
}
|
}
|
||||||
PPC_NATIVE_OVERRIDE_VOID(801AF908, PAD__ControlMotor_HLE, (int32_t chan, uint32_t command), (chan, command));
|
PPC_NATIVE_OVERRIDE_VOID(801AF908, PAD__ControlMotor_HLE, (int32_t chan, uint32_t command), (chan, command));
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
#include "touch_art.h"
|
|
||||||
|
|
||||||
#include <aurora/imgui.h>
|
|
||||||
#include <png.h>
|
|
||||||
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstring>
|
|
||||||
#include <iostream>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "runtime_config.h"
|
|
||||||
|
|
||||||
// Icons are CC BY 3.0 by Zacksly; see runtime/assets/touch/ATTRIBUTION.txt.
|
|
||||||
// Textures are owned by aurora once handed over and live for the process.
|
|
||||||
namespace TouchArt {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
std::unordered_map<std::string, ImTextureID> g_cache;
|
|
||||||
|
|
||||||
// libpng's error path longjmps, so everything it can touch must be released
|
|
||||||
// through this struct rather than by falling off the end of the function.
|
|
||||||
struct PngReader {
|
|
||||||
std::FILE* file = nullptr;
|
|
||||||
png_structp png = nullptr;
|
|
||||||
png_infop info = nullptr;
|
|
||||||
std::vector<png_bytep> rows;
|
|
||||||
~PngReader() {
|
|
||||||
if (png != nullptr) png_destroy_read_struct(&png, info != nullptr ? &info : nullptr, nullptr);
|
|
||||||
if (file != nullptr) std::fclose(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
bool DecodeRgba8(const std::filesystem::path& path, std::vector<uint8_t>& out, uint32_t& width,
|
|
||||||
uint32_t& height) {
|
|
||||||
PngReader r;
|
|
||||||
r.file = std::fopen(path.string().c_str(), "rb");
|
|
||||||
if (r.file == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
r.png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
|
||||||
if (r.png == nullptr) return false;
|
|
||||||
r.info = png_create_info_struct(r.png);
|
|
||||||
if (r.info == nullptr) return false;
|
|
||||||
if (setjmp(png_jmpbuf(r.png))) return false;
|
|
||||||
|
|
||||||
png_init_io(r.png, r.file);
|
|
||||||
png_read_info(r.png, r.info);
|
|
||||||
|
|
||||||
png_set_expand(r.png);
|
|
||||||
png_set_strip_16(r.png);
|
|
||||||
png_set_gray_to_rgb(r.png);
|
|
||||||
png_set_filler(r.png, 0xFF, PNG_FILLER_AFTER);
|
|
||||||
png_read_update_info(r.png, r.info);
|
|
||||||
|
|
||||||
width = png_get_image_width(r.png, r.info);
|
|
||||||
height = png_get_image_height(r.png, r.info);
|
|
||||||
if (width == 0 || height == 0) return false;
|
|
||||||
|
|
||||||
out.assign(static_cast<size_t>(width) * height * 4u, 0);
|
|
||||||
r.rows.resize(height);
|
|
||||||
for (uint32_t y = 0; y < height; ++y) {
|
|
||||||
r.rows[y] = out.data() + static_cast<size_t>(y) * width * 4u;
|
|
||||||
}
|
|
||||||
png_read_image(r.png, r.rows.data());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
ImTextureID Get(const char* name) {
|
|
||||||
if (name == nullptr || *name == '\0') {
|
|
||||||
return ImTextureID{};
|
|
||||||
}
|
|
||||||
if (const auto it = g_cache.find(name); it != g_cache.end()) {
|
|
||||||
return it->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImTextureID id{};
|
|
||||||
if (const auto dir = RuntimeConfigFile::ExecutableDirectory()) {
|
|
||||||
const std::filesystem::path path = *dir / "touch" / (std::string(name) + ".png");
|
|
||||||
std::vector<uint8_t> rgba;
|
|
||||||
uint32_t w = 0;
|
|
||||||
uint32_t h = 0;
|
|
||||||
if (DecodeRgba8(path, rgba, w, h)) {
|
|
||||||
id = aurora_imgui_add_texture(w, h, rgba.data());
|
|
||||||
} else {
|
|
||||||
// Cached null so this reports once rather than every frame.
|
|
||||||
std::cout << "[touch] missing button art: " << path << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
g_cache.emplace(name, id);
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Preload() {
|
|
||||||
// Must match the names used in touch_pad.cpp; a miss costs a hitch, not a
|
|
||||||
// failure.
|
|
||||||
static constexpr const char* kNames[] = {
|
|
||||||
"a", "b", "right_bumper", "l_analog", "r_analog", "start_pause", "d-pad", "control_stick",
|
|
||||||
};
|
|
||||||
for (const char* name : kNames) {
|
|
||||||
Get(name);
|
|
||||||
Get((std::string(name) + "_fill").c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace TouchArt
|
|
||||||
@@ -1,408 +0,0 @@
|
|||||||
#include "touch_pad.h"
|
|
||||||
#include "touch_art.h"
|
|
||||||
|
|
||||||
#include "hle/controller_status_contract.h"
|
|
||||||
#include "settings_overlay.h"
|
|
||||||
|
|
||||||
#include <dolphin/pad.h>
|
|
||||||
#include <imgui.h>
|
|
||||||
#include <SDL3/SDL_gamepad.h>
|
|
||||||
#include <SDL3/SDL_touch.h>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <atomic>
|
|
||||||
#include <cmath>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
namespace TouchPad {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
|
|
||||||
struct Circle {
|
|
||||||
float x, y, r;
|
|
||||||
bool Contains(float px, float py, float aspect) const {
|
|
||||||
const float dx = (px - x) * aspect;
|
|
||||||
const float dy = py - y;
|
|
||||||
return (dx * dx + dy * dy) <= (r * r);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Positions are held as a distance from one screen edge in units of screen
|
|
||||||
// height. A fraction of width would land somewhere different on a 1.45:1 iPad
|
|
||||||
// and a 2.17:1 iPhone.
|
|
||||||
struct Anchor {
|
|
||||||
enum Edge { Left, Right, Centre };
|
|
||||||
Edge edge;
|
|
||||||
float dx; // distance from that edge, in units of screen height
|
|
||||||
float y; // fraction of screen height
|
|
||||||
float r; // radius, in units of screen height
|
|
||||||
|
|
||||||
Circle Resolve(float aspect) const {
|
|
||||||
const float offset = dx / aspect;
|
|
||||||
switch (edge) {
|
|
||||||
case Left: return Circle{offset, y, r};
|
|
||||||
case Right: return Circle{1.0f - offset, y, r};
|
|
||||||
case Centre: return Circle{0.5f + offset, y, r};
|
|
||||||
}
|
|
||||||
return Circle{0.5f, y, r};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr Anchor kStickA{Anchor::Left, 0.215f, 0.400f, 0.130f};
|
|
||||||
constexpr Anchor kShoulderLA{Anchor::Left, 0.130f, 0.090f, 0.070f};
|
|
||||||
constexpr Anchor kShoulderRA{Anchor::Right, 0.130f, 0.090f, 0.070f};
|
|
||||||
constexpr Anchor kButtonAA{Anchor::Right, 0.210f, 0.640f, 0.105f};
|
|
||||||
constexpr Anchor kButtonBA{Anchor::Right, 0.430f, 0.790f, 0.072f};
|
|
||||||
constexpr Anchor kButtonItemA{Anchor::Right, 0.165f, 0.310f, 0.070f};
|
|
||||||
constexpr Anchor kButtonStartA{Anchor::Centre, 0.100f, 0.070f, 0.042f};
|
|
||||||
// There is no F10 on a touch device, so this is the only way into the settings
|
|
||||||
// bar. It is drawn whenever a touch screen exists - including when a pad has
|
|
||||||
// hidden the game controls - or connecting a pad would lock the user out.
|
|
||||||
constexpr Anchor kButtonMenuA{Anchor::Centre, -0.100f, 0.070f, 0.040f};
|
|
||||||
|
|
||||||
// r is the half-extent of the whole cross, not of one arm.
|
|
||||||
constexpr Anchor kDpadA{Anchor::Left, 0.215f, 0.760f, 0.125f};
|
|
||||||
constexpr float kDpadDeadzone = 0.30f;
|
|
||||||
|
|
||||||
struct Layout {
|
|
||||||
Circle stick, shoulderL, shoulderR, a, b, item, start, menu;
|
|
||||||
Circle dpad; // the whole cross; direction is derived, not four zones
|
|
||||||
};
|
|
||||||
|
|
||||||
Layout LayoutFor(float aspect) {
|
|
||||||
Layout l{};
|
|
||||||
l.stick = kStickA.Resolve(aspect);
|
|
||||||
l.shoulderL = kShoulderLA.Resolve(aspect);
|
|
||||||
l.shoulderR = kShoulderRA.Resolve(aspect);
|
|
||||||
l.a = kButtonAA.Resolve(aspect);
|
|
||||||
l.b = kButtonBA.Resolve(aspect);
|
|
||||||
l.item = kButtonItemA.Resolve(aspect);
|
|
||||||
l.start = kButtonStartA.Resolve(aspect);
|
|
||||||
l.menu = kButtonMenuA.Resolve(aspect);
|
|
||||||
|
|
||||||
l.dpad = kDpadA.Resolve(aspect);
|
|
||||||
return l;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Frame {
|
|
||||||
bool stickHeld = false;
|
|
||||||
float stickDx = 0.0f; // -1..1 after deadzone and clamping
|
|
||||||
float stickDy = 0.0f;
|
|
||||||
bool a = false, b = false, item = false, start = false;
|
|
||||||
bool l = false, r = false;
|
|
||||||
bool up = false, down = false, left = false, right = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// A finger inside the stick circle steers; its offset from the circle centre is
|
|
||||||
// the analog deflection. Every other finger is hit-tested against the buttons,
|
|
||||||
// so any number of them can be held at once.
|
|
||||||
// Owning finger, or 0. Single-threaded: guest fibers and the event pump share
|
|
||||||
// the host main thread.
|
|
||||||
SDL_FingerID g_stickFinger = 0;
|
|
||||||
|
|
||||||
Frame SampleFingers(float aspect) {
|
|
||||||
const Layout L = LayoutFor(aspect);
|
|
||||||
Frame frame;
|
|
||||||
bool stickFingerSeen = false;
|
|
||||||
int deviceCount = 0;
|
|
||||||
SDL_TouchID* devices = SDL_GetTouchDevices(&deviceCount);
|
|
||||||
if (devices == nullptr) {
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int d = 0; d < deviceCount; ++d) {
|
|
||||||
int fingerCount = 0;
|
|
||||||
SDL_Finger** fingers = SDL_GetTouchFingers(devices[d], &fingerCount);
|
|
||||||
if (fingers == nullptr) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (int f = 0; f < fingerCount; ++f) {
|
|
||||||
const float px = fingers[f]->x;
|
|
||||||
const float py = fingers[f]->y;
|
|
||||||
|
|
||||||
// Holding the grab lets the thumb slide past the gate without
|
|
||||||
// dropping steering.
|
|
||||||
const bool ownsStick = (g_stickFinger != 0 && fingers[f]->id == g_stickFinger);
|
|
||||||
if (ownsStick || (!frame.stickHeld && g_stickFinger == 0 &&
|
|
||||||
L.stick.Contains(px, py, aspect))) {
|
|
||||||
g_stickFinger = fingers[f]->id;
|
|
||||||
stickFingerSeen = true;
|
|
||||||
frame.stickHeld = true;
|
|
||||||
const float dx = (px - L.stick.x) * aspect / L.stick.r;
|
|
||||||
const float dy = (py - L.stick.y) / L.stick.r;
|
|
||||||
const float len = std::sqrt(dx * dx + dy * dy);
|
|
||||||
// Clamp to the circle so a finger dragged outside still reads as
|
|
||||||
// full deflection rather than overshooting the guest's range.
|
|
||||||
const float scale = (len > 1.0f) ? (1.0f / len) : 1.0f;
|
|
||||||
frame.stickDx = dx * scale;
|
|
||||||
frame.stickDy = dy * scale;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (L.a.Contains(px, py, aspect)) frame.a = true;
|
|
||||||
if (L.b.Contains(px, py, aspect)) frame.b = true;
|
|
||||||
if (L.item.Contains(px, py, aspect)) frame.item = true;
|
|
||||||
if (L.start.Contains(px, py, aspect)) frame.start = true;
|
|
||||||
if (L.shoulderL.Contains(px, py, aspect)) frame.l = true;
|
|
||||||
if (L.shoulderR.Contains(px, py, aspect)) frame.r = true;
|
|
||||||
{
|
|
||||||
const float ddx = (px - L.dpad.x) * aspect / L.dpad.r;
|
|
||||||
const float ddy = (py - L.dpad.y) / L.dpad.r;
|
|
||||||
if (std::fabs(ddx) <= 1.0f && std::fabs(ddy) <= 1.0f &&
|
|
||||||
(ddx * ddx + ddy * ddy) > (kDpadDeadzone * kDpadDeadzone)) {
|
|
||||||
if (std::fabs(ddx) > std::fabs(ddy)) {
|
|
||||||
if (ddx < 0.0f) frame.left = true; else frame.right = true;
|
|
||||||
} else {
|
|
||||||
if (ddy < 0.0f) frame.up = true; else frame.down = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SDL_free(fingers);
|
|
||||||
}
|
|
||||||
SDL_free(devices);
|
|
||||||
if (!stickFingerSeen) {
|
|
||||||
g_stickFinger = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
float CurrentAspect() {
|
|
||||||
const ImGuiIO& io = ImGui::GetIO();
|
|
||||||
if (io.DisplaySize.y <= 0.0f) {
|
|
||||||
return 1.0f;
|
|
||||||
}
|
|
||||||
return io.DisplaySize.x / io.DisplaySize.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GameCube's own button colours. White-on-white is unreadable over bright track
|
|
||||||
// surfaces, so every control also gets a dark halo and a dark label shadow,
|
|
||||||
// which keeps it legible on light and dark backgrounds alike.
|
|
||||||
constexpr ImU32 kColA = IM_COL32(67, 176, 42, 255); // green
|
|
||||||
constexpr ImU32 kColB = IM_COL32(228, 0, 43, 255); // red
|
|
||||||
constexpr ImU32 kColZ = IM_COL32(104, 82, 214, 255); // purple
|
|
||||||
constexpr ImU32 kColGrey = IM_COL32(196, 198, 206, 255);
|
|
||||||
constexpr ImU32 kColStick = IM_COL32(174, 176, 184, 255);
|
|
||||||
|
|
||||||
ImU32 WithAlpha(ImU32 colour, int alpha) {
|
|
||||||
return (colour & 0x00FFFFFFu) | (static_cast<ImU32>(alpha) << IM_COL32_A_SHIFT);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DrawCircle(ImDrawList* list, const Circle& c, const ImVec2& size, const char* label,
|
|
||||||
bool pressed, ImU32 colour, const char* art) {
|
|
||||||
const ImVec2 centre{c.x * size.x, c.y * size.y};
|
|
||||||
const float radius = c.r * size.y;
|
|
||||||
const ImVec2 lo{centre.x - radius, centre.y - radius};
|
|
||||||
const ImVec2 hi{centre.x + radius, centre.y + radius};
|
|
||||||
|
|
||||||
// The solid silhouette tinted black darkens the glyph's own shape; a plain
|
|
||||||
// circle behind it would put a disc around the cross and the triggers.
|
|
||||||
if (art != nullptr) {
|
|
||||||
const ImTextureID fill = TouchArt::Get((std::string(art) + "_fill").c_str());
|
|
||||||
if (fill != ImTextureID{}) {
|
|
||||||
list->AddImage(fill, lo, hi, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f),
|
|
||||||
IM_COL32(0, 0, 0, pressed ? 165 : 120));
|
|
||||||
}
|
|
||||||
const ImTextureID line = TouchArt::Get(art);
|
|
||||||
if (line != ImTextureID{}) {
|
|
||||||
list->AddImage(line, lo, hi, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f),
|
|
||||||
IM_COL32(255, 255, 255, pressed ? 250 : 175));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vector fallback, used when the artwork is missing.
|
|
||||||
list->AddCircleFilled(centre, radius + 3.0f, IM_COL32(0, 0, 0, pressed ? 150 : 110), 48);
|
|
||||||
list->AddCircleFilled(centre, radius, WithAlpha(colour, pressed ? 235 : 130), 48);
|
|
||||||
list->AddCircle(centre, radius, WithAlpha(IM_COL32(255, 255, 255, 255), pressed ? 255 : 170),
|
|
||||||
48, 2.5f);
|
|
||||||
if (label != nullptr && *label != '\0') {
|
|
||||||
const ImVec2 text = ImGui::CalcTextSize(label);
|
|
||||||
const ImVec2 at{centre.x - text.x * 0.5f, centre.y - text.y * 0.5f};
|
|
||||||
list->AddText(ImVec2{at.x + 1.0f, at.y + 1.0f}, IM_COL32(0, 0, 0, 190), label);
|
|
||||||
list->AddText(at, IM_COL32(255, 255, 255, 240), label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
bool HasTouchScreen() {
|
|
||||||
int deviceCount = 0;
|
|
||||||
SDL_TouchID* devices = SDL_GetTouchDevices(&deviceCount);
|
|
||||||
if (devices != nullptr) {
|
|
||||||
SDL_free(devices);
|
|
||||||
}
|
|
||||||
return deviceCount > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edge-triggered: the settings bar must toggle once per tap, not once per frame
|
|
||||||
// for as long as a finger rests on the button.
|
|
||||||
void PollMenuButton() {
|
|
||||||
static bool wasDown = false;
|
|
||||||
bool down = false;
|
|
||||||
const float aspect = CurrentAspect();
|
|
||||||
const Layout L = LayoutFor(aspect);
|
|
||||||
int deviceCount = 0;
|
|
||||||
SDL_TouchID* devices = SDL_GetTouchDevices(&deviceCount);
|
|
||||||
if (devices != nullptr) {
|
|
||||||
for (int d = 0; d < deviceCount && !down; ++d) {
|
|
||||||
int fingerCount = 0;
|
|
||||||
SDL_Finger** fingers = SDL_GetTouchFingers(devices[d], &fingerCount);
|
|
||||||
if (fingers == nullptr) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (int f = 0; f < fingerCount; ++f) {
|
|
||||||
if (L.menu.Contains(fingers[f]->x, fingers[f]->y, aspect)) {
|
|
||||||
down = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SDL_free(fingers);
|
|
||||||
}
|
|
||||||
SDL_free(devices);
|
|
||||||
}
|
|
||||||
if (down && !wasDown) {
|
|
||||||
settings_overlay::ToggleTopBar();
|
|
||||||
}
|
|
||||||
wasDown = down;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tapping the FPS readout opens the settings bar. Preferred over a dedicated
|
|
||||||
// button because it reuses something already on screen.
|
|
||||||
bool PollFpsTap(const ImVec2& size) {
|
|
||||||
float minX = 0.0f, minY = 0.0f, maxX = 0.0f, maxY = 0.0f;
|
|
||||||
if (!settings_overlay::FpsOverlayBounds(&minX, &minY, &maxX, &maxY)) {
|
|
||||||
return false; // readout hidden - caller falls back to its own button
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool wasDown = false;
|
|
||||||
bool down = false;
|
|
||||||
int deviceCount = 0;
|
|
||||||
SDL_TouchID* devices = SDL_GetTouchDevices(&deviceCount);
|
|
||||||
if (devices != nullptr) {
|
|
||||||
for (int d = 0; d < deviceCount && !down; ++d) {
|
|
||||||
int fingerCount = 0;
|
|
||||||
SDL_Finger** fingers = SDL_GetTouchFingers(devices[d], &fingerCount);
|
|
||||||
if (fingers == nullptr) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (int f = 0; f < fingerCount; ++f) {
|
|
||||||
const float px = fingers[f]->x * size.x;
|
|
||||||
const float py = fingers[f]->y * size.y;
|
|
||||||
if (px >= minX && px <= maxX && py >= minY && py <= maxY) {
|
|
||||||
down = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SDL_free(fingers);
|
|
||||||
}
|
|
||||||
SDL_free(devices);
|
|
||||||
}
|
|
||||||
if (down && !wasDown) {
|
|
||||||
settings_overlay::ToggleTopBar();
|
|
||||||
}
|
|
||||||
wasDown = down;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsActive() {
|
|
||||||
|
|
||||||
// A connected pad wins: PAD__Read_HLE already prefers it for input, and the
|
|
||||||
// overlay should get out of the way visually too rather than sit on top of
|
|
||||||
// the game doing nothing.
|
|
||||||
int gamepadCount = 0;
|
|
||||||
SDL_JoystickID* gamepads = SDL_GetGamepads(&gamepadCount);
|
|
||||||
if (gamepads != nullptr) {
|
|
||||||
SDL_free(gamepads);
|
|
||||||
}
|
|
||||||
if (gamepadCount > 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int deviceCount = 0;
|
|
||||||
SDL_TouchID* devices = SDL_GetTouchDevices(&deviceCount);
|
|
||||||
if (devices != nullptr) {
|
|
||||||
SDL_free(devices);
|
|
||||||
}
|
|
||||||
return deviceCount > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
bool Read(std::array<PADStatus, 4>& statuses) {
|
|
||||||
if (!IsActive()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const Frame frame = SampleFingers(CurrentAspect());
|
|
||||||
|
|
||||||
PADStatus& pad = statuses[0];
|
|
||||||
pad = PADStatus{};
|
|
||||||
pad.err = PAD_ERR_NONE;
|
|
||||||
|
|
||||||
uint16_t buttons = 0;
|
|
||||||
if (frame.a) buttons |= PAD_BUTTON_A;
|
|
||||||
if (frame.b) buttons |= PAD_BUTTON_B;
|
|
||||||
if (frame.item) buttons |= PAD_TRIGGER_Z;
|
|
||||||
if (frame.start) buttons |= PAD_BUTTON_START;
|
|
||||||
if (frame.l) buttons |= PAD_TRIGGER_L;
|
|
||||||
if (frame.r) buttons |= PAD_TRIGGER_R;
|
|
||||||
if (frame.up) buttons |= PAD_BUTTON_UP;
|
|
||||||
if (frame.down) buttons |= PAD_BUTTON_DOWN;
|
|
||||||
if (frame.left) buttons |= PAD_BUTTON_LEFT;
|
|
||||||
if (frame.right) buttons |= PAD_BUTTON_RIGHT;
|
|
||||||
pad.button = buttons;
|
|
||||||
|
|
||||||
// Screen y grows downward, the guest stick grows upward.
|
|
||||||
pad.stickX = static_cast<int8_t>(std::clamp(frame.stickDx * 127.0f, -127.0f, 127.0f));
|
|
||||||
pad.stickY = static_cast<int8_t>(std::clamp(-frame.stickDy * 127.0f, -127.0f, 127.0f));
|
|
||||||
pad.triggerLeft = frame.l ? 255 : 0;
|
|
||||||
pad.triggerRight = frame.r ? 255 : 0;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Draw() {
|
|
||||||
const ImGuiIO& io = ImGui::GetIO();
|
|
||||||
const ImVec2 size = io.DisplaySize;
|
|
||||||
if (size.x <= 0.0f || size.y <= 0.0f || !HasTouchScreen()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const Layout L = LayoutFor(size.x / size.y);
|
|
||||||
|
|
||||||
// The way into settings exists whenever there is a screen to tap, even when
|
|
||||||
// a pad has hidden everything else.
|
|
||||||
ImDrawList* menuList = ImGui::GetBackgroundDrawList();
|
|
||||||
if (!PollFpsTap(size)) {
|
|
||||||
// No FPS readout to tap, so fall back to an explicit button - otherwise a
|
|
||||||
// touch host with show_fps off has no way into the settings bar at all.
|
|
||||||
PollMenuButton();
|
|
||||||
DrawCircle(menuList, L.menu, size, settings_overlay::TopBarVisible() ? "X" : "=",
|
|
||||||
settings_overlay::TopBarVisible(), kColGrey, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsActive()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const Frame frame = SampleFingers(size.x / size.y);
|
|
||||||
ImDrawList* list = ImGui::GetBackgroundDrawList();
|
|
||||||
|
|
||||||
// Stick: outer ring plus a thumb dot showing current deflection.
|
|
||||||
// The glyph is the knob; nothing is drawn behind it.
|
|
||||||
const ImVec2 stickCentre{L.stick.x * size.x, L.stick.y * size.y};
|
|
||||||
const float stickRadius = L.stick.r * size.y;
|
|
||||||
const float knobRadius = stickRadius * 0.80f;
|
|
||||||
const float travel = stickRadius * 0.38f;
|
|
||||||
const Circle knob{(stickCentre.x + frame.stickDx * travel) / size.x,
|
|
||||||
(stickCentre.y + frame.stickDy * travel) / size.y,
|
|
||||||
knobRadius / size.y};
|
|
||||||
DrawCircle(list, knob, size, "", frame.stickHeld, kColStick, "control_stick");
|
|
||||||
|
|
||||||
DrawCircle(list, L.a, size, "A", frame.a, kColA, "a");
|
|
||||||
DrawCircle(list, L.b, size, "B", frame.b, kColB, "b");
|
|
||||||
DrawCircle(list, L.item, size, "Z", frame.item, kColZ, "right_bumper");
|
|
||||||
DrawCircle(list, L.start, size, "START", frame.start, kColGrey, "start_pause");
|
|
||||||
DrawCircle(list, L.shoulderL, size, "L", frame.l, kColGrey, "l_analog");
|
|
||||||
DrawCircle(list, L.shoulderR, size, "R", frame.r, kColGrey, "r_analog");
|
|
||||||
const bool dpadHeld = frame.up || frame.down || frame.left || frame.right;
|
|
||||||
DrawCircle(list, L.dpad, size, "", dpadHeld, kColGrey, "d-pad");
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace TouchPad
|
|
||||||
@@ -499,7 +499,7 @@ int32_t HandleIpTopIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const s
|
|||||||
// Nonblocking sockets get -SO_EAGAIN immediately (Dolphin's retry predicate
|
// Nonblocking sockets get -SO_EAGAIN immediately (Dolphin's retry predicate
|
||||||
// short-circuits on nonBlock/forceNonBlock, IOS/Network/Socket.cpp:715-718);
|
// short-circuits on nonBlock/forceNonBlock, IOS/Network/Socket.cpp:715-718);
|
||||||
// waiting here anyway stalled the whole emulation thread on every empty read.
|
// waiting here anyway stalled the whole emulation thread on every empty read.
|
||||||
constexpr int kStreamRecvWaitMs = 250;
|
constexpr int kStreamRecvWaitMs = 1000;
|
||||||
const int streamWaitMs = (forceNonBlock || s->nonblocking) ? 0 : kStreamRecvWaitMs;
|
const int streamWaitMs = (forceNonBlock || s->nonblocking) ? 0 : kStreamRecvWaitMs;
|
||||||
const bool waited = ret < 0 && !fromPtr && s->type == SOCK_STREAM &&
|
const bool waited = ret < 0 && !fromPtr && s->type == SOCK_STREAM &&
|
||||||
IsWouldBlockError(nativeErr) && WaitForReadable(s->native, streamWaitMs);
|
IsWouldBlockError(nativeErr) && WaitForReadable(s->native, streamWaitMs);
|
||||||
|
|||||||
@@ -12,7 +12,25 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr uint32_t kPalProductRegion = 2;
|
// Use the SDK's own value tables, including its unknown-region result.
|
||||||
|
uint32_t LookupProductRegion(uint32_t table, uint32_t stride, uint32_t count,
|
||||||
|
const std::string& value) {
|
||||||
|
for (uint32_t index = 0; index < count; ++index) {
|
||||||
|
const uint32_t entry = table + index * stride;
|
||||||
|
if (!Memory::Contains(entry, stride)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const auto* bytes = static_cast<const uint8_t*>(Memory::GetPointer(entry, stride));
|
||||||
|
if (bytes[0] == 0xFF) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (value.size() < stride - 1 &&
|
||||||
|
std::memcmp(bytes + 1, value.c_str(), value.size() + 1) == 0) {
|
||||||
|
return bytes[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0xFFFFFFFFu;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -50,16 +68,12 @@ extern "C" uint32_t SCGetEuRgb60Mode_HLE()
|
|||||||
|
|
||||||
PPC_NATIVE_OVERRIDE(801B1CAC, SCGetEuRgb60Mode_HLE, uint32_t, (), ());
|
PPC_NATIVE_OVERRIDE(801B1CAC, SCGetEuRgb60Mode_HLE, uint32_t, (), ());
|
||||||
|
|
||||||
// The managed NAND intentionally starts without a console-owned setting.txt.
|
// Expose the selected emulated NAND identity through the SDK SC APIs.
|
||||||
// DWC nevertheless requires the Wii product code and serial number so it can
|
|
||||||
// include csnum in NAS authentication. Expose one stable virtual-console
|
|
||||||
// identity without requiring or mutating a user's real NAND.
|
|
||||||
|
|
||||||
extern "C" uint32_t SCGetProductArea_HLE()
|
extern "C" uint32_t SCGetProductArea_HLE()
|
||||||
{
|
{
|
||||||
// The PAL setting.txt AREA value is "EUR". The SDK's lookup table at
|
return LookupProductRegion(0x8029CEB0u, 5, 13,
|
||||||
// 0x8029CEB0 maps JPN=0, USA=1, EUR=2.
|
RuntimeConsoleIdentity::Current().area);
|
||||||
return kPalProductRegion;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PPC_NATIVE_OVERRIDE(801B23A0, SCGetProductArea_HLE, uint32_t, (), ());
|
PPC_NATIVE_OVERRIDE(801B23A0, SCGetProductArea_HLE, uint32_t, (), ());
|
||||||
@@ -68,12 +82,13 @@ extern "C" uint32_t SCGetProductCode_HLE()
|
|||||||
{
|
{
|
||||||
// Original PAL SC storage for the six-byte CODE value.
|
// Original PAL SC storage for the six-byte CODE value.
|
||||||
constexpr uint32_t kProductCodeAddress = 0x803869E0u;
|
constexpr uint32_t kProductCodeAddress = 0x803869E0u;
|
||||||
static constexpr char kProductCode[] = "LEH";
|
const std::string& productCode = RuntimeConsoleIdentity::Current().productCode;
|
||||||
if (!Memory::Contains(kProductCodeAddress, sizeof(kProductCode))) {
|
const size_t size = productCode.size() + 1;
|
||||||
|
if (!Memory::Contains(kProductCodeAddress, size)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
std::memcpy(Memory::GetPointer(kProductCodeAddress, sizeof(kProductCode)),
|
std::memcpy(Memory::GetPointer(kProductCodeAddress, size),
|
||||||
kProductCode, sizeof(kProductCode));
|
productCode.c_str(), size);
|
||||||
return kProductCodeAddress;
|
return kProductCodeAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,9 +109,8 @@ PPC_NATIVE_OVERRIDE(801B2460, SCGetProductSN_HLE, uint32_t, (uint32_t serialAddr
|
|||||||
|
|
||||||
extern "C" uint32_t SCGetProductGameRegion_HLE()
|
extern "C" uint32_t SCGetProductGameRegion_HLE()
|
||||||
{
|
{
|
||||||
// The PAL setting.txt GAME value is "EU". The SDK's own lookup table at
|
return LookupProductRegion(0x8029CEF8u, 4, 4,
|
||||||
// 0x8029CEF8 maps JP=0, US=1, EU=2.
|
RuntimeConsoleIdentity::Current().gameRegion);
|
||||||
return kPalProductRegion;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PPC_NATIVE_OVERRIDE(801B24C8, SCGetProductGameRegion_HLE, uint32_t, (), ());
|
PPC_NATIVE_OVERRIDE(801B24C8, SCGetProductGameRegion_HLE, uint32_t, (), ());
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
#include "input_bindings.h"
|
||||||
|
|
||||||
|
#include "controller_button_names.h"
|
||||||
|
#include "input_expr.h"
|
||||||
|
#include "runtime_config.h"
|
||||||
|
#include "runtime_log.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <mutex>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <SDL3/SDL_gamepad.h>
|
||||||
|
|
||||||
|
namespace InputBindings {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct Binding {
|
||||||
|
std::string text;
|
||||||
|
InputExpr::Expression expr;
|
||||||
|
bool active = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::mutex g_mutex;
|
||||||
|
std::array<std::array<Binding, kControls.size()>, PAD_CHANMAX> g_bindings;
|
||||||
|
bool g_anyBound = false;
|
||||||
|
bool g_inputBlocked = false;
|
||||||
|
|
||||||
|
// Dolphin input names, mapped onto SDL. XInput-style names are exact; DInput
|
||||||
|
// "Button <n>" indices follow the common PlayStation layout, which is what
|
||||||
|
// DInput reports for a DualShock/DualSense. Other pads may number differently.
|
||||||
|
const std::unordered_map<std::string, SDL_GamepadButton>& ButtonNames() {
|
||||||
|
static const std::unordered_map<std::string, SDL_GamepadButton> table = {
|
||||||
|
{"Button A", SDL_GAMEPAD_BUTTON_SOUTH}, {"Button B", SDL_GAMEPAD_BUTTON_EAST},
|
||||||
|
{"Button X", SDL_GAMEPAD_BUTTON_WEST}, {"Button Y", SDL_GAMEPAD_BUTTON_NORTH},
|
||||||
|
{"Shoulder L", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
||||||
|
{"Shoulder R", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
||||||
|
{"Thumb L", SDL_GAMEPAD_BUTTON_LEFT_STICK}, {"Thumb R", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
|
||||||
|
{"Start", SDL_GAMEPAD_BUTTON_START}, {"Back", SDL_GAMEPAD_BUTTON_BACK},
|
||||||
|
{"Guide", SDL_GAMEPAD_BUTTON_GUIDE},
|
||||||
|
{"Pad N", SDL_GAMEPAD_BUTTON_DPAD_UP}, {"Pad S", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
||||||
|
{"Pad W", SDL_GAMEPAD_BUTTON_DPAD_LEFT}, {"Pad E", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||||
|
{"Hat 0 N", SDL_GAMEPAD_BUTTON_DPAD_UP}, {"Hat 0 S", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
||||||
|
{"Hat 0 W", SDL_GAMEPAD_BUTTON_DPAD_LEFT}, {"Hat 0 E", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||||
|
{"Button 0", SDL_GAMEPAD_BUTTON_WEST}, {"Button 1", SDL_GAMEPAD_BUTTON_SOUTH},
|
||||||
|
{"Button 2", SDL_GAMEPAD_BUTTON_EAST}, {"Button 3", SDL_GAMEPAD_BUTTON_NORTH},
|
||||||
|
{"Button 4", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
||||||
|
{"Button 5", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
||||||
|
{"Button 8", SDL_GAMEPAD_BUTTON_BACK}, {"Button 9", SDL_GAMEPAD_BUTTON_START},
|
||||||
|
{"Button 10", SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
||||||
|
{"Button 11", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
|
||||||
|
{"Button 12", SDL_GAMEPAD_BUTTON_GUIDE}, {"Button 13", SDL_GAMEPAD_BUTTON_TOUCHPAD},
|
||||||
|
};
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signed axis names: SDL axis plus the direction that counts as positive.
|
||||||
|
struct AxisRef {
|
||||||
|
SDL_GamepadAxis axis;
|
||||||
|
int sign;
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::unordered_map<std::string, AxisRef>& AxisNames() {
|
||||||
|
static const std::unordered_map<std::string, AxisRef> table = {
|
||||||
|
{"Axis X-", {SDL_GAMEPAD_AXIS_LEFTX, -1}}, {"Axis X+", {SDL_GAMEPAD_AXIS_LEFTX, 1}},
|
||||||
|
{"Axis Y-", {SDL_GAMEPAD_AXIS_LEFTY, -1}}, {"Axis Y+", {SDL_GAMEPAD_AXIS_LEFTY, 1}},
|
||||||
|
{"Axis Z-", {SDL_GAMEPAD_AXIS_RIGHTX, -1}}, {"Axis Z+", {SDL_GAMEPAD_AXIS_RIGHTX, 1}},
|
||||||
|
{"Axis Zr-", {SDL_GAMEPAD_AXIS_RIGHTY, -1}},{"Axis Zr+", {SDL_GAMEPAD_AXIS_RIGHTY, 1}},
|
||||||
|
{"Left X-", {SDL_GAMEPAD_AXIS_LEFTX, -1}}, {"Left X+", {SDL_GAMEPAD_AXIS_LEFTX, 1}},
|
||||||
|
{"Left Y-", {SDL_GAMEPAD_AXIS_LEFTY, 1}}, {"Left Y+", {SDL_GAMEPAD_AXIS_LEFTY, -1}},
|
||||||
|
{"Right X-", {SDL_GAMEPAD_AXIS_RIGHTX, -1}},{"Right X+", {SDL_GAMEPAD_AXIS_RIGHTX, 1}},
|
||||||
|
{"Right Y-", {SDL_GAMEPAD_AXIS_RIGHTY, 1}}, {"Right Y+", {SDL_GAMEPAD_AXIS_RIGHTY, -1}},
|
||||||
|
{"Full Axis Xr+", {SDL_GAMEPAD_AXIS_LEFT_TRIGGER, 1}},
|
||||||
|
{"Full Axis Yr+", {SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, 1}},
|
||||||
|
{"Trigger L", {SDL_GAMEPAD_AXIS_LEFT_TRIGGER, 1}},
|
||||||
|
{"Trigger R", {SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, 1}},
|
||||||
|
};
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
double ReadInput(SDL_Gamepad* gamepad, const std::string& name) {
|
||||||
|
if (gamepad == nullptr) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
if (const auto it = ButtonNames().find(name); it != ButtonNames().end()) {
|
||||||
|
return SDL_GetGamepadButton(gamepad, it->second) ? 1.0 : 0.0;
|
||||||
|
}
|
||||||
|
if (const auto it = AxisNames().find(name); it != AxisNames().end()) {
|
||||||
|
const double raw = SDL_GetGamepadAxis(gamepad, it->second.axis) / 32767.0;
|
||||||
|
return std::clamp(raw * it->second.sign, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
// Fall back to this project's own positional names, so a binding written
|
||||||
|
// here does not have to use Dolphin vocabulary.
|
||||||
|
if (const auto* native = ControllerNames::FindNativeButton(name)) {
|
||||||
|
if (native->nativeButton != PAD_NATIVE_BUTTON_INVALID) {
|
||||||
|
return SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(native->nativeButton)) ? 1.0
|
||||||
|
: 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Gamepad* GamepadForPort(uint32_t port) {
|
||||||
|
const s32 index = PADGetIndexForPort(port);
|
||||||
|
return index < 0 ? nullptr : PADGetSDLGamepadForIndex(static_cast<u32>(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t ControlIndexForDolphinName(const std::string& name) {
|
||||||
|
for (size_t i = 0; i < kControls.size(); ++i) {
|
||||||
|
if (name == kControls[i].dolphinName) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return kControls.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RecomputeAnyBoundLocked() {
|
||||||
|
g_anyBound = false;
|
||||||
|
for (const auto& port : g_bindings) {
|
||||||
|
for (const auto& binding : port) {
|
||||||
|
if (!binding.expr.Empty()) {
|
||||||
|
g_anyBound = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ConfigKey(uint32_t port, size_t control) {
|
||||||
|
std::string key = "expr_" + std::to_string(port + 1) + "_";
|
||||||
|
for (const char* c = kControls[control].dolphinName; *c != '\0'; ++c) {
|
||||||
|
key += (*c == '/' || *c == '-') ? '_' : static_cast<char>(std::tolower(*c));
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void SetInputBlocked(bool blocked) noexcept {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
g_inputBlocked = blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputBlocked() noexcept {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
return g_inputBlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Reload() noexcept {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
|
||||||
|
for (size_t control = 0; control < kControls.size(); ++control) {
|
||||||
|
Binding& binding = g_bindings[port][control];
|
||||||
|
binding = Binding{};
|
||||||
|
binding.text = RuntimeConfigFile::ControllerExpression(ConfigKey(port, control));
|
||||||
|
std::string error;
|
||||||
|
if (!binding.text.empty() &&
|
||||||
|
!InputExpr::Expression::Parse(binding.text, binding.expr, error)) {
|
||||||
|
RT_LOG(RT_TAG_CONFIG) << "expression for port " << (port + 1) << " "
|
||||||
|
<< kControls[control].dolphinName << ": " << error << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RecomputeAnyBoundLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Apply(PADStatus* statuses) noexcept {
|
||||||
|
if (statuses == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
if (!g_anyBound) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bool blocked = g_inputBlocked;
|
||||||
|
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
|
||||||
|
if (statuses[port].err != PAD_ERR_NONE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SDL_Gamepad* gamepad = GamepadForPort(port);
|
||||||
|
const InputExpr::InputSource source = [gamepad](const std::string& name) {
|
||||||
|
return ReadInput(gamepad, name);
|
||||||
|
};
|
||||||
|
for (size_t control = 0; control < kControls.size(); ++control) {
|
||||||
|
Binding& binding = g_bindings[port][control];
|
||||||
|
if (binding.expr.Empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (blocked) {
|
||||||
|
binding.active = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const double value = binding.expr.Evaluate(source);
|
||||||
|
binding.active = value > InputExpr::kConditionThreshold;
|
||||||
|
const ControlInfo& info = kControls[control];
|
||||||
|
if (info.padButton != 0 && binding.active) {
|
||||||
|
statuses[port].button |= info.padButton;
|
||||||
|
}
|
||||||
|
if (info.analog != 0) {
|
||||||
|
const double safe = std::isfinite(value) ? std::clamp(value, 0.0, 1.0) : 0.0;
|
||||||
|
const auto scaled = static_cast<uint8_t>(safe * 255.0);
|
||||||
|
uint8_t& target =
|
||||||
|
info.analog == 1 ? statuses[port].triggerLeft : statuses[port].triggerRight;
|
||||||
|
target = std::max(target, scaled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetExpression(uint32_t port, size_t control) noexcept {
|
||||||
|
if (port >= PAD_CHANMAX || control >= kControls.size()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
return g_bindings[port][control].text;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SetExpression(uint32_t port, size_t control, const std::string& text, std::string& error) noexcept {
|
||||||
|
if (port >= PAD_CHANMAX || control >= kControls.size()) {
|
||||||
|
error = "invalid control";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
InputExpr::Expression parsed;
|
||||||
|
if (!InputExpr::Expression::Parse(text, parsed, error)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
Binding& binding = g_bindings[port][control];
|
||||||
|
binding.text = text;
|
||||||
|
binding.expr = std::move(parsed);
|
||||||
|
binding.active = false;
|
||||||
|
RecomputeAnyBoundLocked();
|
||||||
|
}
|
||||||
|
RuntimeConfigFile::SetControllerExpression(ConfigKey(port, control), text);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsActive(uint32_t port, size_t control) noexcept {
|
||||||
|
if (port >= PAD_CHANMAX || control >= kControls.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
return g_bindings[port][control].active;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string DefaultDolphinConfigPath() noexcept {
|
||||||
|
std::error_code ec;
|
||||||
|
if (const char* appdata = std::getenv("APPDATA"); appdata != nullptr) {
|
||||||
|
const std::filesystem::path roaming =
|
||||||
|
std::filesystem::path(appdata) / "Dolphin Emulator" / "Config" / "GCPadNew.ini";
|
||||||
|
if (std::filesystem::exists(roaming, ec)) {
|
||||||
|
return RuntimeConfigFile::PathToUtf8(roaming);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto executableDirectory = RuntimeConfigFile::ExecutableDirectory();
|
||||||
|
return RuntimeConfigFile::PathToUtf8(executableDirectory ? *executableDirectory / "GCPadNew.ini"
|
||||||
|
: std::filesystem::path("GCPadNew.ini"));
|
||||||
|
}
|
||||||
|
|
||||||
|
int ImportDolphinConfig(const std::string& path, int padIndex, uint32_t port, std::string& summary,
|
||||||
|
std::string& error) noexcept {
|
||||||
|
std::vector<std::pair<std::string, std::string>> controls;
|
||||||
|
std::string device;
|
||||||
|
if (!InputExpr::ReadDolphinConfig(RuntimeConfigFile::PathFromUtf8(path), padIndex, controls, device,
|
||||||
|
error)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int imported = 0;
|
||||||
|
std::vector<std::string> skipped;
|
||||||
|
for (const auto& [name, text] : controls) {
|
||||||
|
const size_t control = ControlIndexForDolphinName(name);
|
||||||
|
if (control == kControls.size()) {
|
||||||
|
if (name.rfind("Main Stick/", 0) == 0 || name.rfind("C-Stick/", 0) == 0) {
|
||||||
|
skipped.push_back(name);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::string parseError;
|
||||||
|
if (!SetExpression(port, control, text, parseError)) {
|
||||||
|
skipped.push_back(name);
|
||||||
|
RT_LOG(RT_TAG_CONFIG) << "import " << name << ": " << parseError << std::endl;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++imported;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary = "Imported " + std::to_string(imported) + " controls";
|
||||||
|
if (!device.empty()) {
|
||||||
|
summary += " from " + device;
|
||||||
|
}
|
||||||
|
if (!skipped.empty()) {
|
||||||
|
summary += "; skipped " + std::to_string(skipped.size()) +
|
||||||
|
" (stick axes and unsupported inputs keep their existing mapping)";
|
||||||
|
}
|
||||||
|
return imported;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace InputBindings
|
||||||
@@ -0,0 +1,631 @@
|
|||||||
|
#include "input_expr.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace InputExpr {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using Clock = std::chrono::steady_clock;
|
||||||
|
using FSec = std::chrono::duration<double>;
|
||||||
|
|
||||||
|
enum class Kind {
|
||||||
|
Literal, Input, Not, Add, Sub, Mul, Div, And, Or, Xor,
|
||||||
|
Greater, Less, Equal,
|
||||||
|
FnIf, FnMin, FnMax, FnClamp, FnAbs, FnSqrt, FnPow, FnSin, FnCos, FnTan,
|
||||||
|
FnDeadzone, FnTimer, FnToggle, FnHold, FnTap, FnPulse, FnSmooth, FnNot,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FnInfo {
|
||||||
|
Kind kind;
|
||||||
|
int minArgs;
|
||||||
|
int maxArgs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::unordered_map<std::string, FnInfo>& FunctionTable() {
|
||||||
|
static const std::unordered_map<std::string, FnInfo> table = {
|
||||||
|
{"not", {Kind::FnNot, 1, 1}}, {"if", {Kind::FnIf, 3, 3}},
|
||||||
|
{"min", {Kind::FnMin, 2, 2}}, {"max", {Kind::FnMax, 2, 2}},
|
||||||
|
{"clamp", {Kind::FnClamp, 3, 3}}, {"abs", {Kind::FnAbs, 1, 1}},
|
||||||
|
{"sqrt", {Kind::FnSqrt, 1, 1}}, {"pow", {Kind::FnPow, 2, 2}},
|
||||||
|
{"sin", {Kind::FnSin, 1, 1}}, {"cos", {Kind::FnCos, 1, 1}},
|
||||||
|
{"tan", {Kind::FnTan, 1, 1}}, {"deadzone", {Kind::FnDeadzone, 2, 2}},
|
||||||
|
{"timer", {Kind::FnTimer, 1, 1}}, {"toggle", {Kind::FnToggle, 1, 2}},
|
||||||
|
{"hold", {Kind::FnHold, 2, 2}}, {"tap", {Kind::FnTap, 2, 3}},
|
||||||
|
{"pulse", {Kind::FnPulse, 2, 2}}, {"smooth", {Kind::FnSmooth, 2, 3}},
|
||||||
|
};
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
struct Node {
|
||||||
|
Kind kind;
|
||||||
|
double literal = 0.0;
|
||||||
|
std::string input;
|
||||||
|
std::vector<std::unique_ptr<Node>> args;
|
||||||
|
|
||||||
|
// Per-instance state for the stateful functions. Mutable because Evaluate
|
||||||
|
// is logically a read of current input state.
|
||||||
|
mutable bool released = false;
|
||||||
|
mutable bool state = false;
|
||||||
|
mutable unsigned taps = 0;
|
||||||
|
mutable double value = 0.0;
|
||||||
|
mutable Clock::time_point mark = Clock::now();
|
||||||
|
mutable bool marked = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// ---- tokenizer ----------------------------------------------------------
|
||||||
|
|
||||||
|
struct Token {
|
||||||
|
enum Type { End, Input, Number, Ident, Op, LParen, RParen, Comma } type = End;
|
||||||
|
std::string text;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Lexer {
|
||||||
|
public:
|
||||||
|
explicit Lexer(const std::string& text) : m_text(text) {}
|
||||||
|
|
||||||
|
bool Next(Token& tok, std::string& error) {
|
||||||
|
while (m_pos < m_text.size() && std::isspace(static_cast<unsigned char>(m_text[m_pos]))) {
|
||||||
|
++m_pos;
|
||||||
|
}
|
||||||
|
if (m_pos >= m_text.size()) {
|
||||||
|
tok = Token{};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const char c = m_text[m_pos];
|
||||||
|
if (c == '`') {
|
||||||
|
const size_t close = m_text.find('`', m_pos + 1);
|
||||||
|
if (close == std::string::npos) {
|
||||||
|
error = "unterminated ` in expression";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
tok.type = Token::Input;
|
||||||
|
tok.text = m_text.substr(m_pos + 1, close - m_pos - 1);
|
||||||
|
m_pos = close + 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(c)) || c == '.') {
|
||||||
|
size_t end = m_pos;
|
||||||
|
while (end < m_text.size() &&
|
||||||
|
(std::isdigit(static_cast<unsigned char>(m_text[end])) || m_text[end] == '.')) {
|
||||||
|
++end;
|
||||||
|
}
|
||||||
|
tok.type = Token::Number;
|
||||||
|
tok.text = m_text.substr(m_pos, end - m_pos);
|
||||||
|
m_pos = end;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (std::isalpha(static_cast<unsigned char>(c)) || c == '_') {
|
||||||
|
size_t end = m_pos;
|
||||||
|
while (end < m_text.size() &&
|
||||||
|
(std::isalnum(static_cast<unsigned char>(m_text[end])) || m_text[end] == '_' ||
|
||||||
|
m_text[end] == ' ')) {
|
||||||
|
++end;
|
||||||
|
}
|
||||||
|
// Trailing spaces belong to the separator, not the identifier.
|
||||||
|
while (end > m_pos && m_text[end - 1] == ' ') {
|
||||||
|
--end;
|
||||||
|
}
|
||||||
|
tok.type = Token::Ident;
|
||||||
|
tok.text = m_text.substr(m_pos, end - m_pos);
|
||||||
|
m_pos = end;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (c == '(') { tok.type = Token::LParen; ++m_pos; return true; }
|
||||||
|
if (c == ')') { tok.type = Token::RParen; ++m_pos; return true; }
|
||||||
|
if (c == ',') { tok.type = Token::Comma; ++m_pos; return true; }
|
||||||
|
if (std::string("!&|^+-*/><=").find(c) != std::string::npos) {
|
||||||
|
tok.type = Token::Op;
|
||||||
|
tok.text = std::string(1, c);
|
||||||
|
++m_pos;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
error = std::string("unexpected character '") + c + "' in expression";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Position() const { return m_pos; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
const std::string& m_text;
|
||||||
|
size_t m_pos = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- parser -------------------------------------------------------------
|
||||||
|
|
||||||
|
using NodePtr = std::unique_ptr<Node>;
|
||||||
|
|
||||||
|
class Parser {
|
||||||
|
public:
|
||||||
|
explicit Parser(const std::string& text) : m_lexer(text) { Advance(); }
|
||||||
|
|
||||||
|
NodePtr ParseExpression(std::string& error) {
|
||||||
|
NodePtr node = ParseBinary(0, error);
|
||||||
|
if (!node) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (m_failed) {
|
||||||
|
error = m_lexError;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (m_tok.type != Token::End) {
|
||||||
|
error = "unexpected trailing input in expression";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Advance() {
|
||||||
|
if (!m_lexer.Next(m_tok, m_lexError)) {
|
||||||
|
m_tok = Token{};
|
||||||
|
m_failed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Precedence(const std::string& op) {
|
||||||
|
if (op == "|") return 1;
|
||||||
|
if (op == "^") return 2;
|
||||||
|
if (op == "&") return 3;
|
||||||
|
if (op == ">" || op == "<" || op == "=") return 4;
|
||||||
|
if (op == "+" || op == "-") return 5;
|
||||||
|
if (op == "*" || op == "/") return 6;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Kind BinaryKind(const std::string& op) {
|
||||||
|
if (op == "|") return Kind::Or;
|
||||||
|
if (op == "^") return Kind::Xor;
|
||||||
|
if (op == "&") return Kind::And;
|
||||||
|
if (op == ">") return Kind::Greater;
|
||||||
|
if (op == "<") return Kind::Less;
|
||||||
|
if (op == "=") return Kind::Equal;
|
||||||
|
if (op == "+") return Kind::Add;
|
||||||
|
if (op == "-") return Kind::Sub;
|
||||||
|
if (op == "*") return Kind::Mul;
|
||||||
|
return Kind::Div;
|
||||||
|
}
|
||||||
|
|
||||||
|
NodePtr ParseBinary(int minPrec, std::string& error) {
|
||||||
|
NodePtr lhs = ParseUnary(error);
|
||||||
|
if (!lhs) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
while (m_tok.type == Token::Op) {
|
||||||
|
const int prec = Precedence(m_tok.text);
|
||||||
|
if (prec < 0 || prec < minPrec) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const std::string op = m_tok.text;
|
||||||
|
Advance();
|
||||||
|
NodePtr rhs = ParseBinary(prec + 1, error);
|
||||||
|
if (!rhs) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto node = std::make_unique<Node>();
|
||||||
|
node->kind = BinaryKind(op);
|
||||||
|
node->args.push_back(std::move(lhs));
|
||||||
|
node->args.push_back(std::move(rhs));
|
||||||
|
lhs = std::move(node);
|
||||||
|
}
|
||||||
|
return lhs;
|
||||||
|
}
|
||||||
|
|
||||||
|
NodePtr ParseUnary(std::string& error) {
|
||||||
|
if (m_failed) {
|
||||||
|
error = m_lexError;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (m_tok.type == Token::Op && (m_tok.text == "!" || m_tok.text == "-" || m_tok.text == "+")) {
|
||||||
|
const std::string op = m_tok.text;
|
||||||
|
Advance();
|
||||||
|
NodePtr inner = ParseUnary(error);
|
||||||
|
if (!inner) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (op == "+") {
|
||||||
|
return inner;
|
||||||
|
}
|
||||||
|
auto node = std::make_unique<Node>();
|
||||||
|
if (op == "!") {
|
||||||
|
node->kind = Kind::Not;
|
||||||
|
node->args.push_back(std::move(inner));
|
||||||
|
} else {
|
||||||
|
node->kind = Kind::Sub;
|
||||||
|
auto zero = std::make_unique<Node>();
|
||||||
|
zero->kind = Kind::Literal;
|
||||||
|
node->args.push_back(std::move(zero));
|
||||||
|
node->args.push_back(std::move(inner));
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
return ParsePrimary(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
NodePtr ParsePrimary(std::string& error) {
|
||||||
|
if (m_failed) {
|
||||||
|
error = m_lexError;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
switch (m_tok.type) {
|
||||||
|
case Token::Input: {
|
||||||
|
auto node = std::make_unique<Node>();
|
||||||
|
node->kind = Kind::Input;
|
||||||
|
node->input = m_tok.text;
|
||||||
|
Advance();
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
case Token::Number: {
|
||||||
|
auto node = std::make_unique<Node>();
|
||||||
|
node->kind = Kind::Literal;
|
||||||
|
node->literal = std::strtod(m_tok.text.c_str(), nullptr);
|
||||||
|
Advance();
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
case Token::LParen: {
|
||||||
|
Advance();
|
||||||
|
NodePtr inner = ParseBinary(0, error);
|
||||||
|
if (!inner) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (m_tok.type != Token::RParen) {
|
||||||
|
error = "expected closing paren";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
Advance();
|
||||||
|
return inner;
|
||||||
|
}
|
||||||
|
case Token::Ident: {
|
||||||
|
const std::string name = m_tok.text;
|
||||||
|
Advance();
|
||||||
|
if (m_tok.type != Token::LParen) {
|
||||||
|
// A bare identifier is an input name, as Dolphin allows for
|
||||||
|
// simple cases such as "Start" or "LSHIFT".
|
||||||
|
auto node = std::make_unique<Node>();
|
||||||
|
node->kind = Kind::Input;
|
||||||
|
node->input = name;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
const auto it = FunctionTable().find(name);
|
||||||
|
if (it == FunctionTable().end()) {
|
||||||
|
error = "unknown function '" + name + "'";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
Advance();
|
||||||
|
auto node = std::make_unique<Node>();
|
||||||
|
node->kind = it->second.kind;
|
||||||
|
if (m_tok.type != Token::RParen) {
|
||||||
|
while (true) {
|
||||||
|
NodePtr arg = ParseBinary(0, error);
|
||||||
|
if (!arg) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
node->args.push_back(std::move(arg));
|
||||||
|
if (m_tok.type != Token::Comma) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_tok.type != Token::RParen) {
|
||||||
|
error = "expected closing paren after " + name + " arguments";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
Advance();
|
||||||
|
const int count = static_cast<int>(node->args.size());
|
||||||
|
if (count < it->second.minArgs || count > it->second.maxArgs) {
|
||||||
|
error = name + " takes " + std::to_string(it->second.minArgs) + " to " +
|
||||||
|
std::to_string(it->second.maxArgs) + " arguments";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
error = "expected start of expression";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Lexer m_lexer;
|
||||||
|
Token m_tok;
|
||||||
|
std::string m_lexError;
|
||||||
|
bool m_failed = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- evaluator ----------------------------------------------------------
|
||||||
|
|
||||||
|
double Eval(const Node& node, const InputSource& source);
|
||||||
|
|
||||||
|
double Arg(const Node& node, size_t index, const InputSource& source) {
|
||||||
|
return Eval(*node.args[index], source);
|
||||||
|
}
|
||||||
|
|
||||||
|
double Eval(const Node& node, const InputSource& source) {
|
||||||
|
switch (node.kind) {
|
||||||
|
case Kind::Literal: return node.literal;
|
||||||
|
case Kind::Input: return source ? source(node.input) : 0.0;
|
||||||
|
case Kind::Not:
|
||||||
|
case Kind::FnNot: return 1.0 - Arg(node, 0, source);
|
||||||
|
case Kind::Add: return Arg(node, 0, source) + Arg(node, 1, source);
|
||||||
|
case Kind::Sub: return Arg(node, 0, source) - Arg(node, 1, source);
|
||||||
|
case Kind::Mul: return Arg(node, 0, source) * Arg(node, 1, source);
|
||||||
|
case Kind::Div: {
|
||||||
|
// Both sides are evaluated even when the divisor is zero: the left
|
||||||
|
// subtree may hold stateful functions that need their frame update.
|
||||||
|
const double lhs = Arg(node, 0, source);
|
||||||
|
const double rhs = Arg(node, 1, source);
|
||||||
|
return rhs == 0.0 ? 0.0 : lhs / rhs;
|
||||||
|
}
|
||||||
|
case Kind::And: return std::min(Arg(node, 0, source), Arg(node, 1, source));
|
||||||
|
case Kind::Or: return std::max(Arg(node, 0, source), Arg(node, 1, source));
|
||||||
|
case Kind::Xor: {
|
||||||
|
const double a = Arg(node, 0, source);
|
||||||
|
const double b = Arg(node, 1, source);
|
||||||
|
return std::max(std::min(a, 1.0 - b), std::min(b, 1.0 - a));
|
||||||
|
}
|
||||||
|
case Kind::Greater: return Arg(node, 0, source) > Arg(node, 1, source) ? 1.0 : 0.0;
|
||||||
|
case Kind::Less: return Arg(node, 0, source) < Arg(node, 1, source) ? 1.0 : 0.0;
|
||||||
|
case Kind::Equal: return Arg(node, 0, source) == Arg(node, 1, source) ? 1.0 : 0.0;
|
||||||
|
case Kind::FnIf:
|
||||||
|
return Arg(node, 0, source) > kConditionThreshold ? Arg(node, 1, source) : Arg(node, 2, source);
|
||||||
|
case Kind::FnMin: return std::min(Arg(node, 0, source), Arg(node, 1, source));
|
||||||
|
case Kind::FnMax: return std::max(Arg(node, 0, source), Arg(node, 1, source));
|
||||||
|
case Kind::FnClamp: {
|
||||||
|
const double v = Arg(node, 0, source);
|
||||||
|
double lo = Arg(node, 1, source);
|
||||||
|
double hi = Arg(node, 2, source);
|
||||||
|
if (lo > hi) {
|
||||||
|
std::swap(lo, hi);
|
||||||
|
}
|
||||||
|
return std::clamp(v, lo, hi);
|
||||||
|
}
|
||||||
|
case Kind::FnAbs: return std::abs(Arg(node, 0, source));
|
||||||
|
case Kind::FnSqrt: return std::sqrt(Arg(node, 0, source));
|
||||||
|
case Kind::FnPow: return std::pow(Arg(node, 0, source), Arg(node, 1, source));
|
||||||
|
case Kind::FnSin: return std::sin(Arg(node, 0, source));
|
||||||
|
case Kind::FnCos: return std::cos(Arg(node, 0, source));
|
||||||
|
case Kind::FnTan: return std::tan(Arg(node, 0, source));
|
||||||
|
case Kind::FnDeadzone: {
|
||||||
|
const double v = Arg(node, 0, source);
|
||||||
|
const double dz = std::clamp(Arg(node, 1, source), 0.0, 0.999);
|
||||||
|
return std::copysign(std::max(0.0, std::abs(v) - dz) / (1.0 - dz), v);
|
||||||
|
}
|
||||||
|
case Kind::FnTimer: {
|
||||||
|
const auto now = Clock::now();
|
||||||
|
if (!node.marked) {
|
||||||
|
node.mark = now;
|
||||||
|
node.marked = true;
|
||||||
|
}
|
||||||
|
const double period = Arg(node, 0, source);
|
||||||
|
double progress = std::chrono::duration_cast<FSec>(now - node.mark).count() / period;
|
||||||
|
if (!std::isfinite(progress) || progress < 0.0) {
|
||||||
|
progress = 0.0;
|
||||||
|
node.mark = now;
|
||||||
|
} else if (progress >= 1.0) {
|
||||||
|
const double resets = std::floor(progress);
|
||||||
|
node.mark += std::chrono::duration_cast<Clock::duration>(FSec(period * resets));
|
||||||
|
progress -= resets;
|
||||||
|
}
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
case Kind::FnToggle: {
|
||||||
|
const double inner = Arg(node, 0, source);
|
||||||
|
if (inner < kConditionThreshold) {
|
||||||
|
node.released = true;
|
||||||
|
} else if (node.released) {
|
||||||
|
node.released = false;
|
||||||
|
node.state = !node.state;
|
||||||
|
}
|
||||||
|
if (node.args.size() == 2 && Arg(node, 1, source) > kConditionThreshold) {
|
||||||
|
node.state = false;
|
||||||
|
}
|
||||||
|
return node.state ? 1.0 : 0.0;
|
||||||
|
}
|
||||||
|
case Kind::FnHold: {
|
||||||
|
const auto now = Clock::now();
|
||||||
|
if (!node.marked) {
|
||||||
|
node.mark = now;
|
||||||
|
node.marked = true;
|
||||||
|
}
|
||||||
|
const double input = Arg(node, 0, source);
|
||||||
|
if (input < kConditionThreshold) {
|
||||||
|
node.state = false;
|
||||||
|
node.mark = now;
|
||||||
|
} else if (!node.state) {
|
||||||
|
if (std::chrono::duration_cast<FSec>(now - node.mark).count() >= Arg(node, 1, source)) {
|
||||||
|
node.state = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node.state ? 1.0 : 0.0;
|
||||||
|
}
|
||||||
|
case Kind::FnTap: {
|
||||||
|
const auto now = Clock::now();
|
||||||
|
if (!node.marked) {
|
||||||
|
node.mark = now;
|
||||||
|
node.marked = true;
|
||||||
|
}
|
||||||
|
const double elapsed = std::chrono::duration_cast<FSec>(now - node.mark).count();
|
||||||
|
const double input = Arg(node, 0, source);
|
||||||
|
const bool timeUp = elapsed > Arg(node, 1, source);
|
||||||
|
// The count is user authored, so a negative or huge value must not
|
||||||
|
// reach the unsigned conversion.
|
||||||
|
double requested = node.args.size() == 3 ? Arg(node, 2, source) : 2.0;
|
||||||
|
if (!std::isfinite(requested)) {
|
||||||
|
requested = 2.0;
|
||||||
|
}
|
||||||
|
const auto desired = static_cast<unsigned>(std::clamp(requested + 0.5, 1.0, 64.0));
|
||||||
|
if (input < kConditionThreshold) {
|
||||||
|
node.released = true;
|
||||||
|
if (node.taps > 0 && timeUp) {
|
||||||
|
node.taps = 0;
|
||||||
|
}
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
if (node.released) {
|
||||||
|
if (node.taps == 0) {
|
||||||
|
node.mark = now;
|
||||||
|
}
|
||||||
|
++node.taps;
|
||||||
|
node.released = false;
|
||||||
|
}
|
||||||
|
return desired == node.taps ? 1.0 : 0.0;
|
||||||
|
}
|
||||||
|
case Kind::FnPulse: {
|
||||||
|
const auto now = Clock::now();
|
||||||
|
const double input = Arg(node, 0, source);
|
||||||
|
if (input < kConditionThreshold) {
|
||||||
|
node.released = true;
|
||||||
|
} else if (node.released) {
|
||||||
|
node.released = false;
|
||||||
|
const double requested = Arg(node, 1, source);
|
||||||
|
const double safe = std::isfinite(requested) ? std::clamp(requested, 0.0, 3600.0) : 0.0;
|
||||||
|
const auto seconds = std::chrono::duration_cast<Clock::duration>(FSec(safe));
|
||||||
|
if (node.state) {
|
||||||
|
node.mark += seconds;
|
||||||
|
} else {
|
||||||
|
node.state = true;
|
||||||
|
node.mark = now + seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (node.state && now >= node.mark) {
|
||||||
|
node.state = false;
|
||||||
|
}
|
||||||
|
return node.state ? 1.0 : 0.0;
|
||||||
|
}
|
||||||
|
case Kind::FnSmooth: {
|
||||||
|
const auto now = Clock::now();
|
||||||
|
if (!node.marked) {
|
||||||
|
node.mark = now;
|
||||||
|
node.marked = true;
|
||||||
|
}
|
||||||
|
const double elapsed = std::chrono::duration_cast<FSec>(now - node.mark).count();
|
||||||
|
node.mark = now;
|
||||||
|
const double desired = Arg(node, 0, source);
|
||||||
|
const double up = Arg(node, 1, source);
|
||||||
|
const double down = node.args.size() == 3 ? Arg(node, 2, source) : up;
|
||||||
|
const double rate = (desired < node.value) ? down : up;
|
||||||
|
const double maxMove = elapsed / rate;
|
||||||
|
if (!std::isfinite(maxMove)) {
|
||||||
|
node.value = desired;
|
||||||
|
} else {
|
||||||
|
const double diff = desired - node.value;
|
||||||
|
node.value += std::copysign(std::min(maxMove, std::abs(diff)), diff);
|
||||||
|
}
|
||||||
|
return node.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Collect(const Node& node, std::vector<std::string>& out) {
|
||||||
|
if (node.kind == Kind::Input) {
|
||||||
|
if (std::find(out.begin(), out.end(), node.input) == out.end()) {
|
||||||
|
out.push_back(node.input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& arg : node.args) {
|
||||||
|
Collect(*arg, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Trim(const std::string& text) {
|
||||||
|
const size_t begin = text.find_first_not_of(" \t\r\n");
|
||||||
|
if (begin == std::string::npos) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return text.substr(begin, text.find_last_not_of(" \t\r\n") - begin + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Expression::Expression() = default;
|
||||||
|
Expression::~Expression() = default;
|
||||||
|
Expression::Expression(Expression&&) noexcept = default;
|
||||||
|
Expression& Expression::operator=(Expression&&) noexcept = default;
|
||||||
|
|
||||||
|
bool Expression::Parse(const std::string& text, Expression& out, std::string& error) {
|
||||||
|
out.m_root.reset();
|
||||||
|
if (Trim(text).empty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Parser parser(text);
|
||||||
|
NodePtr root = parser.ParseExpression(error);
|
||||||
|
if (!root) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.m_root = std::move(root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
double Expression::Evaluate(const InputSource& source) const {
|
||||||
|
if (m_root == nullptr) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
const double value = Eval(*m_root, source);
|
||||||
|
return std::isfinite(value) ? value : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> Expression::ReferencedInputs() const {
|
||||||
|
std::vector<std::string> out;
|
||||||
|
if (m_root) {
|
||||||
|
Collect(*m_root, out);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadDolphinConfig(const std::filesystem::path& path, int padIndex,
|
||||||
|
std::vector<std::pair<std::string, std::string>>& controls,
|
||||||
|
std::string& deviceName, std::string& error) {
|
||||||
|
std::ifstream file(path);
|
||||||
|
if (!file) {
|
||||||
|
error = "could not open " + path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const std::string wanted = "[GCPad" + std::to_string(padIndex) + "]";
|
||||||
|
bool inSection = false;
|
||||||
|
bool found = false;
|
||||||
|
std::string line;
|
||||||
|
controls.clear();
|
||||||
|
deviceName.clear();
|
||||||
|
while (std::getline(file, line)) {
|
||||||
|
const std::string trimmed = Trim(line);
|
||||||
|
if (trimmed.empty() || trimmed[0] == '#' || trimmed[0] == ';') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (trimmed.front() == '[') {
|
||||||
|
inSection = trimmed == wanted;
|
||||||
|
found = found || inSection;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!inSection) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const size_t eq = trimmed.find('=');
|
||||||
|
if (eq == std::string::npos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::string key = Trim(trimmed.substr(0, eq));
|
||||||
|
const std::string value = Trim(trimmed.substr(eq + 1));
|
||||||
|
if (key == "Device") {
|
||||||
|
deviceName = value;
|
||||||
|
} else if (!value.empty()) {
|
||||||
|
controls.emplace_back(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
error = wanted + " not found in " + path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace InputExpr
|
||||||
@@ -60,7 +60,6 @@
|
|||||||
#include "aurora_events.h"
|
#include "aurora_events.h"
|
||||||
#include "wii_remote_input.h"
|
#include "wii_remote_input.h"
|
||||||
#include "discord_presence.h"
|
#include "discord_presence.h"
|
||||||
#include "touch_art.h"
|
|
||||||
#include "fiber_manager.h"
|
#include "fiber_manager.h"
|
||||||
#include "hle_stubs.h"
|
#include "hle_stubs.h"
|
||||||
#include "runtime_config.h"
|
#include "runtime_config.h"
|
||||||
@@ -1432,10 +1431,6 @@ int RuntimeMain(int argc, char** argv) {
|
|||||||
}
|
}
|
||||||
aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait);
|
aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait);
|
||||||
GxGuestWrite::InstallAuroraHooks();
|
GxGuestWrite::InstallAuroraHooks();
|
||||||
#ifdef MKW_PLATFORM_IOS
|
|
||||||
// Up front, or the frame the controls appear on pays for every decode.
|
|
||||||
TouchArt::Preload();
|
|
||||||
#endif
|
|
||||||
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
|
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
|
||||||
auroraInfo.windowSize.native_fb_height);
|
auroraInfo.windowSize.native_fb_height);
|
||||||
settings_overlay::InitializeRuntimeSettings();
|
settings_overlay::InitializeRuntimeSettings();
|
||||||
@@ -1514,28 +1509,7 @@ int RuntimeMain(int argc, char** argv) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SDL must own main() on iOS: it supplies the entry point that starts
|
|
||||||
// UIApplicationMain and hands control back here once the app is running.
|
|
||||||
#ifdef MKW_PLATFORM_IOS
|
|
||||||
#include <SDL3/SDL_main.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
#ifdef MKW_PLATFORM_IOS
|
|
||||||
// SDL's iOS backend calls SDL_main from scene:willConnectToSession:, once
|
|
||||||
// for every scene that connects (SDL_uikitappdelegate.m, postFinishLaunch).
|
|
||||||
// Attaching an external display makes iOS create a second scene, so this
|
|
||||||
// runs again while the first call is still inside the game loop, pumping
|
|
||||||
// events. Re-entering would boot the guest a second time in a live process:
|
|
||||||
// the observed result was data sections reloaded, 0 static constructors on
|
|
||||||
// the second pass, and aurora dying on "Only one window allowed per
|
|
||||||
// display". Declaring UIApplicationSupportsMultipleScenes=false does not
|
|
||||||
// prevent the extra scene, so refuse the re-entry here instead.
|
|
||||||
static std::atomic_flag alreadyRunning = ATOMIC_FLAG_INIT;
|
|
||||||
if (alreadyRunning.test_and_set(std::memory_order_acq_rel)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
return RuntimeMain(argc, argv);
|
return RuntimeMain(argc, argv);
|
||||||
}
|
}
|
||||||
extern "C" bool g_dynamicAspectRatioEnabled = false;
|
extern "C" bool g_dynamicAspectRatioEnabled = false;
|
||||||
|
|||||||
@@ -59,18 +59,6 @@ std::filesystem::path ApplicationDataDirectory(std::string_view applicationName)
|
|||||||
CoTaskMemFree(rawPath);
|
CoTaskMemFree(rawPath);
|
||||||
return directory;
|
return directory;
|
||||||
}
|
}
|
||||||
#elif defined(MKW_PLATFORM_IOS)
|
|
||||||
// The whole container is already private to this app, so user state lives
|
|
||||||
// in Documents rather than a name-scoped Application Support subdirectory.
|
|
||||||
// That puts Config.toml beside the game data the user copied in, which is
|
|
||||||
// what makes a relative dvd_root resolve, and UIFileSharingEnabled exposes
|
|
||||||
// the directory in Files.app so the user can manage it.
|
|
||||||
if (const char* home = std::getenv("HOME"); home && *home) {
|
|
||||||
return std::filesystem::path(home) / "Documents";
|
|
||||||
}
|
|
||||||
if (const passwd* user = getpwuid(getuid()); user && user->pw_dir && *user->pw_dir) {
|
|
||||||
return std::filesystem::path(user->pw_dir) / "Documents";
|
|
||||||
}
|
|
||||||
#elif defined(__APPLE__)
|
#elif defined(__APPLE__)
|
||||||
if (const char* home = std::getenv("HOME"); home && *home) {
|
if (const char* home = std::getenv("HOME"); home && *home) {
|
||||||
return std::filesystem::path(home) / "Library" / "Application Support" / applicationName;
|
return std::filesystem::path(home) / "Library" / "Application Support" / applicationName;
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
// Apple's clang links libclang_rt.ios.a implicitly; upstream clang on a Linux
|
|
||||||
// host has no iOS compiler-rt, and the only builtins this program needs from
|
|
||||||
// it are the two @available() checks. They are thin wrappers over dyld, so
|
|
||||||
// provide them here rather than asking people to extract a file from Xcode.
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint32_t platform;
|
|
||||||
uint32_t version;
|
|
||||||
} dyld_build_version_t;
|
|
||||||
|
|
||||||
extern int _availability_version_check(uint32_t count, dyld_build_version_t versions[]);
|
|
||||||
|
|
||||||
int32_t __isPlatformVersionAtLeast(uint32_t platform, uint32_t major, uint32_t minor, uint32_t subminor) {
|
|
||||||
dyld_build_version_t wanted = {platform, (major << 16) | (minor << 8) | subminor};
|
|
||||||
return _availability_version_check(1, &wanted);
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t __isOSVersionAtLeast(int32_t major, int32_t minor, int32_t subminor) {
|
|
||||||
return __isPlatformVersionAtLeast(2 /* PLATFORM_IOS */, (uint32_t)major, (uint32_t)minor, (uint32_t)subminor);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
#include "settings_overlay.h"
|
#include "settings_overlay.h"
|
||||||
#include "touch_pad.h"
|
|
||||||
#include "audio_backend.h"
|
#include "audio_backend.h"
|
||||||
|
#include "controller_button_names.h"
|
||||||
#include "controller_mapping_wizard.h"
|
#include "controller_mapping_wizard.h"
|
||||||
|
#include "input_bindings.h"
|
||||||
#include "game_graphics_options.h"
|
#include "game_graphics_options.h"
|
||||||
#include "music_attenuation.h"
|
#include "music_attenuation.h"
|
||||||
#include "runtime_config.h"
|
#include "runtime_config.h"
|
||||||
@@ -35,6 +36,8 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <dolphin/pad.h>
|
#include <dolphin/pad.h>
|
||||||
|
|
||||||
|
extern "C" void PAD_HLE_SetRumbleEnabled(bool enabled);
|
||||||
#include <dolphin/vi.h>
|
#include <dolphin/vi.h>
|
||||||
#include <aurora/aurora.h>
|
#include <aurora/aurora.h>
|
||||||
#include <aurora/gfx.h>
|
#include <aurora/gfx.h>
|
||||||
@@ -66,8 +69,7 @@ const char* GraphicsApiDisplayName() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool g_topBarVisible = false;
|
bool g_topBarVisible = false;
|
||||||
float g_fpsBounds[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
bool g_rumbleEnabled = RuntimeConfigFile::RumbleEnabled(true);
|
||||||
bool g_fpsBoundsValid = false;
|
|
||||||
int g_controllerPort = 0;
|
int g_controllerPort = 0;
|
||||||
float g_resolutionScale = RuntimeConfigFile::ResolutionMultiplier(1.0f);
|
float g_resolutionScale = RuntimeConfigFile::ResolutionMultiplier(1.0f);
|
||||||
int g_audioVolumePercent = static_cast<int>(std::lround(RuntimeConfigFile::AudioVolume(1.0f) * 100.0f));
|
int g_audioVolumePercent = static_cast<int>(std::lround(RuntimeConfigFile::AudioVolume(1.0f) * 100.0f));
|
||||||
@@ -109,62 +111,9 @@ std::array<int32_t, PAD_MAX_CONTROLLERS> g_configuredControllerIndices = [] {
|
|||||||
return indices;
|
return indices;
|
||||||
}();
|
}();
|
||||||
|
|
||||||
struct ControllerButtonItem {
|
using ControllerNames::kNativeButtons;
|
||||||
const char* configKey;
|
using ControllerNames::NativeButtonItem;
|
||||||
const char* label;
|
constexpr const auto& kControllerButtons = ControllerNames::kGameCubeButtons;
|
||||||
PADButton padButton;
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr std::array<ControllerButtonItem, PAD_BUTTON_COUNT> kControllerButtons = {{
|
|
||||||
{"a", "A", PAD_BUTTON_A},
|
|
||||||
{"b", "B", PAD_BUTTON_B},
|
|
||||||
{"x", "X", PAD_BUTTON_X},
|
|
||||||
{"y", "Y", PAD_BUTTON_Y},
|
|
||||||
{"start", "Start", PAD_BUTTON_START},
|
|
||||||
{"z", "Z", PAD_TRIGGER_Z},
|
|
||||||
{"l", "L", PAD_TRIGGER_L},
|
|
||||||
{"r", "R", PAD_TRIGGER_R},
|
|
||||||
{"up", "D-pad Up", PAD_BUTTON_UP},
|
|
||||||
{"down", "D-pad Down", PAD_BUTTON_DOWN},
|
|
||||||
{"left", "D-pad Left", PAD_BUTTON_LEFT},
|
|
||||||
{"right", "D-pad Right", PAD_BUTTON_RIGHT},
|
|
||||||
}};
|
|
||||||
|
|
||||||
struct NativeButtonItem {
|
|
||||||
const char* configName;
|
|
||||||
const char* label;
|
|
||||||
uint32_t nativeButton;
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr std::array<NativeButtonItem, SDL_GAMEPAD_BUTTON_COUNT + 1> kNativeButtons = {{
|
|
||||||
{"unmapped", "Unmapped / analog trigger", PAD_NATIVE_BUTTON_INVALID},
|
|
||||||
{"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH},
|
|
||||||
{"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST},
|
|
||||||
{"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST},
|
|
||||||
{"north", "North (Y / Triangle)", SDL_GAMEPAD_BUTTON_NORTH},
|
|
||||||
{"back", "Back / Select", SDL_GAMEPAD_BUTTON_BACK},
|
|
||||||
{"guide", "Guide / Home", SDL_GAMEPAD_BUTTON_GUIDE},
|
|
||||||
{"start", "Start / Options", SDL_GAMEPAD_BUTTON_START},
|
|
||||||
{"left_stick", "Left stick click", SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
|
||||||
{"right_stick", "Right stick click", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
|
|
||||||
{"left_shoulder", "Left shoulder", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
|
||||||
{"right_shoulder", "Right shoulder", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
|
||||||
{"dpad_up", "D-pad Up", SDL_GAMEPAD_BUTTON_DPAD_UP},
|
|
||||||
{"dpad_down", "D-pad Down", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
|
||||||
{"dpad_left", "D-pad Left", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
|
|
||||||
{"dpad_right", "D-pad Right", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
|
||||||
{"misc1", "Misc 1 / Share", SDL_GAMEPAD_BUTTON_MISC1},
|
|
||||||
{"right_paddle1", "Right paddle 1", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
|
|
||||||
{"left_paddle1", "Left paddle 1", SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
|
|
||||||
{"right_paddle2", "Right paddle 2", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
|
|
||||||
{"left_paddle2", "Left paddle 2", SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
|
|
||||||
{"touchpad", "Touchpad", SDL_GAMEPAD_BUTTON_TOUCHPAD},
|
|
||||||
{"misc2", "Misc 2", SDL_GAMEPAD_BUTTON_MISC2},
|
|
||||||
{"misc3", "Misc 3 / GC L click", SDL_GAMEPAD_BUTTON_MISC3},
|
|
||||||
{"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4},
|
|
||||||
{"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5},
|
|
||||||
{"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6},
|
|
||||||
}};
|
|
||||||
|
|
||||||
// Classic Controller Pro layout, indexed like kControllerButtons: the SNES-style
|
// Classic Controller Pro layout, indexed like kControllerButtons: the SNES-style
|
||||||
// diamond (A right, B bottom, X top, Y left) with digital bumpers driving the GC
|
// diamond (A right, B bottom, X top, Y left) with digital bumpers driving the GC
|
||||||
@@ -181,6 +130,13 @@ constexpr std::array<const char*, PAD_BUTTON_COUNT> kClassicProPreset = {
|
|||||||
"dpad_up", "dpad_down", "dpad_left", "dpad_right",
|
"dpad_up", "dpad_down", "dpad_left", "dpad_right",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// PlayStation layout: bumpers drive the GC triggers, Z moves to Create/Share.
|
||||||
|
constexpr std::array<const char*, PAD_BUTTON_COUNT> kPlayStationPreset = {
|
||||||
|
"south", "east", "west", "north", "start", "back",
|
||||||
|
"left_shoulder", "right_shoulder",
|
||||||
|
"dpad_up", "dpad_down", "dpad_left", "dpad_right",
|
||||||
|
};
|
||||||
|
|
||||||
struct ResolutionItem {
|
struct ResolutionItem {
|
||||||
const char* label;
|
const char* label;
|
||||||
float scale;
|
float scale;
|
||||||
@@ -199,12 +155,7 @@ constexpr std::array<std::string_view, 3> kDisplayModeConfigNames = {
|
|||||||
uint64_t g_presentedFrame = 0;
|
uint64_t g_presentedFrame = 0;
|
||||||
std::atomic_bool g_strapInputAccepted = false;
|
std::atomic_bool g_strapInputAccepted = false;
|
||||||
std::atomic_uint64_t g_startupDismissFrame = UINT64_MAX;
|
std::atomic_uint64_t g_startupDismissFrame = UINT64_MAX;
|
||||||
// The strap screen is accepted the instant it becomes eligible, so the frames
|
constexpr uint64_t kStrapTransitionCoverFrames = 60;
|
||||||
// it would have spent waiting for A are still rendered underneath this cover.
|
|
||||||
// One second was not enough on an Apple TV 4K or an iPhone loading from the
|
|
||||||
// app container: the scene change landed after the cover lifted and the strap
|
|
||||||
// warning flashed through. Three seconds covers the slowest device measured.
|
|
||||||
constexpr uint64_t kStrapTransitionCoverFrames = 180;
|
|
||||||
|
|
||||||
constexpr std::array<ResolutionItem, 8> kResolutions = {{
|
constexpr std::array<ResolutionItem, 8> kResolutions = {{
|
||||||
{"Auto (window size)", 0.0f}, {"Native (1x)", 1.0f}, {"1.5x", 1.5f}, {"2x", 2.0f},
|
{"Auto (window size)", 0.0f}, {"Native (1x)", 1.0f}, {"1.5x", 1.5f}, {"2x", 2.0f},
|
||||||
@@ -233,43 +184,25 @@ void LimitResolutionForFrameRate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const NativeButtonItem* FindNativeButton(std::string value) {
|
using ControllerNames::FindNativeButton;
|
||||||
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(), [&](const NativeButtonItem& item) {
|
|
||||||
return value == item.configName;
|
|
||||||
});
|
|
||||||
return it == kNativeButtons.end() ? nullptr : &*it;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ControllerBindingPair {
|
struct ControllerBindingPair {
|
||||||
std::string primary;
|
std::string primary;
|
||||||
std::string secondary;
|
std::string secondary;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string TrimBindingToken(const std::string& token) {
|
|
||||||
const size_t begin = token.find_first_not_of(" \t");
|
|
||||||
if (begin == std::string::npos) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const size_t end = token.find_last_not_of(" \t");
|
|
||||||
return token.substr(begin, end - begin + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config values hold up to two comma-separated button names ("dpad_up" or
|
// Config values hold up to two comma-separated button names ("dpad_up" or
|
||||||
// "dpad_up,left_shoulder"); pressing either one counts as the GC button.
|
// "dpad_up,left_shoulder"); pressing either one counts as the GC button.
|
||||||
ControllerBindingPair SplitControllerBinding(const std::string& value) {
|
ControllerBindingPair SplitControllerBinding(const std::string& value) {
|
||||||
const size_t comma = value.find(',');
|
const size_t comma = value.find(',');
|
||||||
if (comma == std::string::npos) {
|
if (comma == std::string::npos) {
|
||||||
return {TrimBindingToken(value), {}};
|
return {ControllerNames::TrimToken(value), {}};
|
||||||
}
|
}
|
||||||
return {TrimBindingToken(value.substr(0, comma)), TrimBindingToken(value.substr(comma + 1))};
|
return {ControllerNames::TrimToken(value.substr(0, comma)), ControllerNames::TrimToken(value.substr(comma + 1))};
|
||||||
}
|
}
|
||||||
|
|
||||||
const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) {
|
using ControllerNames::NativeButtonForValue;
|
||||||
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(), [&](const NativeButtonItem& item) {
|
|
||||||
return nativeButton == item.nativeButton;
|
|
||||||
});
|
|
||||||
return it == kNativeButtons.end() ? kNativeButtons.front() : *it;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SetTopBarVisible(bool visible) {
|
void SetTopBarVisible(bool visible) {
|
||||||
if (g_topBarVisible == visible) {
|
if (g_topBarVisible == visible) {
|
||||||
@@ -401,10 +334,8 @@ void DrawWiiRemoteSettings(uint32_t selectedGamePort) {
|
|||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (WiiRemoteInput::IsScanning() && WiiRemoteInput::PeriodicRescanEnabled()) {
|
if (WiiRemoteInput::IsScanning()) {
|
||||||
ImGui::TextDisabled("Scanning... (%u so far) - press 1+2 on the remote", WiiRemoteInput::ScanCount());
|
ImGui::TextDisabled("Scanning... (%u so far) - press 1+2 on the remote", WiiRemoteInput::ScanCount());
|
||||||
} else if (WiiRemoteInput::IsScanning()) {
|
|
||||||
ImGui::TextDisabled("Waiting for a remote - press 1+2 on the remote");
|
|
||||||
} else {
|
} else {
|
||||||
ImGui::TextDisabled("Not scanning");
|
ImGui::TextDisabled("Not scanning");
|
||||||
}
|
}
|
||||||
@@ -465,6 +396,100 @@ void DrawWiiRemoteSettings(uint32_t selectedGamePort) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Controller settings menu: port selection, controller assignment and button mapping.
|
// Controller settings menu: port selection, controller assignment and button mapping.
|
||||||
|
int ExpressionResizeCallback(ImGuiInputTextCallbackData* data) {
|
||||||
|
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
|
||||||
|
auto* text = static_cast<std::string*>(data->UserData);
|
||||||
|
text->resize(static_cast<size_t>(data->BufTextLen));
|
||||||
|
data->Buf = text->data();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawExpressionSettings() {
|
||||||
|
ImGui::SeparatorText("Expressions (Dolphin syntax)");
|
||||||
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 440.0f);
|
||||||
|
ImGui::TextDisabled(
|
||||||
|
"Optional. An expression overrides nothing: its result is combined with the "
|
||||||
|
"button mapping above. Operators ! & | ^ and functions if, min, max, clamp, "
|
||||||
|
"timer, toggle, hold, tap, pulse, smooth, deadzone behave as they do in Dolphin.");
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
|
||||||
|
static std::array<std::string, InputBindings::kControls.size()> errors;
|
||||||
|
static std::array<std::string, InputBindings::kControls.size()> buffers;
|
||||||
|
static std::string importStatus;
|
||||||
|
static int loadedPort = -1;
|
||||||
|
static bool reloadBuffers = true;
|
||||||
|
const auto port = static_cast<uint32_t>(g_controllerPort);
|
||||||
|
|
||||||
|
if (loadedPort != g_controllerPort || reloadBuffers) {
|
||||||
|
for (size_t i = 0; i < buffers.size(); ++i) {
|
||||||
|
buffers[i] = InputBindings::GetExpression(port, i);
|
||||||
|
}
|
||||||
|
errors.fill(std::string());
|
||||||
|
loadedPort = g_controllerPort;
|
||||||
|
reloadBuffers = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::Button("Import from Dolphin")) {
|
||||||
|
const std::string path = InputBindings::DefaultDolphinConfigPath();
|
||||||
|
std::string summary;
|
||||||
|
std::string error;
|
||||||
|
if (InputBindings::ImportDolphinConfig(path, g_controllerPort + 1, port, summary, error) < 0) {
|
||||||
|
importStatus = error;
|
||||||
|
} else {
|
||||||
|
importStatus = summary;
|
||||||
|
errors.fill(std::string());
|
||||||
|
reloadBuffers = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetTooltip("Reads [GCPad%d] from %%APPDATA%%\\Dolphin Emulator\\Config\\GCPadNew.ini,\n"
|
||||||
|
"or GCPadNew.ini next to the executable.", g_controllerPort + 1);
|
||||||
|
}
|
||||||
|
if (!importStatus.empty()) {
|
||||||
|
ImGui::TextDisabled("%s", importStatus.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < InputBindings::kControls.size(); ++i) {
|
||||||
|
ImGui::PushID(static_cast<int>(i) + 2000);
|
||||||
|
std::string& text = buffers[i];
|
||||||
|
ImGui::SetNextItemWidth(300.0f);
|
||||||
|
if (ImGui::InputText(InputBindings::kControls[i].label, text.data(), text.capacity() + 1,
|
||||||
|
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackResize,
|
||||||
|
ExpressionResizeCallback, &text)) {
|
||||||
|
std::string error;
|
||||||
|
errors[i] = InputBindings::SetExpression(port, i, text, error) ? std::string() : error;
|
||||||
|
}
|
||||||
|
if (InputBindings::IsActive(port, i)) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f), "active");
|
||||||
|
}
|
||||||
|
if (!errors[i].empty()) {
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.3f, 1.0f), "%s", errors[i].c_str());
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawRumbleSettings() {
|
||||||
|
ImGui::SeparatorText("Vibration");
|
||||||
|
if (ImGui::Checkbox("Controller vibration", &g_rumbleEnabled)) {
|
||||||
|
PAD_HLE_SetRumbleEnabled(g_rumbleEnabled);
|
||||||
|
RuntimeConfigFile::SetRumbleEnabled(g_rumbleEnabled);
|
||||||
|
if (!g_rumbleEnabled) {
|
||||||
|
// Stop whatever is already running: the game will not send another
|
||||||
|
// motor command until its own state machine decides to.
|
||||||
|
constexpr std::array<uint32_t, PAD_MAX_CONTROLLERS> stopAll{
|
||||||
|
PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD,
|
||||||
|
};
|
||||||
|
PADControlAllMotors(stopAll.data());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetTooltip("Applies to every port.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void DrawControllerSettings() {
|
void DrawControllerSettings() {
|
||||||
for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
||||||
const std::string label = "Port " + std::to_string(port + 1);
|
const std::string label = "Port " + std::to_string(port + 1);
|
||||||
@@ -553,20 +578,28 @@ void DrawControllerSettings() {
|
|||||||
PADSerializeMappings();
|
PADSerializeMappings();
|
||||||
mappings = PADGetButtonMappings(port, &mappingCount);
|
mappings = PADGetButtonMappings(port, &mappingCount);
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
const auto applyPreset = [&](const std::array<const char*, PAD_BUTTON_COUNT>& preset) {
|
||||||
if (ImGui::Button("Classic Controller Pro")) {
|
|
||||||
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
|
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
|
||||||
for (size_t i = 0; i < kControllerButtons.size(); ++i) {
|
for (size_t i = 0; i < kControllerButtons.size(); ++i) {
|
||||||
if (const NativeButtonItem* native = FindNativeButton(kClassicProPreset[i])) {
|
if (const NativeButtonItem* native = FindNativeButton(preset[i])) {
|
||||||
PADSetButtonMapping(port, PADButtonMapping{native->nativeButton, kControllerButtons[i].padButton});
|
PADSetButtonMapping(port, PADButtonMapping{native->nativeButton, kControllerButtons[i].padButton});
|
||||||
PADSetAltButtonMapping(port,
|
PADSetAltButtonMapping(port,
|
||||||
PADButtonMapping{PAD_NATIVE_BUTTON_INVALID, kControllerButtons[i].padButton});
|
PADButtonMapping{PAD_NATIVE_BUTTON_INVALID, kControllerButtons[i].padButton});
|
||||||
RuntimeConfigFile::SetControllerButton(i, kClassicProPreset[i]);
|
RuntimeConfigFile::SetControllerButton(i, preset[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
altRowExpanded.fill(false);
|
altRowExpanded.fill(false);
|
||||||
PADSerializeMappings();
|
PADSerializeMappings();
|
||||||
mappings = PADGetButtonMappings(port, &mappingCount);
|
mappings = PADGetButtonMappings(port, &mappingCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Classic Controller Pro")) {
|
||||||
|
applyPreset(kClassicProPreset);
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("PlayStation")) {
|
||||||
|
applyPreset(kPlayStationPreset);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SeparatorText("Button mapping");
|
ImGui::SeparatorText("Button mapping");
|
||||||
@@ -648,6 +681,8 @@ void DrawControllerSettings() {
|
|||||||
ImGui::TextUnformatted(kControllerButtons[i].label);
|
ImGui::TextUnformatted(kControllerButtons[i].label);
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
|
DrawExpressionSettings();
|
||||||
|
DrawRumbleSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawAudioSettings() {
|
void DrawAudioSettings() {
|
||||||
@@ -806,9 +841,6 @@ void DrawGraphicsSettings() {
|
|||||||
void DrawFpsOverlay() {
|
void DrawFpsOverlay() {
|
||||||
AuroraPresentTiming presentTiming{};
|
AuroraPresentTiming presentTiming{};
|
||||||
aurora_get_present_timing(&presentTiming);
|
aurora_get_present_timing(&presentTiming);
|
||||||
// Before the early return: a stale rectangle would keep swallowing taps
|
|
||||||
// meant for the menu button, leaving no way into settings.
|
|
||||||
g_fpsBoundsValid = false;
|
|
||||||
if (!g_showFps) {
|
if (!g_showFps) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -826,13 +858,6 @@ void DrawFpsOverlay() {
|
|||||||
ImGuiWindowFlags_NoNav |
|
ImGuiWindowFlags_NoNav |
|
||||||
ImGuiWindowFlags_NoSavedSettings;
|
ImGuiWindowFlags_NoSavedSettings;
|
||||||
if (ImGui::Begin("FPS Overlay", nullptr, kFlags)) {
|
if (ImGui::Begin("FPS Overlay", nullptr, kFlags)) {
|
||||||
const ImVec2 wpos = ImGui::GetWindowPos();
|
|
||||||
const ImVec2 wsize = ImGui::GetWindowSize();
|
|
||||||
constexpr float kTouchPad = 14.0f;
|
|
||||||
g_fpsBounds[0] = wpos.x - kTouchPad; g_fpsBounds[1] = wpos.y - kTouchPad;
|
|
||||||
g_fpsBounds[2] = wpos.x + wsize.x + kTouchPad;
|
|
||||||
g_fpsBounds[3] = wpos.y + wsize.y + kTouchPad;
|
|
||||||
g_fpsBoundsValid = true;
|
|
||||||
if (presentTiming.sampleCount == 0) {
|
if (presentTiming.sampleCount == 0) {
|
||||||
ImGui::TextUnformatted("FPS: --");
|
ImGui::TextUnformatted("FPS: --");
|
||||||
} else {
|
} else {
|
||||||
@@ -1012,6 +1037,8 @@ void PersistDisplayModeIfChanged() {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void InitializeRuntimeSettings() noexcept {
|
void InitializeRuntimeSettings() noexcept {
|
||||||
|
PAD_HLE_SetRumbleEnabled(g_rumbleEnabled);
|
||||||
|
InputBindings::Reload();
|
||||||
controller_mapping_wizard::LoadPersistedMappings();
|
controller_mapping_wizard::LoadPersistedMappings();
|
||||||
ApplyConfiguredMappings();
|
ApplyConfiguredMappings();
|
||||||
AudioBackend::Instance().SetMasterVolume(static_cast<float>(g_audioVolumePercent) / 100.0f);
|
AudioBackend::Instance().SetMasterVolume(static_cast<float>(g_audioVolumePercent) / 100.0f);
|
||||||
@@ -1032,6 +1059,7 @@ void InitializeRuntimeSettings() noexcept {
|
|||||||
g_strapInputAccepted.store(false, std::memory_order_relaxed);
|
g_strapInputAccepted.store(false, std::memory_order_relaxed);
|
||||||
g_startupDismissFrame.store(UINT64_MAX, std::memory_order_relaxed);
|
g_startupDismissFrame.store(UINT64_MAX, std::memory_order_relaxed);
|
||||||
PADBlockInput(false);
|
PADBlockInput(false);
|
||||||
|
InputBindings::SetInputBlocked(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HandleEvents(const AuroraEvent* events) noexcept {
|
void HandleEvents(const AuroraEvent* events) noexcept {
|
||||||
@@ -1055,14 +1083,6 @@ void HandleEvents(const AuroraEvent* events) noexcept {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ToggleTopBar() noexcept { SetTopBarVisible(!g_topBarVisible); }
|
|
||||||
bool TopBarVisible() noexcept { return g_topBarVisible; }
|
|
||||||
bool FpsOverlayBounds(float* a, float* b, float* c, float* d) noexcept {
|
|
||||||
if (!g_fpsBoundsValid) return false;
|
|
||||||
*a = g_fpsBounds[0]; *b = g_fpsBounds[1]; *c = g_fpsBounds[2]; *d = g_fpsBounds[3];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Draw() noexcept {
|
void Draw() noexcept {
|
||||||
// Wait for the frame worker's DONE phase: it has replayed the previous frame's ImGui draw lists
|
// Wait for the frame worker's DONE phase: it has replayed the previous frame's ImGui draw lists
|
||||||
// and started the next ImGui frame, so all overlay callers can now safely issue ImGui commands.
|
// and started the next ImGui frame, so all overlay callers can now safely issue ImGui commands.
|
||||||
@@ -1081,13 +1101,10 @@ void Draw() noexcept {
|
|||||||
DrawFpsOverlay();
|
DrawFpsOverlay();
|
||||||
DrawTopBar();
|
DrawTopBar();
|
||||||
controller_mapping_wizard::Draw();
|
controller_mapping_wizard::Draw();
|
||||||
#ifdef MKW_PLATFORM_IOS
|
// The wizard captures raw presses; keep them out of the game.
|
||||||
TouchPad::Draw();
|
const bool inputBlocked = controller_mapping_wizard::IsActive();
|
||||||
#endif
|
PADBlockInput(inputBlocked);
|
||||||
// The wizard captures raw presses; keep them out of the game even when the
|
InputBindings::SetInputBlocked(inputBlocked);
|
||||||
// top bar is hidden mid-setup. The top bar matters for touch too: while it
|
|
||||||
// is open, taps belong to the bar and must not drive the guest.
|
|
||||||
PADBlockInput(g_topBarVisible || controller_mapping_wizard::IsActive());
|
|
||||||
DrawStartupScreen();
|
DrawStartupScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#include "nand_settings.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
static void Require(bool condition) {
|
||||||
|
if (!condition) {
|
||||||
|
throw std::runtime_error("NAND settings check failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
const auto root = std::filesystem::temp_directory_path() /
|
||||||
|
("wiicomp-nand-settings-" + std::to_string(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||||
|
const auto path = root / "title/00000001/00000002/data/setting.txt";
|
||||||
|
try {
|
||||||
|
Require(!RuntimeNandSettings::Read(root));
|
||||||
|
Require(!std::filesystem::exists(root));
|
||||||
|
std::filesystem::create_directories(path.parent_path());
|
||||||
|
const std::string plain = "AREA=USA\r\n\nCODE=LU\r\nSERNO=987654321\r\nGAME=US\r\n";
|
||||||
|
std::array<uint8_t, 256> fixture{};
|
||||||
|
for (size_t i = 0; i < fixture.size(); ++i) {
|
||||||
|
const unsigned shift = i % 32;
|
||||||
|
const uint32_t key = shift == 0 ? 0x73B5DBFAu :
|
||||||
|
(0x73B5DBFAu << shift) | (0x73B5DBFAu >> (32 - shift));
|
||||||
|
fixture[i] = static_cast<uint8_t>(key) ^ (i < plain.size() ? plain[i] : 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::ofstream output(path, std::ios::binary);
|
||||||
|
output.write(reinterpret_cast<const char*>(fixture.data()), fixture.size());
|
||||||
|
}
|
||||||
|
auto settings = RuntimeNandSettings::Read(root);
|
||||||
|
Require(settings && RuntimeNandSettings::HasIdentity(*settings));
|
||||||
|
Require(settings->at("SERNO") == "987654321" && settings->at("CODE") == "LU");
|
||||||
|
Require(settings->at("AREA") == "USA" && settings->at("GAME") == "US");
|
||||||
|
std::array<uint8_t, 256> after{};
|
||||||
|
{
|
||||||
|
std::ifstream input(path, std::ios::binary);
|
||||||
|
input.read(reinterpret_cast<char*>(after.data()), after.size());
|
||||||
|
}
|
||||||
|
Require(after == fixture);
|
||||||
|
for (const auto serial : {"", "000000000", "1234567890", "123ABC789"}) {
|
||||||
|
(*settings)["SERNO"] = serial;
|
||||||
|
Require(!RuntimeNandSettings::HasIdentity(*settings));
|
||||||
|
}
|
||||||
|
(*settings)["SERNO"] = "012345678";
|
||||||
|
Require(RuntimeNandSettings::HasIdentity(*settings));
|
||||||
|
(*settings)["CODE"] = "TOOLONG";
|
||||||
|
Require(!RuntimeNandSettings::HasIdentity(*settings));
|
||||||
|
(*settings)["CODE"] = "LEH";
|
||||||
|
settings->erase("GAME");
|
||||||
|
Require(!RuntimeNandSettings::HasIdentity(*settings));
|
||||||
|
std::filesystem::resize_file(path, 128);
|
||||||
|
Require(!RuntimeNandSettings::Read(root));
|
||||||
|
std::filesystem::remove_all(root);
|
||||||
|
std::cout << "NAND settings checks passed\n";
|
||||||
|
return 0;
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
std::filesystem::remove_all(root);
|
||||||
|
std::cerr << error.what() << '\n';
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
// Verifies the expression engine against Dolphin's documented semantics,
|
||||||
|
// including the exact line from the user's GCPadNew.ini.
|
||||||
|
|
||||||
|
#include "input_expr.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
static int g_failures = 0;
|
||||||
|
static std::map<std::string, double> g_inputs;
|
||||||
|
|
||||||
|
static InputExpr::InputSource Source() {
|
||||||
|
return [](const std::string& name) {
|
||||||
|
const auto it = g_inputs.find(name);
|
||||||
|
return it == g_inputs.end() ? 0.0 : it->second;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Check(bool ok, const std::string& what) {
|
||||||
|
if (!ok) {
|
||||||
|
std::printf(" FAIL: %s\n", what.c_str());
|
||||||
|
++g_failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static InputExpr::Expression Compile(const std::string& text) {
|
||||||
|
InputExpr::Expression expr;
|
||||||
|
std::string error;
|
||||||
|
if (!InputExpr::Expression::Parse(text, expr, error)) {
|
||||||
|
std::printf(" FAIL: parse '%s': %s\n", text.c_str(), error.c_str());
|
||||||
|
++g_failures;
|
||||||
|
}
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Pressed(const InputExpr::Expression& e) {
|
||||||
|
return e.Evaluate(Source()) > InputExpr::kConditionThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Sleep(int ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); }
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::printf("Dolphin expression engine\n");
|
||||||
|
|
||||||
|
// Operators: & is min, | is max, ! is 1-x, matching Dolphin.
|
||||||
|
g_inputs["A"] = 1.0;
|
||||||
|
g_inputs["B"] = 0.0;
|
||||||
|
Check(Pressed(Compile("`A`")), "bare input");
|
||||||
|
Check(!Pressed(Compile("!`A`")), "not");
|
||||||
|
Check(!Pressed(Compile("`A` & `B`")), "and is min");
|
||||||
|
Check(Pressed(Compile("`A` | `B`")), "or is max");
|
||||||
|
Check(Pressed(Compile("`A` ^ `B`")), "xor");
|
||||||
|
Check(!Pressed(Compile("`A` ^ `A`")), "xor of equal inputs is false");
|
||||||
|
|
||||||
|
// Precedence: & binds tighter than |, so this is A | (B & A).
|
||||||
|
g_inputs["B"] = 0.0;
|
||||||
|
Check(Pressed(Compile("`A` | `B` & `A`")), "& binds tighter than |");
|
||||||
|
|
||||||
|
// Parens and numeric literals.
|
||||||
|
Check(Pressed(Compile("(`B` | 1)")), "literal");
|
||||||
|
Check(Pressed(Compile("min(1, `A`)")), "min");
|
||||||
|
Check(!Pressed(Compile("min(0, `A`)")), "min with zero");
|
||||||
|
Check(Pressed(Compile("if(`A`, 1, 0)")), "if");
|
||||||
|
Check(Pressed(Compile("clamp(5, 0, 1)")), "clamp");
|
||||||
|
|
||||||
|
// toggle flips on each rising edge and holds between them.
|
||||||
|
auto toggle = Compile("toggle(`T`)");
|
||||||
|
g_inputs["T"] = 0.0;
|
||||||
|
toggle.Evaluate(Source());
|
||||||
|
g_inputs["T"] = 1.0;
|
||||||
|
Check(Pressed(toggle), "toggle on after first press");
|
||||||
|
g_inputs["T"] = 0.0;
|
||||||
|
Check(Pressed(toggle), "toggle stays on after release");
|
||||||
|
g_inputs["T"] = 1.0;
|
||||||
|
Check(!Pressed(toggle), "toggle off on second press");
|
||||||
|
|
||||||
|
// hold requires the input to be down for the full duration.
|
||||||
|
auto hold = Compile("hold(`H`, 0.05)");
|
||||||
|
g_inputs["H"] = 1.0;
|
||||||
|
Check(!Pressed(hold), "hold not satisfied immediately");
|
||||||
|
Sleep(70);
|
||||||
|
Check(Pressed(hold), "hold satisfied after the interval");
|
||||||
|
g_inputs["H"] = 0.0;
|
||||||
|
Check(!Pressed(hold), "hold clears on release");
|
||||||
|
|
||||||
|
// pulse fires for the given duration after a rising edge.
|
||||||
|
auto pulse = Compile("pulse(`P`, 0.05)");
|
||||||
|
g_inputs["P"] = 0.0;
|
||||||
|
pulse.Evaluate(Source());
|
||||||
|
g_inputs["P"] = 1.0;
|
||||||
|
Check(Pressed(pulse), "pulse fires on rising edge");
|
||||||
|
Sleep(80);
|
||||||
|
Check(!Pressed(pulse), "pulse expires");
|
||||||
|
|
||||||
|
// The timing-window idiom seen in shared Dolphin configs.
|
||||||
|
auto window = Compile("!pulse(`W`, 0.05) & pulse(`W`, 0.15)");
|
||||||
|
g_inputs["W"] = 0.0;
|
||||||
|
window.Evaluate(Source());
|
||||||
|
g_inputs["W"] = 1.0;
|
||||||
|
Check(!Pressed(window), "window closed before its start");
|
||||||
|
Sleep(90);
|
||||||
|
Check(Pressed(window), "window open between the two pulses");
|
||||||
|
Sleep(90);
|
||||||
|
Check(!Pressed(window), "window closed after its end");
|
||||||
|
|
||||||
|
// timer ramps 0..1 and wraps, so a threshold turns it into a square wave.
|
||||||
|
auto timer = Compile("`X` & timer(0.1)");
|
||||||
|
g_inputs["X"] = 1.0;
|
||||||
|
int high = 0;
|
||||||
|
int low = 0;
|
||||||
|
for (int i = 0; i < 40; ++i) {
|
||||||
|
(Pressed(timer) ? high : low)++;
|
||||||
|
Sleep(5);
|
||||||
|
}
|
||||||
|
Check(high > 5 && low > 5, "timer alternates high and low");
|
||||||
|
|
||||||
|
// The exact D-Pad/Up line from the user's GCPadNew.ini.
|
||||||
|
auto dolphinLine = Compile("`Hat 0 N` | `Button 4` & timer(0.01)");
|
||||||
|
g_inputs["Hat 0 N"] = 0.0;
|
||||||
|
g_inputs["Button 4"] = 0.0;
|
||||||
|
Check(!Pressed(dolphinLine), "idle with nothing held");
|
||||||
|
g_inputs["Hat 0 N"] = 1.0;
|
||||||
|
Check(Pressed(dolphinLine), "hat alone presses");
|
||||||
|
g_inputs["Hat 0 N"] = 0.0;
|
||||||
|
g_inputs["Button 4"] = 1.0;
|
||||||
|
high = low = 0;
|
||||||
|
for (int i = 0; i < 60; ++i) {
|
||||||
|
(Pressed(dolphinLine) ? high : low)++;
|
||||||
|
Sleep(2);
|
||||||
|
}
|
||||||
|
Check(high > 5 && low > 5, "LB alternates via timer(0.01)");
|
||||||
|
|
||||||
|
// Regression tests for the CodeRabbit findings on PR #89.
|
||||||
|
g_inputs["A"] = 1.0;
|
||||||
|
// clamp with reversed bounds: std::clamp is UB when lo > hi.
|
||||||
|
Check(Compile("clamp(0.5, 1, 0)").Evaluate(Source()) == 0.5, "clamp tolerates reversed bounds");
|
||||||
|
// deadzone(v, 1) would divide by zero.
|
||||||
|
{
|
||||||
|
const double v = Compile("deadzone(`A`, 1)").Evaluate(Source());
|
||||||
|
Check(std::isfinite(v), "deadzone with dz=1 stays finite");
|
||||||
|
}
|
||||||
|
// timer with a zero or negative period would produce inf or NaN.
|
||||||
|
for (const char* text : {"timer(0)", "timer(-1)"}) {
|
||||||
|
const double v = Compile(text).Evaluate(Source());
|
||||||
|
Check(std::isfinite(v), std::string(text) + " stays finite");
|
||||||
|
}
|
||||||
|
// Any non-finite result is squashed before it can reach the uint8_t cast.
|
||||||
|
for (const char* text : {"sqrt(0 - 1)", "pow(10, 10000)", "tan(1.5707963267948966)"}) {
|
||||||
|
const double v = Compile(text).Evaluate(Source());
|
||||||
|
Check(std::isfinite(v), std::string(text) + " is sanitised at the boundary");
|
||||||
|
}
|
||||||
|
|
||||||
|
// tap count is user authored; negative, huge and non-finite must not reach
|
||||||
|
// the unsigned conversion.
|
||||||
|
for (const char* text : {"tap(`A`, 0.2, -1)", "tap(`A`, 0.2, 999999999)", "tap(`A`, 0.2, 0)"}) {
|
||||||
|
InputExpr::Expression e;
|
||||||
|
std::string err;
|
||||||
|
Check(InputExpr::Expression::Parse(text, e, err), std::string("parse ") + text);
|
||||||
|
const double v = e.Evaluate(Source());
|
||||||
|
Check(std::isfinite(v), std::string(text) + " evaluates without UB");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exponent notation is not part of the number syntax, matching Dolphin's
|
||||||
|
// lexer; it is rejected rather than silently misparsed.
|
||||||
|
{
|
||||||
|
InputExpr::Expression e;
|
||||||
|
std::string err;
|
||||||
|
Check(!InputExpr::Expression::Parse("tap(`A`, 0.2, 1e30)", e, err), "exponent notation rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A zero divisor must not skip the left subtree: stateful functions there
|
||||||
|
// still need their per-frame update.
|
||||||
|
{
|
||||||
|
auto divToggle = Compile("toggle(`D`) / `Z`");
|
||||||
|
g_inputs["Z"] = 0.0;
|
||||||
|
g_inputs["D"] = 0.0;
|
||||||
|
divToggle.Evaluate(Source());
|
||||||
|
g_inputs["D"] = 1.0;
|
||||||
|
divToggle.Evaluate(Source()); // rising edge seen even though rhs is 0
|
||||||
|
g_inputs["D"] = 0.0;
|
||||||
|
g_inputs["Z"] = 1.0;
|
||||||
|
Check(divToggle.Evaluate(Source()) > InputExpr::kConditionThreshold,
|
||||||
|
"toggle still latched while the divisor was zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
// smooth with a zero rate divides 0 by 0; NaN must not stick in the node.
|
||||||
|
{
|
||||||
|
auto sm = Compile("smooth(`A`, 0)");
|
||||||
|
g_inputs["A"] = 1.0;
|
||||||
|
sm.Evaluate(Source());
|
||||||
|
Sleep(5);
|
||||||
|
Check(std::isfinite(sm.Evaluate(Source())), "smooth with a zero rate stays finite");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Referenced inputs, used for diagnostics in the UI.
|
||||||
|
const auto refs = dolphinLine.ReferencedInputs();
|
||||||
|
Check(refs.size() == 2, "two referenced inputs");
|
||||||
|
|
||||||
|
// Errors are reported, not silently swallowed.
|
||||||
|
InputExpr::Expression bad;
|
||||||
|
std::string error;
|
||||||
|
Check(!InputExpr::Expression::Parse("`A` & ", bad, error), "trailing operator rejected");
|
||||||
|
Check(!InputExpr::Expression::Parse("nope(1)", bad, error), "unknown function rejected");
|
||||||
|
Check(!InputExpr::Expression::Parse("(`A`", bad, error), "missing paren rejected");
|
||||||
|
Check(!InputExpr::Expression::Parse("`A", bad, error), "unterminated backtick rejected");
|
||||||
|
Check(InputExpr::Expression::Parse("", bad, error) && bad.Empty(), "empty parses to empty");
|
||||||
|
Check(!InputExpr::Expression::Parse("hold(`A`)", bad, error), "wrong arg count rejected");
|
||||||
|
|
||||||
|
if (g_failures == 0) {
|
||||||
|
std::printf("all checks passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
std::printf("%d check(s) failed\n", g_failures);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||