mirror of
https://github.com/gopher64/gopher64.git
synced 2026-07-11 09:35:21 +02:00
Compare commits
263 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c2ae4dc5a | |||
| 0a884e85df | |||
| 47098f60dd | |||
| ca4a20f524 | |||
| 3b145909e8 | |||
| 6eb3367f1b | |||
| 03f83fe917 | |||
| c4771f91e0 | |||
| bdd12352e1 | |||
| 224bd2df1d | |||
| 412f928781 | |||
| 779ef9fe77 | |||
| 048d1d662f | |||
| cf3a27cecf | |||
| b8c3556df6 | |||
| 9e337295ca | |||
| 8cb57811a6 | |||
| 82c02461f8 | |||
| 26253b5ed7 | |||
| f520aeab82 | |||
| edb6960d4f | |||
| a488f0d91a | |||
| 73d895a64a | |||
| 805bcba249 | |||
| 66abd14c3b | |||
| d856499f59 | |||
| bc3dd4d39a | |||
| 5a91d09f9e | |||
| b09f8bf891 | |||
| 8ee10e40ad | |||
| f8a7153fdd | |||
| a592d0eb84 | |||
| 28aa8ea280 | |||
| cb87f79710 | |||
| be117214c3 | |||
| bb056a77f4 | |||
| d4a90155f9 | |||
| e24a9a8126 | |||
| 83fab4c638 | |||
| 8196b52289 | |||
| 8dadbbbf58 | |||
| 093b39b17d | |||
| 7100750e2d | |||
| 40e250386e | |||
| d2e47509b2 | |||
| 451d66beee | |||
| 20bad48894 | |||
| b40ec6940f | |||
| d12f16fe29 | |||
| d061a856d2 | |||
| 7725aa2692 | |||
| baa14de3f3 | |||
| c14b461612 | |||
| 595089187d | |||
| c4b86eb4b1 | |||
| 4240a99ad5 | |||
| 9d7db93c70 | |||
| 9b71d3aa0b | |||
| e8898c52fc | |||
| 4b5c8d03a9 | |||
| 2706f8f6a5 | |||
| e4391b3089 | |||
| 4b54ea279d | |||
| 9f6194d64f | |||
| 8bb3179534 | |||
| 35dbd957cc | |||
| 125f79a0e6 | |||
| f865ccfacd | |||
| 914798e717 | |||
| 406ebba93d | |||
| d8ec09d475 | |||
| a1da699580 | |||
| 1226052035 | |||
| 2917e662df | |||
| 601713e102 | |||
| 6ed53e4e9d | |||
| 650fd270e1 | |||
| 54d82713ac | |||
| 11cb242ed2 | |||
| e932239a63 | |||
| 5207bd4b27 | |||
| d378ae187a | |||
| e72ddf73f0 | |||
| 4bba36f0c3 | |||
| 20b5cf3e4d | |||
| 973f4bb28d | |||
| de0db4b46d | |||
| 11d3e52779 | |||
| cef70faf1e | |||
| 9b0b3e67a4 | |||
| ebd22415cd | |||
| 5177aaab53 | |||
| ad5dc8311a | |||
| cc538f942f | |||
| 9528473f11 | |||
| 8b770cfd18 | |||
| 46cb6738a2 | |||
| 9b8040873d | |||
| bff39ab04d | |||
| 86ce5795fa | |||
| a834a4f9a4 | |||
| 1a4de8e911 | |||
| 0f607e61aa | |||
| 6220a566eb | |||
| 5da6a6c3ee | |||
| 567dd3a79d | |||
| 4a0a80b045 | |||
| 74b087e77e | |||
| faadefe96e | |||
| 72df783d8c | |||
| e7dfd02e17 | |||
| c6703244b3 | |||
| 8291a93337 | |||
| 3e177ea4be | |||
| 6de1ef3548 | |||
| 73a3e6a22a | |||
| fcceca5976 | |||
| acf5e08b97 | |||
| 768fcdaf7f | |||
| 1c6e23b7f2 | |||
| b4ae7db3dd | |||
| e750b6eed1 | |||
| f8b777248a | |||
| 51e9b3aa55 | |||
| efbeaeab88 | |||
| f24a5e7f05 | |||
| 55170fcca3 | |||
| b952c45e0b | |||
| b13f37baf8 | |||
| db40ec5034 | |||
| cedb2d5c22 | |||
| 22d57677cb | |||
| f29f497405 | |||
| 0aab9f71e1 | |||
| 7f84eb2aff | |||
| b460d55714 | |||
| ceaf56c563 | |||
| 5937922558 | |||
| e9e71a6e02 | |||
| 9775f103e0 | |||
| bf01898c6f | |||
| ce3b12bba9 | |||
| 56ff2b260c | |||
| 367c22877b | |||
| befbacc5b3 | |||
| e1a6663bb5 | |||
| c5185e60c6 | |||
| 1aa879d759 | |||
| bfad71f09f | |||
| 431b787998 | |||
| d48e3dc106 | |||
| 8be1ac7740 | |||
| 56d0b1b2e2 | |||
| 7f63bf1fde | |||
| 05be960358 | |||
| 38a5079acf | |||
| 144e31c7fc | |||
| e7a21171f4 | |||
| 413da0178e | |||
| d8c6669da1 | |||
| c6fcfddf8c | |||
| 0aa5c3e63a | |||
| 6f8433138d | |||
| ea5bbfb218 | |||
| 10af598223 | |||
| c08511adc5 | |||
| f116617ac3 | |||
| 1141095b65 | |||
| 162a6df56d | |||
| 72f47e5aff | |||
| 6d40f5a12d | |||
| 64e6d75db1 | |||
| 20761faf8c | |||
| 79eb34d83d | |||
| c4057c2f21 | |||
| 581cc0b9aa | |||
| 6f6e21c718 | |||
| 181774a1de | |||
| 002fbbe8a4 | |||
| ab630cc8cb | |||
| 06f9c56a79 | |||
| c3f4b11519 | |||
| 921a0d5362 | |||
| 24fc5b9638 | |||
| 1559856483 | |||
| 1deb91fc1e | |||
| 726a58f854 | |||
| fe0297690f | |||
| 847df61526 | |||
| 6de8128f5b | |||
| 6c09176d12 | |||
| 2050098735 | |||
| d3ecc37da6 | |||
| a0237e9f7d | |||
| d6d608b674 | |||
| f254bec968 | |||
| bd4aff2b2c | |||
| 0bf70a77ea | |||
| e23e7f33c1 | |||
| 9d0516545e | |||
| e3023a902f | |||
| 431c7c2a8d | |||
| cf99539f85 | |||
| c2493b829e | |||
| 2f25de55d7 | |||
| 116ffe9845 | |||
| f3271cba63 | |||
| 9037680cda | |||
| fc41e26a6d | |||
| d1f28bb7a8 | |||
| 9db831fdef | |||
| 004b603e96 | |||
| 8661e2c9b1 | |||
| c712f2a288 | |||
| 366162e6fe | |||
| 2b93fb8aa1 | |||
| a191701610 | |||
| 0605967c28 | |||
| d6e6a16814 | |||
| e374825d5e | |||
| f6d6746207 | |||
| 100238f198 | |||
| daee53a626 | |||
| 018d8ca3e3 | |||
| 24d26d864e | |||
| 7b8c549e8e | |||
| f6c7e3d3c0 | |||
| 374b01dccf | |||
| 569b26a067 | |||
| 380d196ae7 | |||
| 4fa1a8ad39 | |||
| 8acf3b9de8 | |||
| b3297f82b2 | |||
| 2127705e7b | |||
| cccb079d63 | |||
| c52b680753 | |||
| 8799650245 | |||
| 90be2130c1 | |||
| 96af33c434 | |||
| 0a42152bcb | |||
| db335e24d2 | |||
| 4857b86897 | |||
| 26022fff72 | |||
| 658dede9e9 | |||
| 6ad1a31e34 | |||
| 4476d476de | |||
| 8037fb426e | |||
| f0381a2cd1 | |||
| c8e70e6125 | |||
| e9664b1087 | |||
| 3f72700da0 | |||
| 9fcd2835eb | |||
| c6767c94a6 | |||
| f1968e3a58 | |||
| 23fdfff1ac | |||
| fc512d9517 | |||
| 3c317460fd | |||
| b1fcaa5cf4 | |||
| 98c9cf142d | |||
| 1eb1503981 | |||
| 5aa9a65e7b | |||
| 71f1d98132 | |||
| 0037c0f344 |
+10
-3
@@ -1,16 +1,23 @@
|
||||
[target.'cfg(target_os = "linux")']
|
||||
[target.'cfg(all(target_arch = "aarch64", target_os = "linux"))']
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
|
||||
|
||||
[target.'cfg(target_os = "windows")']
|
||||
[target.'cfg(any(target_os = "windows", target_os = "macos"))']
|
||||
linker = "rust-lld"
|
||||
|
||||
[target.'cfg(target_os = "windows")']
|
||||
rustflags = ["-C", "link-arg=/STACK:8388608"]
|
||||
|
||||
[target.'cfg(target_arch = "x86_64")']
|
||||
rustflags = ["-C", "target-cpu=x86-64-v3"]
|
||||
|
||||
[target.'cfg(target_arch = "aarch64")']
|
||||
[target.'cfg(all(target_arch = "aarch64", target_os = "macos"))']
|
||||
rustflags = ["-C", "target-cpu=apple-m1"]
|
||||
|
||||
[target.'cfg(all(target_arch = "aarch64", not(target_os = "macos")))']
|
||||
rustflags = ["-C", "target-cpu=cortex-a76"]
|
||||
|
||||
[env]
|
||||
CC = "clang"
|
||||
CXX = "clang++"
|
||||
AR = "llvm-ar"
|
||||
SDKROOT = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"
|
||||
|
||||
@@ -3,7 +3,12 @@ updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "rust-toolchain"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
# - package-ecosystem: "cargo"
|
||||
# directory: "/"
|
||||
# schedule:
|
||||
|
||||
+103
-31
@@ -4,14 +4,15 @@ permissions:
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches: [ "main" ]
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
branches: [ "main" ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RA_HARDCORE: ${{ github.ref_type == 'tag' && 'true' || 'false' }}
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
@@ -29,42 +30,51 @@ jobs:
|
||||
arch: linux-aarch64
|
||||
llvm: ARM64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
rustflags: ""
|
||||
cache: ${{ github.ref_type != 'tag' }}
|
||||
- name: Set LLVM_VERSION
|
||||
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut -d':' -f2 | xargs)" >> $GITHUB_ENV
|
||||
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
|
||||
-d':' -f2 | xargs)" >> $GITHUB_ENV
|
||||
- name: Install SDL Dependencies
|
||||
# https://wiki.libsdl.org/SDL3/README-linux#build-dependencies
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1
|
||||
with:
|
||||
packages: build-essential git make \
|
||||
pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev \
|
||||
libaudio-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
|
||||
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
|
||||
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
|
||||
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev fcitx-libs-dev \
|
||||
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
|
||||
packages: build-essential git make pkg-config cmake ninja-build
|
||||
gnome-desktop-testing libasound2-dev libpulse-dev libaudio-dev
|
||||
libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev
|
||||
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev
|
||||
libxtst-dev libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev
|
||||
libgles2-mesa-dev libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev
|
||||
libudev-dev libthai-dev libpipewire-0.3-dev libwayland-dev
|
||||
libdecor-0-dev liburing-dev libfreetype-dev libfontconfig-dev
|
||||
version: ${{ matrix.target }}-1.0
|
||||
- name: Install LLVM and Clang
|
||||
uses: KyleMayes/install-llvm-action@v2
|
||||
with:
|
||||
version: ${{ env.LLVM_VERSION }}
|
||||
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{ env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-Linux-${{ matrix.llvm }}.tar.xz
|
||||
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
|
||||
env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-Linux-${{
|
||||
matrix.llvm }}.tar.xz
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build --release -v --target=${{ matrix.target }}
|
||||
mkdir output
|
||||
cp target/${{ matrix.target }}/release/gopher64 output/gopher64-${{ matrix.arch }}
|
||||
env:
|
||||
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
|
||||
- name: Upload file
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gopher64-${{ matrix.arch }}
|
||||
path: output/gopher64-${{ matrix.arch }}
|
||||
archive: false
|
||||
|
||||
build-windows:
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -76,18 +86,20 @@ jobs:
|
||||
target: x86_64-pc-windows-msvc
|
||||
arch: windows-x86_64
|
||||
llvm: win64
|
||||
#- os: windows-11-arm
|
||||
# target: aarch64-pc-windows-msvc
|
||||
# arch: windows-aarch64
|
||||
# llvm: woa64
|
||||
- os: windows-11-arm
|
||||
target: aarch64-pc-windows-msvc
|
||||
arch: windows-aarch64
|
||||
llvm: woa64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
rustflags: ""
|
||||
cache: ${{ github.ref_type != 'tag' }}
|
||||
- name: Set LLVM_VERSION
|
||||
run: |
|
||||
$llvm_version = rustc --version --verbose | Select-String "LLVM version" | ForEach-Object { $_.Line.Split(":")[1].Trim() }
|
||||
@@ -96,21 +108,28 @@ jobs:
|
||||
uses: KyleMayes/install-llvm-action@v2
|
||||
with:
|
||||
version: ${{ env.LLVM_VERSION }}
|
||||
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{ env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-${{ matrix.llvm }}.exe
|
||||
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
|
||||
env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-${{ matrix.llvm
|
||||
}}.exe
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build --release -v --target=${{ matrix.target }}
|
||||
mkdir output
|
||||
cp target/${{ matrix.target }}/release/gopher64.exe output/gopher64-${{ matrix.arch }}.exe
|
||||
env:
|
||||
CC: clang-cl
|
||||
CXX: clang-cl
|
||||
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
|
||||
- name: Upload unsigned artifact
|
||||
id: upload-unsigned-artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gopher64-${{ matrix.arch }}
|
||||
path: output/gopher64-${{ matrix.arch }}.exe
|
||||
archive: ${{ github.ref_type == 'tag' && true || false }}
|
||||
- name: SignPath
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
uses: signpath/github-action-submit-signing-request@v1.2
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
with:
|
||||
api-token: "${{ secrets.SIGNPATH_API_TOKEN }}"
|
||||
organization-id: "${{ secrets.SIGNPATH_ORG_ID }}"
|
||||
@@ -118,6 +137,17 @@ jobs:
|
||||
artifact-configuration-slug: "gopher64-${{ matrix.arch }}"
|
||||
signing-policy-slug: "release-signing"
|
||||
github-artifact-id: "${{ steps.upload-unsigned-artifact.outputs.artifact-id }}"
|
||||
wait-for-completion: true
|
||||
wait-for-completion-timeout-in-seconds: 3600
|
||||
output-artifact-directory: output-signed
|
||||
- name: Upload signed artifact
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gopher64-${{ matrix.arch }}
|
||||
path: output-signed/gopher64-${{ matrix.arch }}.exe
|
||||
archive: false
|
||||
overwrite: true
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-15
|
||||
@@ -125,27 +155,69 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-apple-darwin
|
||||
arch: mac-x86_64
|
||||
- target: aarch64-apple-darwin
|
||||
arch: mac-aarch64
|
||||
arch: macos-aarch64
|
||||
llvm: ARM64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
rustflags: ""
|
||||
cache: ${{ github.ref_type != 'tag' }}
|
||||
- name: Set LLVM_VERSION
|
||||
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
|
||||
-d':' -f2 | xargs)" >> $GITHUB_ENV
|
||||
- name: Install LLVM and Clang
|
||||
uses: KyleMayes/install-llvm-action@v2
|
||||
with:
|
||||
version: ${{ env.LLVM_VERSION }}
|
||||
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
|
||||
env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-macOS-${{
|
||||
matrix.llvm }}.tar.xz
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build --release -v --target=${{ matrix.target }}
|
||||
mkdir output
|
||||
cp target/${{ matrix.target }}/release/gopher64 output/gopher64-${{ matrix.arch }}
|
||||
cargo install cargo-bundle
|
||||
brew install molten-vk
|
||||
cargo build --release --no-default-features -v --target=${{ matrix.target }}
|
||||
mv target/${{ matrix.target }}/release/gopher64 target/${{ matrix.target }}/release/gopher64-cli
|
||||
cargo bundle --release --target=${{ matrix.target }} --format osx
|
||||
cp target/${{ matrix.target }}/release/gopher64-cli target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/MacOS/gopher64-cli
|
||||
install_name_tool -change /opt/homebrew/opt/freetype/lib/libfreetype.6.dylib \
|
||||
@executable_path/../Frameworks/libfreetype.6.dylib target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/MacOS/gopher64
|
||||
install_name_tool -change /opt/homebrew/opt/freetype/lib/libfreetype.6.dylib \
|
||||
@executable_path/../Frameworks/libfreetype.6.dylib target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/MacOS/gopher64-cli
|
||||
install_name_tool -change /opt/homebrew/opt/libpng/lib/libpng16.16.dylib \
|
||||
@executable_path/../Frameworks/libpng16.16.dylib target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/Frameworks/libfreetype.6.dylib
|
||||
env:
|
||||
AR: ar
|
||||
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
|
||||
MACOSX_DEPLOYMENT_TARGET: "15.7"
|
||||
- name: Sign Bundle Dev
|
||||
if: ${{ github.ref_type != 'tag' }}
|
||||
run: |
|
||||
codesign -f --entitlements data/macos/entitlements_dev.plist --deep --timestamp --options runtime -s - target/${{ matrix.target }}/release/bundle/osx/Gopher64.app
|
||||
- name: Import Codesign Certs
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
uses: apple-actions/import-codesign-certs@v6
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.APPLE_CERTIFICATES_FILE_BASE64 }}
|
||||
p12-password: ${{ secrets.APPLE_CERTIFICATES_PASSWORD }}
|
||||
- name: Sign Bundle Release
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
run: |
|
||||
codesign -f --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/Frameworks/libpng16.16.dylib
|
||||
codesign -f --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/Frameworks/libfreetype.6.dylib
|
||||
codesign -f --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/Frameworks/libMoltenVK.dylib
|
||||
codesign -f --entitlements data/macos/entitlements_child.plist --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/MacOS/gopher64-cli
|
||||
codesign -f --entitlements data/macos/entitlements_prod.plist --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/release/bundle/osx/Gopher64.app
|
||||
zip -r gopher64-${{ matrix.arch }}.zip target/${{ matrix.target }}/release/bundle/osx/Gopher64.app
|
||||
xcrun notarytool submit --wait --apple-id "${{ secrets.APPLE_ID }}" --team-id "${{ secrets.APPLE_DEVELOPER_ID }}" --password "${{ secrets.APPLE_APP_PASSWORD }}" gopher64-${{ matrix.arch }}.zip
|
||||
xcrun stapler staple target/${{ matrix.target }}/release/bundle/osx/Gopher64.app
|
||||
- name: Upload file
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gopher64-${{ matrix.arch }}
|
||||
path: output/gopher64-${{ matrix.arch }}
|
||||
path: target/${{ matrix.target }}/release/bundle/osx
|
||||
|
||||
+16
-12
@@ -2,9 +2,9 @@ name: Rust-lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
branches: [ "main" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -16,33 +16,37 @@ jobs:
|
||||
lint-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
components: clippy, rustfmt
|
||||
- name: Set LLVM_VERSION
|
||||
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut -d':' -f2 | xargs)" >> $GITHUB_ENV
|
||||
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
|
||||
-d':' -f2 | xargs)" >> $GITHUB_ENV
|
||||
- name: Install SDL Dependencies
|
||||
# https://wiki.libsdl.org/SDL3/README-linux#build-dependencies
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1
|
||||
with:
|
||||
packages: build-essential git make \
|
||||
pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev \
|
||||
libaudio-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
|
||||
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
|
||||
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
|
||||
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev fcitx-libs-dev \
|
||||
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
|
||||
packages: build-essential git make pkg-config cmake ninja-build
|
||||
gnome-desktop-testing libasound2-dev libpulse-dev libaudio-dev
|
||||
libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev
|
||||
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev
|
||||
libxtst-dev libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev
|
||||
libgles2-mesa-dev libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev
|
||||
libudev-dev libthai-dev libpipewire-0.3-dev libwayland-dev
|
||||
libdecor-0-dev liburing-dev libfreetype-dev libfontconfig-dev
|
||||
version: lint-1.0
|
||||
- name: Install LLVM and Clang
|
||||
uses: KyleMayes/install-llvm-action@v2
|
||||
with:
|
||||
version: ${{ env.LLVM_VERSION }}
|
||||
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{ env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-Linux-X64.tar.xz
|
||||
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
|
||||
env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-Linux-X64.tar.xz
|
||||
- name: Rustfmt Check
|
||||
uses: actions-rust-lang/rustfmt@v1
|
||||
- name: Lint
|
||||
run: |
|
||||
cargo clippy -- -Dwarnings
|
||||
cargo clippy --no-default-features -- -Dwarnings
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
checks: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: rustsec/audit-check@v2
|
||||
|
||||
+9
-2
@@ -1,4 +1,11 @@
|
||||
[submodule "parallel-rdp/parallel-rdp-standalone"]
|
||||
path = parallel-rdp/parallel-rdp-standalone
|
||||
url = https://github.com/gopher64/parallel-rdp-standalone.git
|
||||
branch = gopher64
|
||||
url = https://github.com/Themaister/parallel-rdp-standalone.git
|
||||
branch = master
|
||||
[submodule "src/compat/sse2neon"]
|
||||
path = src/compat/sse2neon
|
||||
url = https://github.com/DLTcollab/sse2neon.git
|
||||
branch = master
|
||||
[submodule "retroachievements/rcheevos"]
|
||||
path = retroachievements/rcheevos
|
||||
url = https://github.com/RetroAchievements/rcheevos.git
|
||||
|
||||
Generated
+2505
-1543
File diff suppressed because it is too large
Load Diff
+32
-16
@@ -1,47 +1,63 @@
|
||||
[package]
|
||||
name = "gopher64"
|
||||
version = "1.1.2"
|
||||
version = "1.1.18"
|
||||
edition = "2024"
|
||||
rust-version = "1.88"
|
||||
rust-version = "1.95.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[features]
|
||||
default = ["gui"]
|
||||
gui = ["dep:rfd", "dep:slint", "dep:slint-build"]
|
||||
|
||||
[dependencies]
|
||||
dirs = "6.0"
|
||||
zip = "4.3"
|
||||
governor = "0.10"
|
||||
sevenz-rust2 = { version = "0.17", default-features = false }
|
||||
zip = "8.0"
|
||||
sevenz-rust2 = "0.20"
|
||||
chrono = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde-big-array = "0.5"
|
||||
slint = { version = "1.12", default-features = false, features = ["compat-1-2", "std", "backend-winit", "renderer-femtovg"] }
|
||||
slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "unstable-winit-030", "backend-winit", "renderer-skia", "accessibility"], optional = true }
|
||||
open = "5.3"
|
||||
sha2 = "0.10"
|
||||
ab_glyph = "0.2"
|
||||
sdl3-sys = { version = "0.5", features = ["build-from-source-static"] }
|
||||
rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "tokio"] }
|
||||
tokio = {version = "1.46", features = ["rt-multi-thread", "macros"] }
|
||||
sha2 = "0.11"
|
||||
sdl3-sys = { version = "0.6", features = ["build-from-source-static"] }
|
||||
sdl3-ttf-sys = { version = "0.6", features = ["build-from-source-static", "no-sdlttf-harfbuzz", "no-sdlttf-plutosvg"] }
|
||||
rfd = {version = "0.17", optional = true }
|
||||
tokio = {version = "1.46", features = ["rt-multi-thread", "macros", "fs", "process"] }
|
||||
spin_sleep = "1.3"
|
||||
encoding_rs = "0.8"
|
||||
mimalloc = "0.1"
|
||||
regex = "1.11"
|
||||
tokio-tungstenite = "0.27"
|
||||
tokio-tungstenite = "0.29"
|
||||
futures = "0.3"
|
||||
socket2 = { version = "0.6", features = ["all"] }
|
||||
rand_chacha = "0.9"
|
||||
rand = "0.10"
|
||||
fatfs = "0.3"
|
||||
semver = "1.0"
|
||||
postcard = { version = "1.1", features = ["use-std"] }
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["http2","rustls-tls","charset","json"] }
|
||||
reqwest = { version = "0.13", features = ["json", "query"] }
|
||||
|
||||
[build-dependencies]
|
||||
winresource = "0.1"
|
||||
bindgen = "0.72"
|
||||
slint-build = "1.12"
|
||||
slint-build = { version = "1.12", optional = true }
|
||||
cc = { version = "1.2", features = ["parallel"] }
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
lto = "thin"
|
||||
strip = true
|
||||
|
||||
[profile.release-with-debug]
|
||||
inherits = "release"
|
||||
debug = true
|
||||
strip = false
|
||||
panic = "unwind"
|
||||
|
||||
[package.metadata.bundle]
|
||||
name = "Gopher64"
|
||||
identifier = "io.github.gopher64.gopher64"
|
||||
category = "public.app-category.games"
|
||||
icon = ["data/icon/gopher64_128x128.png","data/icon/gopher64_256x256.png","data/icon/gopher64_512x512.png"]
|
||||
osx_frameworks = ["/opt/homebrew/opt/molten-vk/lib/libMoltenVK.dylib","/opt/homebrew/opt/freetype/lib/libfreetype.6.dylib","/opt/homebrew/opt/libpng/lib/libpng16.16.dylib"]
|
||||
osx_minimum_system_version = "15.7"
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
# gopher64
|
||||
Gopher64 is a cross-platform N64 emulator. Some notable features:
|
||||
* Netplay
|
||||
* Homebrew support
|
||||
* Upscaling
|
||||
* CRT shader
|
||||
* Emulate CPU overclocking
|
||||
* Cheats
|
||||
* Savestates
|
||||
* RetroAchievements
|
||||
|
||||
## download
|
||||
|
||||
Windows: https://github.com/gopher64/gopher64/releases/latest/download/gopher64-windows-x86_64.exe
|
||||
|
||||
MacOS: https://github.com/gopher64/gopher64/releases/latest/download/gopher64-macos-aarch64.zip
|
||||
|
||||
Linux: https://flathub.org/apps/io.github.gopher64.gopher64
|
||||
|
||||
## wiki
|
||||
@@ -11,7 +23,7 @@ https://github.com/gopher64/gopher64/wiki
|
||||
|
||||
## discord
|
||||
|
||||
https://discord.gg/9RGXq8W8JQ
|
||||
[Discord invite link](https://discord.gg/9RGXq8W8JQ)
|
||||
|
||||
## controls
|
||||
|
||||
@@ -19,7 +31,7 @@ Keys are mapped according to [these defaults](https://github.com/gopher64/gopher
|
||||
|
||||
## netplay
|
||||
|
||||
Gopher64 supports netplay (online play with others) via cloud hosted servers. You can also run the server (https://github.com/gopher64/gopher64-netplay-server) yourself on a LAN.
|
||||
Gopher64 supports netplay (online play with others) via cloud hosted servers. You can also run the [server](https://github.com/gopher64/gopher64-netplay-server) yourself on a LAN.
|
||||
|
||||
## portable mode
|
||||
|
||||
@@ -35,8 +47,8 @@ flatpak run --filesystem=host:ro io.github.gopher64.gopher64 /path/to/rom.z64
|
||||
|
||||
## building and usage
|
||||
|
||||
1. Linux only: install the SDL3 dependencies: https://wiki.libsdl.org/SDL3/README-linux#build-dependencies
|
||||
2. Install rust: https://www.rust-lang.org/tools/install
|
||||
1. Linux only: [install the SDL3 dependencies](https://wiki.libsdl.org/SDL3/README-linux#build-dependencies)
|
||||
2. [Install rust](https://www.rust-lang.org/tools/install)
|
||||
3. `git clone --recursive https://github.com/gopher64/gopher64.git`
|
||||
4. `cd gopher64`
|
||||
5. `cargo build --release`
|
||||
@@ -48,10 +60,12 @@ I am very open to contributions! Please contact me via a GitHub issue or Discord
|
||||
|
||||
## license
|
||||
|
||||
Gopher64 is licensed under the GPLv3 license. Many portions of gopher64 have been adapted from mupen64plus and/or ares. The license for mupen64plus can be found here: https://github.com/mupen64plus/mupen64plus-core/blob/master/LICENSES. The license for ares can be found here: https://github.com/ares-emulator/ares/blob/master/LICENSE.
|
||||
Gopher64 is licensed under the GPLv3 license. Many portions of gopher64 have been adapted from mupen64plus and/or ares. The license for mupen64plus can be found [here](https://github.com/mupen64plus/mupen64plus-core/blob/master/LICENSES). The license for ares can be found [here](https://github.com/ares-emulator/ares/blob/master/LICENSE).
|
||||
|
||||
## privacy and code signing policy
|
||||
|
||||
Free code signing for the Windows release is provided by [SignPath.io](https://about.signpath.io), certificate by [SignPath Foundation](https://signpath.org).
|
||||
|
||||
During online netplay sessions, the server logs your IP address and basic session information (game title and session name) for operational purposes. No additional personal data is collected or stored.
|
||||
|
||||
If you enable the RetroAchievements feature, some data is sent to their systems. Please see their terms [here](https://retroachievements.org/terms).
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
fn main() {
|
||||
println!("cargo::rerun-if-changed=parallel-rdp");
|
||||
println!("cargo::rerun-if-changed=retroachievements");
|
||||
println!("cargo::rerun-if-changed=src/compat");
|
||||
|
||||
let slint_config = slint_build::CompilerConfiguration::new().with_style("cosmic".into());
|
||||
slint_build::compile_with_config("src/ui/gui/appwindow.slint", slint_config).unwrap();
|
||||
#[cfg(feature = "gui")]
|
||||
{
|
||||
let slint_config = slint_build::CompilerConfiguration::new();
|
||||
slint_build::compile_with_config("src/ui/gui/appwindow.slint", slint_config).unwrap();
|
||||
}
|
||||
|
||||
let mut simd_build = cc::Build::new();
|
||||
let mut volk_build = cc::Build::new();
|
||||
@@ -14,7 +18,7 @@ fn main() {
|
||||
let mut rdp_build = cc::Build::new();
|
||||
rdp_build
|
||||
.cpp(true)
|
||||
.std("c++23")
|
||||
.std("c++20")
|
||||
.flag("-Wno-unused-parameter")
|
||||
.flag("-Wno-missing-field-initializers")
|
||||
.file("parallel-rdp/parallel-rdp-standalone/parallel-rdp/command_ring.cpp")
|
||||
@@ -36,7 +40,6 @@ fn main() {
|
||||
.file("parallel-rdp/parallel-rdp-standalone/vulkan/image.cpp")
|
||||
.file("parallel-rdp/parallel-rdp-standalone/vulkan/indirect_layout.cpp")
|
||||
.file("parallel-rdp/parallel-rdp-standalone/vulkan/memory_allocator.cpp")
|
||||
.file("parallel-rdp/parallel-rdp-standalone/vulkan/pipeline_cache.cpp")
|
||||
.file("parallel-rdp/parallel-rdp-standalone/vulkan/pipeline_event.cpp")
|
||||
.file("parallel-rdp/parallel-rdp-standalone/vulkan/query_pool.cpp")
|
||||
.file("parallel-rdp/parallel-rdp-standalone/vulkan/render_pass.cpp")
|
||||
@@ -62,16 +65,55 @@ fn main() {
|
||||
.include("parallel-rdp/parallel-rdp-standalone/vulkan-headers/include")
|
||||
.include("parallel-rdp/parallel-rdp-standalone/util")
|
||||
.include(
|
||||
std::path::PathBuf::from(std::env::var("DEP_SDL3_OUT_DIR").to_owned().unwrap())
|
||||
std::path::PathBuf::from(std::env::var("DEP_SDL3_OUT_DIR").unwrap()).join("include"),
|
||||
)
|
||||
.include(
|
||||
std::path::PathBuf::from(std::env::var("DEP_SDL3_TTF_OUT_DIR").unwrap())
|
||||
.join("include"),
|
||||
);
|
||||
|
||||
let mut retroachievements_build = cc::Build::new();
|
||||
retroachievements_build
|
||||
.flag("-Wno-unused-parameter")
|
||||
.include("retroachievements/rcheevos/include")
|
||||
.flag("-DRC_CLIENT_SUPPORTS_HASH")
|
||||
.file("retroachievements/rcheevos/src/rc_client.c")
|
||||
.file("retroachievements/rcheevos/src/rc_compat.c")
|
||||
.file("retroachievements/rcheevos/src/rc_util.c")
|
||||
.file("retroachievements/rcheevos/src/rcheevos/alloc.c")
|
||||
.file("retroachievements/rcheevos/src/rcheevos/condition.c")
|
||||
.file("retroachievements/rcheevos/src/rcheevos/condset.c")
|
||||
.file("retroachievements/rcheevos/src/rcheevos/consoleinfo.c")
|
||||
.file("retroachievements/rcheevos/src/rcheevos/format.c")
|
||||
.file("retroachievements/rcheevos/src/rcheevos/lboard.c")
|
||||
.file("retroachievements/rcheevos/src/rcheevos/memref.c")
|
||||
.file("retroachievements/rcheevos/src/rcheevos/operand.c")
|
||||
.file("retroachievements/rcheevos/src/rcheevos/richpresence.c")
|
||||
.file("retroachievements/rcheevos/src/rcheevos/runtime.c")
|
||||
.file("retroachievements/rcheevos/src/rcheevos/runtime_progress.c")
|
||||
.file("retroachievements/rcheevos/src/rcheevos/trigger.c")
|
||||
.file("retroachievements/rcheevos/src/rcheevos/value.c")
|
||||
.file("retroachievements/rcheevos/src/rapi/rc_api_common.c")
|
||||
.file("retroachievements/rcheevos/src/rapi/rc_api_info.c")
|
||||
.file("retroachievements/rcheevos/src/rapi/rc_api_runtime.c")
|
||||
.file("retroachievements/rcheevos/src/rapi/rc_api_user.c")
|
||||
.file("retroachievements/rcheevos/src/rhash/aes.c")
|
||||
.file("retroachievements/rcheevos/src/rhash/cdreader.c")
|
||||
.file("retroachievements/rcheevos/src/rhash/md5.c")
|
||||
.file("retroachievements/rcheevos/src/rhash/hash.c")
|
||||
.file("retroachievements/rcheevos/src/rhash/hash_disc.c")
|
||||
.file("retroachievements/rcheevos/src/rhash/hash_encrypted.c")
|
||||
.file("retroachievements/rcheevos/src/rhash/hash_rom.c")
|
||||
.file("retroachievements/rcheevos/src/rhash/hash_zip.c")
|
||||
.file("retroachievements/retroachievements.c");
|
||||
|
||||
let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||
let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
|
||||
let profile = std::env::var("PROFILE").unwrap();
|
||||
let opt_flag = if arch == "x86_64" {
|
||||
"-march=x86-64-v3"
|
||||
} else if arch == "aarch64" {
|
||||
} else if arch == "aarch64" && os == "macos" {
|
||||
"-march=armv8.4-a"
|
||||
} else if arch == "aarch64" && os != "macos" {
|
||||
"-march=armv8.2-a"
|
||||
} else {
|
||||
panic!("unknown arch")
|
||||
@@ -80,6 +122,7 @@ fn main() {
|
||||
volk_build.flag(opt_flag);
|
||||
rdp_build.flag(opt_flag);
|
||||
simd_build.flag(opt_flag);
|
||||
retroachievements_build.flag(opt_flag);
|
||||
|
||||
if os == "windows" {
|
||||
volk_build.flag("-DVK_USE_PLATFORM_WIN32_KHR");
|
||||
@@ -89,30 +132,85 @@ fn main() {
|
||||
.set_icon("data/icon/icon.ico")
|
||||
.compile()
|
||||
.unwrap();
|
||||
} else if os == "macos" {
|
||||
println!("cargo:rustc-link-search=native=/opt/homebrew/opt/freetype/lib");
|
||||
println!("cargo:rustc-link-lib=dylib=freetype");
|
||||
|
||||
let output = std::process::Command::new("clang")
|
||||
.args(["--print-runtime-dir"])
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let runtime_dir = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
|
||||
println!("cargo:rustc-link-search=native={}", runtime_dir);
|
||||
println!("cargo:rustc-link-lib=static=clang_rt.osx");
|
||||
}
|
||||
if cfg!(not(feature = "gui")) && os == "linux" {
|
||||
println!("cargo:rustc-link-lib=dylib=freetype");
|
||||
}
|
||||
|
||||
if profile == "release" {
|
||||
volk_build.flag("-flto=thin");
|
||||
rdp_build.flag("-flto=thin");
|
||||
simd_build.flag("-flto=thin");
|
||||
}
|
||||
volk_build.flag("-flto=thin");
|
||||
rdp_build.flag("-flto=thin");
|
||||
simd_build.flag("-flto=thin");
|
||||
retroachievements_build.flag("-flto=thin");
|
||||
|
||||
volk_build.compile("volk");
|
||||
rdp_build.compile("parallel-rdp");
|
||||
retroachievements_build.compile("retroachievements");
|
||||
|
||||
let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
|
||||
|
||||
let mut retroachievements_builder = bindgen::Builder::default()
|
||||
.header("retroachievements/retroachievements.h")
|
||||
.allowlist_function("ra_init_client")
|
||||
.allowlist_function("ra_welcome")
|
||||
.allowlist_function("ra_shutdown_client")
|
||||
.allowlist_function("ra_get_hardcore")
|
||||
.allowlist_function("ra_load_game")
|
||||
.allowlist_function("ra_unload_game")
|
||||
.allowlist_function("ra_set_dmem")
|
||||
.allowlist_function("ra_do_frame")
|
||||
.allowlist_function("ra_do_idle")
|
||||
.allowlist_function("ra_http_callback")
|
||||
.allowlist_function("ra_login_user")
|
||||
.allowlist_function("ra_login_token_user")
|
||||
.allowlist_function("ra_state_size")
|
||||
.allowlist_function("ra_save_state")
|
||||
.allowlist_function("ra_load_state");
|
||||
|
||||
if cfg!(feature = "gui") {
|
||||
retroachievements_builder = retroachievements_builder
|
||||
.allowlist_function("ra_logout_user")
|
||||
.allowlist_function("ra_get_username")
|
||||
.allowlist_function("ra_get_token");
|
||||
}
|
||||
|
||||
let retroachievements_bindings = retroachievements_builder
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
.generate()
|
||||
.expect("Unable to generate bindings");
|
||||
|
||||
retroachievements_bindings
|
||||
.write_to_file(out_path.join("retroachievements_bindings.rs"))
|
||||
.expect("Couldn't write bindings!");
|
||||
|
||||
let parallel_bindings = bindgen::Builder::default()
|
||||
.header("parallel-rdp/interface.hpp")
|
||||
.allowlist_function("rdp_init")
|
||||
.allowlist_function("rdp_close")
|
||||
.allowlist_function("rdp_set_vi_register")
|
||||
.allowlist_function("rdp_update_screen")
|
||||
.allowlist_function("rdp_render_frame")
|
||||
.allowlist_function("rdp_process_commands")
|
||||
.allowlist_function("rdp_onscreen_message")
|
||||
.allowlist_function("rdp_check_callback")
|
||||
.allowlist_function("rdp_new_processor")
|
||||
.allowlist_function("rdp_check_framebuffers")
|
||||
.allowlist_function("rdp_state_size")
|
||||
.allowlist_function("rdp_save_state")
|
||||
.allowlist_function("rdp_load_state")
|
||||
.allowlist_function("rdp_set_fps")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
.generate()
|
||||
.expect("Unable to generate bindings");
|
||||
@@ -123,7 +221,7 @@ fn main() {
|
||||
|
||||
if arch == "aarch64" {
|
||||
let simd_bindings = bindgen::Builder::default()
|
||||
.header("src/compat/sse2neon/v1.8.0/sse2neon.h")
|
||||
.header("src/compat/sse2neon/sse2neon.h")
|
||||
.allowlist_function("_mm_setzero_si128")
|
||||
.allowlist_function("_mm_set_epi8")
|
||||
.allowlist_function("_mm_movemask_epi8")
|
||||
@@ -157,6 +255,7 @@ fn main() {
|
||||
.allowlist_function("_mm_cmplt_epi16")
|
||||
.allowlist_function("_mm_subs_epu16")
|
||||
.allowlist_function("_mm_set1_epi32")
|
||||
.allowlist_function("_mm_set1_epi16")
|
||||
.blocklist_type("__m128i")
|
||||
.blocklist_type("int64x2_t")
|
||||
.wrap_static_fns(true)
|
||||
@@ -178,13 +277,35 @@ fn main() {
|
||||
.compile("simd");
|
||||
}
|
||||
|
||||
let git_output = std::process::Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.unwrap();
|
||||
let mut git_output = std::process::Command::new("git");
|
||||
git_output.args(["describe", "--dirty"]);
|
||||
|
||||
let git_hash = String::from_utf8(git_output.stdout).unwrap();
|
||||
println!("cargo:rustc-env=GIT_HASH={git_hash}");
|
||||
let git_describe = if let Ok(git_output) = git_output.output()
|
||||
&& git_output.status.success()
|
||||
{
|
||||
String::from_utf8(git_output.stdout).unwrap()
|
||||
} else if let Ok(git_output) = git_output.args(["--always"]).output()
|
||||
&& git_output.status.success()
|
||||
{
|
||||
format!(
|
||||
"v{}-{}",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
String::from_utf8(git_output.stdout).unwrap()
|
||||
)
|
||||
} else {
|
||||
panic!("Failed to get git describe");
|
||||
};
|
||||
println!("cargo:rustc-env=GIT_DESCRIBE={git_describe}");
|
||||
|
||||
println!("cargo:rustc-env=N64_STACK_SIZE={}", 8 * 1024 * 1024);
|
||||
println!("cargo:rerun-if-env-changed=NETPLAY_ID");
|
||||
let netplay_id = std::env::var("NETPLAY_ID").unwrap_or("gopher64".to_string());
|
||||
println!("cargo:rustc-env=NETPLAY_ID={netplay_id}");
|
||||
|
||||
println!("cargo:rerun-if-env-changed=RA_HARDCORE");
|
||||
println!("cargo:rustc-check-cfg=cfg(ra_hardcore_enabled)");
|
||||
if let Ok(ra_hardcore) = std::env::var("RA_HARDCORE")
|
||||
&& ra_hardcore.to_lowercase() == "true"
|
||||
{
|
||||
println!("cargo:rustc-cfg=ra_hardcore_enabled");
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
<?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>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.inherit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?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>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?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>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.usb</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.bluetooth</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+846
-496
File diff suppressed because it is too large
Load Diff
+59
-38
@@ -4,48 +4,69 @@
|
||||
#include <cstdint>
|
||||
#include <stddef.h>
|
||||
|
||||
extern "C"
|
||||
{
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint8_t *RDRAM;
|
||||
uint8_t *DMEM;
|
||||
uint32_t RDRAM_SIZE;
|
||||
uint32_t *DPC_CURRENT_REG;
|
||||
uint32_t *DPC_START_REG;
|
||||
uint32_t *DPC_END_REG;
|
||||
uint32_t *DPC_STATUS_REG;
|
||||
bool PAL;
|
||||
bool widescreen;
|
||||
bool fullscreen;
|
||||
bool integer_scaling;
|
||||
uint32_t upscale;
|
||||
bool crt;
|
||||
} GFX_INFO;
|
||||
typedef struct {
|
||||
uint8_t *RDRAM;
|
||||
uint8_t *DMEM;
|
||||
uint32_t RDRAM_SIZE;
|
||||
uint32_t *DPC_CURRENT_REG;
|
||||
uint32_t *DPC_START_REG;
|
||||
uint32_t *DPC_END_REG;
|
||||
uint32_t *DPC_STATUS_REG;
|
||||
bool PAL;
|
||||
bool widescreen;
|
||||
bool fullscreen;
|
||||
bool vsync;
|
||||
bool integer_scaling;
|
||||
uint32_t upscale;
|
||||
bool crt;
|
||||
} GFX_INFO;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
bool emu_running;
|
||||
bool save_state;
|
||||
bool load_state;
|
||||
bool enable_speedlimiter;
|
||||
bool lower_volume;
|
||||
bool raise_volume;
|
||||
bool paused;
|
||||
} CALL_BACK;
|
||||
typedef struct {
|
||||
bool emu_running;
|
||||
bool save_state;
|
||||
bool load_state;
|
||||
bool reset_game;
|
||||
bool enable_speedlimiter;
|
||||
bool lower_volume;
|
||||
bool raise_volume;
|
||||
bool paused;
|
||||
bool frame_advance;
|
||||
uint32_t save_state_slot;
|
||||
} CALL_BACK;
|
||||
|
||||
void rdp_init(void *_window, GFX_INFO _gfx_info);
|
||||
void rdp_close();
|
||||
void rdp_set_vi_register(uint32_t reg, uint32_t value);
|
||||
void rdp_update_screen();
|
||||
CALL_BACK rdp_check_callback();
|
||||
uint64_t rdp_process_commands();
|
||||
void rdp_new_processor(GFX_INFO _gfx_info);
|
||||
size_t rdp_state_size();
|
||||
void rdp_save_state(uint8_t *state);
|
||||
void rdp_load_state(const uint8_t *state);
|
||||
typedef enum {
|
||||
MESSAGE_VERY_SHORT = 500,
|
||||
MESSAGE_SHORT = 3000,
|
||||
MESSAGE_LONG = 6000,
|
||||
} MESSAGE_LENGTH;
|
||||
|
||||
void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
|
||||
size_t font_size, uint32_t save_state_slot);
|
||||
void rdp_close();
|
||||
void rdp_set_vi_register(uint32_t reg, uint32_t value);
|
||||
void rdp_update_screen();
|
||||
void rdp_render_frame();
|
||||
CALL_BACK rdp_check_callback();
|
||||
uint64_t rdp_process_commands();
|
||||
void rdp_onscreen_message(const char *message, MESSAGE_LENGTH milliseconds);
|
||||
void rdp_new_processor(GFX_INFO _gfx_info);
|
||||
void rdp_check_framebuffers(uint32_t address, uint32_t length);
|
||||
size_t rdp_state_size();
|
||||
void rdp_save_state(uint8_t *state);
|
||||
void rdp_load_state(const uint8_t *state);
|
||||
void rdp_set_fps(uint32_t fps, uint32_t vis);
|
||||
|
||||
void achievement_challenge_indicator_add(const char *achievement_title);
|
||||
void achievement_challenge_indicator_remove(const char *achievement_title);
|
||||
void achievement_progress_add(const char *achievement_title,
|
||||
const char *progress);
|
||||
void achievement_progress_remove();
|
||||
void leaderboard_tracker_add(uint32_t id, const char *title,
|
||||
const char *display);
|
||||
void leaderboard_tracker_remove(uint32_t id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
Submodule parallel-rdp/parallel-rdp-standalone updated: 3bff34d835...388d70f583
@@ -1,73 +1,55 @@
|
||||
#include "wsi_platform.hpp"
|
||||
#include <SDL3/SDL_vulkan.h>
|
||||
|
||||
VkSurfaceKHR SDL_WSIPlatform::create_surface(VkInstance instance, VkPhysicalDevice gpu)
|
||||
{
|
||||
VkSurfaceKHR surface = nullptr;
|
||||
bool result = SDL_Vulkan_CreateSurface(window, instance, NULL, &surface);
|
||||
if (result != true)
|
||||
{
|
||||
printf("Error creating surface\n");
|
||||
}
|
||||
return surface;
|
||||
VkSurfaceKHR SDL_WSIPlatform::create_surface(VkInstance instance,
|
||||
VkPhysicalDevice gpu) {
|
||||
VkSurfaceKHR surface = nullptr;
|
||||
bool result = SDL_Vulkan_CreateSurface(window, instance, NULL, &surface);
|
||||
if (result != true) {
|
||||
printf("Error creating surface\n");
|
||||
}
|
||||
return surface;
|
||||
}
|
||||
|
||||
void SDL_WSIPlatform::destroy_surface(VkInstance instance, VkSurfaceKHR surface)
|
||||
{
|
||||
SDL_Vulkan_DestroySurface(instance, surface, NULL);
|
||||
void SDL_WSIPlatform::destroy_surface(VkInstance instance,
|
||||
VkSurfaceKHR surface) {
|
||||
SDL_Vulkan_DestroySurface(instance, surface, NULL);
|
||||
}
|
||||
|
||||
std::vector<const char *> SDL_WSIPlatform::get_instance_extensions()
|
||||
{
|
||||
std::vector<const char *> SDL_WSIPlatform::get_instance_extensions() {
|
||||
|
||||
unsigned int extensionCount = 0;
|
||||
char const *const *extensions = SDL_Vulkan_GetInstanceExtensions(&extensionCount);
|
||||
if (extensions == NULL)
|
||||
{
|
||||
printf("Error getting instance extensions\n");
|
||||
}
|
||||
unsigned int extensionCount = 0;
|
||||
char const *const *extensions =
|
||||
SDL_Vulkan_GetInstanceExtensions(&extensionCount);
|
||||
if (extensions == NULL) {
|
||||
printf("Error getting instance extensions\n");
|
||||
}
|
||||
|
||||
std::vector<const char *> extensionNames;
|
||||
for (unsigned int i = 0; i < extensionCount; ++i)
|
||||
{
|
||||
extensionNames.push_back(extensions[i]);
|
||||
}
|
||||
return extensionNames;
|
||||
std::vector<const char *> extensionNames;
|
||||
for (unsigned int i = 0; i < extensionCount; ++i) {
|
||||
extensionNames.push_back(extensions[i]);
|
||||
}
|
||||
return extensionNames;
|
||||
}
|
||||
|
||||
uint32_t SDL_WSIPlatform::get_surface_width()
|
||||
{
|
||||
int w, h;
|
||||
SDL_GetWindowSize(window, &w, &h);
|
||||
return w;
|
||||
uint32_t SDL_WSIPlatform::get_surface_width() {
|
||||
int w, h;
|
||||
SDL_GetWindowSize(window, &w, &h);
|
||||
return w;
|
||||
}
|
||||
|
||||
uint32_t SDL_WSIPlatform::get_surface_height()
|
||||
{
|
||||
int w, h;
|
||||
SDL_GetWindowSize(window, &w, &h);
|
||||
return h;
|
||||
uint32_t SDL_WSIPlatform::get_surface_height() {
|
||||
int w, h;
|
||||
SDL_GetWindowSize(window, &w, &h);
|
||||
return h;
|
||||
}
|
||||
|
||||
bool SDL_WSIPlatform::alive(Vulkan::WSI &wsi)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
bool SDL_WSIPlatform::alive(Vulkan::WSI &wsi) { return true; }
|
||||
|
||||
void SDL_WSIPlatform::poll_input()
|
||||
{
|
||||
}
|
||||
void SDL_WSIPlatform::poll_input() {}
|
||||
|
||||
void SDL_WSIPlatform::poll_input_async(Granite::InputTrackerHandler *handler)
|
||||
{
|
||||
}
|
||||
void SDL_WSIPlatform::poll_input_async(Granite::InputTrackerHandler *handler) {}
|
||||
|
||||
void SDL_WSIPlatform::set_window(SDL_Window *_window)
|
||||
{
|
||||
window = _window;
|
||||
}
|
||||
void SDL_WSIPlatform::set_window(SDL_Window *_window) { window = _window; }
|
||||
|
||||
void SDL_WSIPlatform::do_resize()
|
||||
{
|
||||
resize = true;
|
||||
}
|
||||
void SDL_WSIPlatform::do_resize() { resize = true; }
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
#include "wsi.hpp"
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
class SDL_WSIPlatform : public Vulkan::WSIPlatform
|
||||
{
|
||||
class SDL_WSIPlatform : public Vulkan::WSIPlatform {
|
||||
public:
|
||||
VkSurfaceKHR create_surface(VkInstance instance, VkPhysicalDevice gpu) override;
|
||||
void destroy_surface(VkInstance instance, VkSurfaceKHR surface) override;
|
||||
std::vector<const char *> get_instance_extensions() override;
|
||||
uint32_t get_surface_width() override;
|
||||
uint32_t get_surface_height() override;
|
||||
bool alive(Vulkan::WSI &wsi) override;
|
||||
void poll_input() override;
|
||||
void poll_input_async(Granite::InputTrackerHandler *handler) override;
|
||||
void set_window(SDL_Window *_window);
|
||||
void do_resize();
|
||||
VkSurfaceKHR create_surface(VkInstance instance,
|
||||
VkPhysicalDevice gpu) override;
|
||||
void destroy_surface(VkInstance instance, VkSurfaceKHR surface) override;
|
||||
std::vector<const char *> get_instance_extensions() override;
|
||||
uint32_t get_surface_width() override;
|
||||
uint32_t get_surface_height() override;
|
||||
bool alive(Vulkan::WSI &wsi) override;
|
||||
void poll_input() override;
|
||||
void poll_input_async(Granite::InputTrackerHandler *handler) override;
|
||||
void set_window(SDL_Window *_window);
|
||||
void do_resize();
|
||||
|
||||
private:
|
||||
SDL_Window *window;
|
||||
SDL_Window *window;
|
||||
};
|
||||
|
||||
Submodule
+1
Submodule retroachievements/rcheevos added at e9ca3694c8
@@ -0,0 +1,399 @@
|
||||
#include "retroachievements.h"
|
||||
#include "../parallel-rdp/interface.hpp"
|
||||
#include <rc_client.h>
|
||||
#include <rc_consoles.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
void rust_server_call(const char *url, const char *post_data,
|
||||
const char *content_type,
|
||||
rc_client_server_callback_t callback,
|
||||
void *callback_data);
|
||||
void store_retroachievements_credentials(const char *username,
|
||||
const char *token, void *userdata);
|
||||
|
||||
void notify_load_game(void *userdata);
|
||||
|
||||
static rc_client_t *g_client = NULL;
|
||||
static const uint8_t *g_dmem = NULL;
|
||||
static size_t g_dmem_size = 0;
|
||||
static bool g_challenge = false;
|
||||
static bool g_leaderboard = false;
|
||||
static rc_client_leaderboard_list_t *g_leaderboard_list = NULL;
|
||||
|
||||
static uint32_t read_memory(uint32_t address, uint8_t *buffer,
|
||||
uint32_t num_bytes, rc_client_t *client) {
|
||||
if (address + num_bytes > g_dmem_size)
|
||||
memset(buffer, 0, num_bytes);
|
||||
else
|
||||
memcpy(buffer, &g_dmem[address], num_bytes);
|
||||
return num_bytes;
|
||||
}
|
||||
|
||||
static void server_call(const rc_api_request_t *request,
|
||||
rc_client_server_callback_t callback,
|
||||
void *callback_data, rc_client_t *client) {
|
||||
rust_server_call(request->url, request->post_data, request->content_type,
|
||||
callback, callback_data);
|
||||
}
|
||||
|
||||
void ra_http_callback(const char *content, size_t content_size, int status_code,
|
||||
void *callback, void *callback_data) {
|
||||
// Prepare a data object to pass the HTTP response to the callback
|
||||
rc_api_server_response_t server_response;
|
||||
memset(&server_response, 0, sizeof(server_response));
|
||||
server_response.body = content;
|
||||
server_response.body_length = content_size;
|
||||
server_response.http_status_code = status_code;
|
||||
|
||||
// handle non-http errors (socket timeout, no internet available, etc)
|
||||
if (status_code == 0) {
|
||||
// Let rc_client know the error was not catastrophic and could be retried.
|
||||
// It may decide to retry or just immediately pass the error to the
|
||||
// callback. To prevent possible retries, use
|
||||
// RC_API_SERVER_RESPONSE_CLIENT_ERROR.
|
||||
server_response.http_status_code =
|
||||
RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR;
|
||||
}
|
||||
|
||||
((rc_client_server_callback_t)callback)(&server_response, callback_data);
|
||||
}
|
||||
|
||||
static void log_message(const char *message, const rc_client_t *client) {
|
||||
printf("RetroAchievements: %s\n", message);
|
||||
}
|
||||
|
||||
static void login_callback(int result, const char *error_message,
|
||||
rc_client_t *client, void *userdata) {
|
||||
// If not successful, just report the error and bail.
|
||||
if (result != RC_OK) {
|
||||
if (client && !rc_client_get_userdata(client))
|
||||
rc_client_set_userdata(client, strdup(error_message));
|
||||
store_retroachievements_credentials(NULL, NULL, userdata);
|
||||
return;
|
||||
}
|
||||
|
||||
// Login was successful. Capture the token for future logins so we don't have
|
||||
// to store the password anywhere.
|
||||
const rc_client_user_t *user = rc_client_get_user_info(client);
|
||||
store_retroachievements_credentials(user->username, user->token, userdata);
|
||||
}
|
||||
|
||||
const char *ra_get_username() {
|
||||
const rc_client_user_t *user = rc_client_get_user_info(g_client);
|
||||
return user ? user->username : NULL;
|
||||
}
|
||||
const char *ra_get_token() {
|
||||
const rc_client_user_t *user = rc_client_get_user_info(g_client);
|
||||
return user ? user->token : NULL;
|
||||
}
|
||||
|
||||
void ra_logout_user() { rc_client_logout(g_client); }
|
||||
|
||||
void ra_login_user(const char *username, const char *password, void *userdata) {
|
||||
// This will generate an HTTP payload and call the server_call chain above.
|
||||
// Eventually, login_callback will be called to let us know if the login was
|
||||
// successful.
|
||||
rc_client_begin_login_with_password(g_client, username, password,
|
||||
login_callback, userdata);
|
||||
}
|
||||
|
||||
void ra_login_token_user(const char *username, const char *token,
|
||||
void *userdata) {
|
||||
// This is exactly the same functionality as
|
||||
// rc_client_begin_login_with_password, but uses the token captured from the
|
||||
// first login instead of a password. Note that it uses the same callback.
|
||||
rc_client_begin_login_with_token(g_client, username, token, login_callback,
|
||||
userdata);
|
||||
}
|
||||
|
||||
static void load_game_callback(int result, const char *error_message,
|
||||
rc_client_t *client, void *userdata) {
|
||||
if (result != RC_OK) {
|
||||
if (client && !rc_client_get_userdata(client))
|
||||
rc_client_set_userdata(client, strdup(error_message));
|
||||
rc_client_set_hardcore_enabled(client, false);
|
||||
notify_load_game(userdata);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rc_client_is_processing_required(client)) {
|
||||
rc_client_set_hardcore_enabled(client, false);
|
||||
}
|
||||
|
||||
g_leaderboard_list = rc_client_create_leaderboard_list(
|
||||
client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE);
|
||||
|
||||
notify_load_game(userdata);
|
||||
}
|
||||
|
||||
void ra_welcome() {
|
||||
if (!g_client)
|
||||
return;
|
||||
|
||||
char buffer[512];
|
||||
|
||||
char *error_message = rc_client_get_userdata(g_client);
|
||||
if (error_message) {
|
||||
snprintf(buffer, sizeof(buffer), "RA Error: %s", error_message);
|
||||
rdp_onscreen_message(buffer, MESSAGE_LONG);
|
||||
free(error_message);
|
||||
rc_client_set_userdata(g_client, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
const rc_client_game_t *game = rc_client_get_game_info(g_client);
|
||||
|
||||
rc_client_user_game_summary_t summary;
|
||||
rc_client_get_user_game_summary(g_client, &summary);
|
||||
|
||||
int message_length = snprintf(
|
||||
buffer, sizeof(buffer), "%s\nMode: %s\n", game->title,
|
||||
rc_client_get_hardcore_enabled(g_client) ? "Hardcore" : "Softcore");
|
||||
|
||||
if (summary.num_core_achievements != 0) {
|
||||
snprintf(buffer + message_length, sizeof(buffer) - message_length,
|
||||
"%u/%u achievements unlocked", summary.num_unlocked_achievements,
|
||||
summary.num_core_achievements);
|
||||
} else {
|
||||
snprintf(buffer + message_length, sizeof(buffer) - message_length,
|
||||
"Game has no achievements");
|
||||
}
|
||||
rdp_onscreen_message(buffer, MESSAGE_LONG);
|
||||
}
|
||||
|
||||
void ra_load_game(const uint8_t *rom, size_t rom_size, void *userdata) {
|
||||
rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_NINTENDO_64, NULL,
|
||||
rom, rom_size, load_game_callback,
|
||||
userdata);
|
||||
}
|
||||
|
||||
void ra_unload_game() {
|
||||
if (g_leaderboard_list) {
|
||||
rc_client_destroy_leaderboard_list(g_leaderboard_list);
|
||||
g_leaderboard_list = NULL;
|
||||
}
|
||||
rc_client_unload_game(g_client);
|
||||
}
|
||||
|
||||
void ra_set_dmem(const uint8_t *dmem, size_t dmem_size) {
|
||||
g_dmem = dmem;
|
||||
g_dmem_size = dmem_size;
|
||||
}
|
||||
|
||||
static void leaderboard_submitted(const rc_client_leaderboard_t *leaderboard) {
|
||||
char buffer[512];
|
||||
|
||||
snprintf(buffer, sizeof(buffer), "Leaderboard submitted: %s - %s",
|
||||
leaderboard->title, leaderboard->tracker_value);
|
||||
rdp_onscreen_message(buffer, MESSAGE_SHORT);
|
||||
}
|
||||
|
||||
static void achievement_triggered(const rc_client_achievement_t *achievement) {
|
||||
char buffer[512];
|
||||
|
||||
snprintf(buffer, sizeof(buffer), "Unlocked: %s", achievement->title);
|
||||
rdp_onscreen_message(buffer, MESSAGE_SHORT);
|
||||
}
|
||||
|
||||
static void game_completed(rc_client_t *client) {
|
||||
char buffer[512];
|
||||
const rc_client_game_t *game = rc_client_get_game_info(client);
|
||||
|
||||
snprintf(buffer, sizeof(buffer), "%s: %s",
|
||||
rc_client_get_hardcore_enabled(client) ? "Mastered" : "Completed",
|
||||
game->title);
|
||||
rdp_onscreen_message(buffer, MESSAGE_SHORT);
|
||||
}
|
||||
|
||||
static void subset_completed(const rc_client_subset_t *subset,
|
||||
rc_client_t *client) {
|
||||
char buffer[512];
|
||||
|
||||
snprintf(buffer, sizeof(buffer), "Subset %s: %s",
|
||||
rc_client_get_hardcore_enabled(client) ? "mastered" : "completed",
|
||||
subset->title);
|
||||
rdp_onscreen_message(buffer, MESSAGE_SHORT);
|
||||
}
|
||||
|
||||
static void server_error(const rc_client_server_error_t *server_error) {
|
||||
char buffer[512];
|
||||
|
||||
snprintf(buffer, sizeof(buffer), "RA server error: %s",
|
||||
server_error->error_message);
|
||||
rdp_onscreen_message(buffer, MESSAGE_SHORT);
|
||||
}
|
||||
|
||||
static const char *get_leaderboard_title(const char *display) {
|
||||
if (g_leaderboard_list == NULL)
|
||||
return NULL;
|
||||
|
||||
for (uint32_t i = 0; i < g_leaderboard_list->num_buckets; i++) {
|
||||
for (uint32_t j = 0; j < g_leaderboard_list->buckets[i].num_leaderboards;
|
||||
j++) {
|
||||
// this looks like a mistake, but it is intentional.
|
||||
// Leaderboard trackers don't contain the title,
|
||||
// but we take advantage of the fact that tracker_value and display point
|
||||
// to the same char* to find a matching leaderboard.
|
||||
if (g_leaderboard_list->buckets[i].leaderboards[j]->tracker_value ==
|
||||
display) {
|
||||
return g_leaderboard_list->buckets[i].leaderboards[j]->title;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void event_handler(const rc_client_event_t *event, rc_client_t *client) {
|
||||
switch (event->type) {
|
||||
case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED:
|
||||
achievement_triggered(event->achievement);
|
||||
break;
|
||||
case RC_CLIENT_EVENT_LEADERBOARD_STARTED:
|
||||
break;
|
||||
case RC_CLIENT_EVENT_LEADERBOARD_FAILED:
|
||||
break;
|
||||
case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED:
|
||||
if (g_leaderboard)
|
||||
leaderboard_submitted(event->leaderboard);
|
||||
break;
|
||||
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW:
|
||||
if (g_challenge)
|
||||
achievement_challenge_indicator_add(event->achievement->title);
|
||||
break;
|
||||
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE:
|
||||
if (g_challenge)
|
||||
achievement_challenge_indicator_remove(event->achievement->title);
|
||||
break;
|
||||
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW:
|
||||
achievement_progress_add(event->achievement->title,
|
||||
event->achievement->measured_progress);
|
||||
break;
|
||||
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE:
|
||||
achievement_progress_remove();
|
||||
break;
|
||||
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE:
|
||||
achievement_progress_add(event->achievement->title,
|
||||
event->achievement->measured_progress);
|
||||
break;
|
||||
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW:
|
||||
if (g_leaderboard)
|
||||
leaderboard_tracker_add(
|
||||
event->leaderboard_tracker->id,
|
||||
get_leaderboard_title(event->leaderboard_tracker->display),
|
||||
event->leaderboard_tracker->display);
|
||||
break;
|
||||
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE:
|
||||
if (g_leaderboard)
|
||||
leaderboard_tracker_remove(event->leaderboard_tracker->id);
|
||||
break;
|
||||
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE:
|
||||
if (g_leaderboard)
|
||||
leaderboard_tracker_add(
|
||||
event->leaderboard_tracker->id,
|
||||
get_leaderboard_title(event->leaderboard_tracker->display),
|
||||
event->leaderboard_tracker->display);
|
||||
break;
|
||||
case RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD:
|
||||
break;
|
||||
case RC_CLIENT_EVENT_GAME_COMPLETED:
|
||||
game_completed(client);
|
||||
break;
|
||||
case RC_CLIENT_EVENT_SUBSET_COMPLETED:
|
||||
subset_completed(event->subset, client);
|
||||
break;
|
||||
case RC_CLIENT_EVENT_SERVER_ERROR:
|
||||
server_error(event->server_error);
|
||||
break;
|
||||
default:
|
||||
printf("RetroAchievements: Unhandled event %d\n", event->type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ra_init_client(bool hardcore, bool challenge, bool leaderboard) {
|
||||
// Create the client instance (using a global variable simplifies this
|
||||
// example)
|
||||
g_client = rc_client_create(read_memory, server_call);
|
||||
g_dmem = NULL;
|
||||
g_dmem_size = 0;
|
||||
|
||||
// Provide a logging function to simplify debugging
|
||||
rc_client_enable_logging(g_client, RC_CLIENT_LOG_LEVEL_WARN, log_message);
|
||||
|
||||
rc_client_set_event_handler(g_client, event_handler);
|
||||
|
||||
rc_client_set_hardcore_enabled(g_client, hardcore);
|
||||
|
||||
g_challenge = challenge;
|
||||
g_leaderboard = leaderboard;
|
||||
}
|
||||
|
||||
bool ra_get_hardcore() { return rc_client_get_hardcore_enabled(g_client); }
|
||||
|
||||
void ra_shutdown_client() {
|
||||
if (g_client) {
|
||||
// Release resources associated to the client instance
|
||||
rc_client_destroy(g_client);
|
||||
g_client = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void ra_do_frame() { rc_client_do_frame(g_client); }
|
||||
|
||||
void ra_do_idle() { rc_client_idle(g_client); }
|
||||
|
||||
size_t ra_state_size() { return rc_client_progress_size(g_client); }
|
||||
|
||||
void ra_save_state(uint8_t *state, size_t state_size) {
|
||||
if (!rc_client_is_game_loaded(g_client))
|
||||
return;
|
||||
|
||||
if (rc_client_serialize_progress_sized(g_client, state, state_size) !=
|
||||
RC_OK) {
|
||||
printf("RetroAchievements: Failed to serialize progress\n");
|
||||
}
|
||||
}
|
||||
|
||||
void ra_load_state(const uint8_t *state, size_t state_size) {
|
||||
if (!rc_client_is_game_loaded(g_client))
|
||||
return;
|
||||
|
||||
if (rc_client_deserialize_progress_sized(g_client, state, state_size) !=
|
||||
RC_OK) {
|
||||
printf("RetroAchievements: Failed to deserialize progress\n");
|
||||
}
|
||||
}
|
||||
|
||||
void ra_display_inprogress_achievements(void *userdata) {
|
||||
if (!rc_client_is_game_loaded(g_client))
|
||||
return;
|
||||
|
||||
rc_client_achievement_list_t *list = rc_client_create_achievement_list(
|
||||
g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE,
|
||||
RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE);
|
||||
|
||||
char buffer[1024] = {0};
|
||||
size_t buffer_length = 0;
|
||||
|
||||
for (uint32_t i = 0; i < list->num_buckets; i++) {
|
||||
if (list->buckets[i].bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED) {
|
||||
for (uint32_t j = 0; j < list->buckets[i].num_achievements; j++) {
|
||||
const rc_client_achievement_t *achievement =
|
||||
list->buckets[i].achievements[j];
|
||||
if (achievement->measured_percent > 0.0f) {
|
||||
if (buffer_length < sizeof(buffer) - 1) {
|
||||
buffer_length += snprintf(
|
||||
buffer + buffer_length, sizeof(buffer) - buffer_length,
|
||||
"%s: %s\n", achievement->title, achievement->measured_progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (buffer_length > 0) {
|
||||
rdp_onscreen_message(buffer, MESSAGE_SHORT);
|
||||
}
|
||||
rc_client_destroy_achievement_list(list);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
void ra_init_client(bool hardcore, bool challenge, bool leaderboard);
|
||||
void ra_shutdown_client();
|
||||
void ra_welcome();
|
||||
bool ra_get_hardcore();
|
||||
void ra_load_game(const uint8_t *rom, size_t rom_size, void *userdata);
|
||||
void ra_unload_game();
|
||||
void ra_set_dmem(const uint8_t *dmem, size_t dmem_size);
|
||||
void ra_do_frame();
|
||||
void ra_do_idle();
|
||||
void ra_http_callback(const char *content, size_t content_size, int status_code,
|
||||
void *callback, void *callback_data);
|
||||
void ra_logout_user();
|
||||
void ra_login_user(const char *username, const char *password, void *userdata);
|
||||
void ra_login_token_user(const char *username, const char *token,
|
||||
void *userdata);
|
||||
const char *ra_get_username();
|
||||
const char *ra_get_token();
|
||||
size_t ra_state_size();
|
||||
void ra_save_state(uint8_t *state, size_t state_size);
|
||||
void ra_load_state(const uint8_t *state, size_t state_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
void ra_display_inprogress_achievements(void *userdata);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "1.88.0"
|
||||
channel = "1.95.0"
|
||||
|
||||
+15
-7
@@ -15,16 +15,24 @@ pub struct Cheats {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone)]
|
||||
pub struct CheatData {
|
||||
pub note: String,
|
||||
pub data: Vec<String>,
|
||||
pub options: Option<std::collections::BTreeMap<String, String>>,
|
||||
}
|
||||
|
||||
pub type Cheat = std::collections::BTreeMap<String, std::collections::BTreeMap<String, CheatData>>;
|
||||
|
||||
pub fn init(
|
||||
device: &mut device::Device,
|
||||
cheat_settings: std::collections::HashMap<String, Option<String>>,
|
||||
) {
|
||||
let cheats =
|
||||
serde_json::from_slice::<ui::cheats::Cheats>(include_bytes!("../data/cheats.json"))
|
||||
.unwrap()
|
||||
.get(&ui::storage::get_game_crc(&device.cart.rom))
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let cheats = serde_json::from_slice::<Cheat>(include_bytes!("../data/cheats.json"))
|
||||
.unwrap()
|
||||
.get(&ui::storage::get_game_crc(&device.cart.rom))
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let re = regex::Regex::new(r"\?+").unwrap();
|
||||
for cheat_setting in cheat_settings.iter() {
|
||||
@@ -71,7 +79,7 @@ pub fn init(
|
||||
|
||||
if !device.cheats.cheats.is_empty() {
|
||||
device.cheats.enabled = true;
|
||||
ui::audio::play_cheat_event(&device.ui);
|
||||
ui::video::onscreen_message("Cheats enabled", ui::video::MESSAGE_LENGTH_MESSAGE_SHORT);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "src/compat/sse2neon/v1.8.0/sse2neon.h"
|
||||
#include "src/compat/sse2neon/sse2neon.h"
|
||||
|
||||
// Static wrappers
|
||||
|
||||
|
||||
Submodule
+1
Submodule src/compat/sse2neon added at b26afbb44d
File diff suppressed because it is too large
Load Diff
+35
-24
@@ -10,9 +10,9 @@ include!(concat!(env!("OUT_DIR"), "/simd_bindings.rs"));
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
use std::arch::x86_64::*;
|
||||
|
||||
use rand_chacha::rand_core::{RngCore, SeedableRng};
|
||||
use rand::{Rng, SeedableRng};
|
||||
|
||||
use crate::{cheats, netplay, ui};
|
||||
use crate::{cheats, netplay, retroachievements, ui};
|
||||
use std::{collections::HashMap, fs, io::Read};
|
||||
|
||||
pub mod ai;
|
||||
@@ -44,24 +44,30 @@ pub mod tlb;
|
||||
pub mod unmapped;
|
||||
pub mod vi;
|
||||
|
||||
pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, game_settings: ui::gui::GameSettings) {
|
||||
device.ui.video.fullscreen = game_settings.fullscreen;
|
||||
pub fn run_game(device: &mut Device, rom_contents: &[u8], game_settings: ui::GameSettings) {
|
||||
device.cpu.overclock = game_settings.overclock;
|
||||
if game_settings.disable_expansion_pak {
|
||||
device.rdram.size = 0x400000;
|
||||
}
|
||||
if let Some(slot) = game_settings.load_savestate_slot {
|
||||
device.ui.storage.save_state_slot = slot;
|
||||
device.load_state = true;
|
||||
}
|
||||
|
||||
init_rng(device);
|
||||
|
||||
cart::rom::init(device, rom_contents); // cart needs to come before rdram
|
||||
|
||||
// rdram pointer is shared with parallel-rdp
|
||||
// rdram pointer is shared with parallel-rdp and retroachievements
|
||||
rdram::init(device);
|
||||
|
||||
ui::video::init(device);
|
||||
ui::audio::init(&mut device.ui, device.ai.freq);
|
||||
ui::input::init(&mut device.ui);
|
||||
|
||||
// must be after video init
|
||||
retroachievements::welcome();
|
||||
|
||||
mi::init(device);
|
||||
pif::init(device);
|
||||
if device.ui.config.input.emulate_vru && device.netplay.is_none() {
|
||||
@@ -77,31 +83,34 @@ pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, game_settings: ui::g
|
||||
ui::storage::init(&mut device.ui, &device.cart.rom);
|
||||
ui::storage::load_saves(&mut device.ui, &mut device.netplay);
|
||||
|
||||
cheats::init(device, game_settings.cheats);
|
||||
if !retroachievements::get_hardcore() {
|
||||
cheats::init(device, game_settings.cheats);
|
||||
}
|
||||
|
||||
cpu::run(device);
|
||||
|
||||
retroachievements::unload_game();
|
||||
|
||||
ui::storage::write_saves(device);
|
||||
ui::input::close(&mut device.ui);
|
||||
ui::audio::close(&mut device.ui);
|
||||
ui::video::close(&device.ui);
|
||||
}
|
||||
|
||||
fn set_rng() -> rand_chacha::ChaCha8Rng {
|
||||
rand_chacha::ChaCha8Rng::from_os_rng()
|
||||
fn set_rng() -> rand::rngs::Xoshiro256PlusPlus {
|
||||
rand::rngs::Xoshiro256PlusPlus::try_from_rng(&mut rand::rngs::SysRng).unwrap()
|
||||
}
|
||||
|
||||
fn init_rng(device: &mut Device) {
|
||||
let mut rng_seed = set_rng().next_u64();
|
||||
if device.netplay.is_some() {
|
||||
let netplay = device.netplay.as_mut().unwrap();
|
||||
if let Some(netplay) = &mut device.netplay {
|
||||
if netplay.player_number == 0 {
|
||||
netplay::send_rng(netplay, rng_seed);
|
||||
} else {
|
||||
rng_seed = netplay::receive_rng(netplay);
|
||||
}
|
||||
}
|
||||
device.rng = rand_chacha::ChaCha8Rng::seed_from_u64(rng_seed);
|
||||
device.rng = rand::rngs::Xoshiro256PlusPlus::seed_from_u64(rng_seed);
|
||||
}
|
||||
|
||||
fn swap_rom(contents: Vec<u8>) -> Option<Vec<u8>> {
|
||||
@@ -132,7 +141,11 @@ fn swap_rom(contents: Vec<u8>) -> Option<Vec<u8>> {
|
||||
|
||||
pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
|
||||
let mut contents = vec![];
|
||||
if file_path.extension().unwrap().eq_ignore_ascii_case("zip") {
|
||||
if file_path
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.eq_ignore_ascii_case("zip")
|
||||
{
|
||||
let zip_file = fs::File::open(file_path).unwrap();
|
||||
let mut archive = zip::ZipArchive::new(zip_file).unwrap();
|
||||
for i in 0..archive.len() {
|
||||
@@ -149,7 +162,11 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if file_path.extension().unwrap().eq_ignore_ascii_case("7z") {
|
||||
} else if file_path
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.eq_ignore_ascii_case("7z")
|
||||
{
|
||||
let mut archive =
|
||||
sevenz_rust2::ArchiveReader::open(file_path, sevenz_rust2::Password::empty()).unwrap();
|
||||
|
||||
@@ -174,7 +191,8 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
|
||||
)
|
||||
.expect("ok");
|
||||
} else {
|
||||
contents = fs::read(file_path).expect("Should have been able to read the file");
|
||||
contents = fs::read(file_path)
|
||||
.unwrap_or_else(|_| panic!("Could not read ROM file: {}", file_path.display()));
|
||||
}
|
||||
|
||||
if contents.is_empty() {
|
||||
@@ -207,9 +225,8 @@ pub struct Device {
|
||||
pub si: si::Si,
|
||||
pub ri: ri::Ri,
|
||||
#[serde(skip, default = "set_rng")]
|
||||
pub rng: rand_chacha::ChaCha8Rng,
|
||||
pub rng: rand::rngs::Xoshiro256PlusPlus,
|
||||
pub vru: controller::vru::Vru,
|
||||
pub vru_window: controller::vru::VruWindow,
|
||||
pub transferpaks: [controller::transferpak::TransferPak; 4],
|
||||
pub cheats: cheats::Cheats,
|
||||
}
|
||||
@@ -239,7 +256,6 @@ impl Device {
|
||||
reg_write_masks: [0; cop0::COP0_REGS_COUNT as usize],
|
||||
reg_latch: 0,
|
||||
is_event: false,
|
||||
pending_compare_interrupt: false,
|
||||
instrs: [cop0::reserved; 32],
|
||||
instrs2: [cop0::reserved; 32],
|
||||
tlb_lut_w: vec![
|
||||
@@ -347,6 +363,7 @@ impl Device {
|
||||
sector: 0,
|
||||
buffer: vec![0; 8192],
|
||||
writeback_sector: vec![0; 256],
|
||||
usb_buffer: vec![],
|
||||
},
|
||||
flashram: cart::sram::Flashram {
|
||||
status: 0,
|
||||
@@ -473,19 +490,13 @@ impl Device {
|
||||
delay: 0,
|
||||
count_per_scanline: 0,
|
||||
enable_speed_limiter: true,
|
||||
limiter: None,
|
||||
next_pace_deadline: None,
|
||||
vi_counter: 0,
|
||||
last_origin: 0,
|
||||
internal_frame_counter: 0,
|
||||
min_wait_time: std::time::Duration::from_secs(1),
|
||||
frame_time: 0.0,
|
||||
limit_freq: 2,
|
||||
limit_freq_check: std::time::Instant::now(),
|
||||
},
|
||||
vru_window: controller::vru::VruWindow {
|
||||
window_notifier: None,
|
||||
word_receiver: None,
|
||||
},
|
||||
vru: controller::vru::Vru {
|
||||
status: 0,
|
||||
voice_state: 0,
|
||||
|
||||
+1
-2
@@ -45,11 +45,10 @@ fn get_remaining_dma_length(device: &device::Device) -> u64 {
|
||||
}
|
||||
|
||||
fn get_dma_duration(device: &device::Device) -> u64 {
|
||||
let samples_per_sec = device.vi.clock / (1 + device.ai.regs[AI_DACRATE_REG as usize]) as u64;
|
||||
let bytes_per_sample = 4; /* XXX: assume 16bit stereo - should depends on bitrate instead */
|
||||
let length = (device.ai.regs[AI_LEN_REG as usize] & !7) as u64;
|
||||
|
||||
length * (device.cpu.clock_rate / (bytes_per_sample * samples_per_sec))
|
||||
(length * device.cpu.clock_rate) / (bytes_per_sample * device.ai.freq)
|
||||
}
|
||||
|
||||
fn do_dma(device: &mut device::Device) {
|
||||
|
||||
+1
-11
@@ -21,16 +21,6 @@ const JDT_EEPROM_16K: u16 = 0xc000; /* 16k EEPROM */
|
||||
const EEPROM_BLOCK_SIZE: usize = 8;
|
||||
pub const EEPROM_MAX_SIZE: usize = 0x800;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub enum CicType {
|
||||
CicNus6101,
|
||||
CicNus6102,
|
||||
CicNus6103,
|
||||
CicNus6105,
|
||||
CicNus6106,
|
||||
CicNus5167,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct AfRtc {
|
||||
pub control: u16,
|
||||
@@ -157,7 +147,7 @@ fn eeprom_write_block(device: &mut device::Device, block: usize, offset: usize,
|
||||
|
||||
device.pif.ram[status] = 0x00;
|
||||
|
||||
device.ui.storage.saves.eeprom.written = true
|
||||
ui::storage::schedule_save(device, ui::storage::SaveTypes::Eeprom4k);
|
||||
}
|
||||
|
||||
fn time2data(device: &mut device::Device, offset: usize) {
|
||||
|
||||
+12
-8
@@ -1,8 +1,9 @@
|
||||
use crate::device;
|
||||
use crate::netplay;
|
||||
use crate::ui;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
const CART_MASK: usize = 0xFFFFFFF;
|
||||
pub const CART_MASK: usize = 0xFFFFFFF;
|
||||
|
||||
fn read_cart_word(device: &device::Device, address: usize) -> u32 {
|
||||
let mut data: [u8; 4] = [0; 4];
|
||||
@@ -63,7 +64,7 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
|
||||
.data
|
||||
.insert((masked_address + i) as u32, *item);
|
||||
}
|
||||
device.ui.storage.saves.romsave.written = true;
|
||||
ui::storage::schedule_save(device, ui::storage::SaveTypes::Romsave);
|
||||
}
|
||||
|
||||
device.cart.latch = value & mask;
|
||||
@@ -94,7 +95,7 @@ pub fn dma_read(
|
||||
.unwrap_or(&0),
|
||||
);
|
||||
}
|
||||
device.ui.storage.saves.romsave.written = true;
|
||||
ui::storage::schedule_save(device, ui::storage::SaveTypes::Romsave);
|
||||
}
|
||||
|
||||
device::pi::calculate_cycles(device, 1, length)
|
||||
@@ -132,11 +133,10 @@ pub fn dma_write(
|
||||
device::pi::calculate_cycles(device, 1, length)
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device, rom_file: Vec<u8>) {
|
||||
pub fn init(device: &mut device::Device, rom_file: &[u8]) {
|
||||
let now: chrono::DateTime<chrono::Local> = chrono::Local::now();
|
||||
device.cart.rtc_timestamp = now.naive_local().and_utc().timestamp();
|
||||
if device.netplay.is_some() {
|
||||
let netplay = device.netplay.as_mut().unwrap();
|
||||
if let Some(netplay) = &mut device.netplay {
|
||||
if netplay.player_number == 0 {
|
||||
netplay::send_rtc(netplay, device.cart.rtc_timestamp);
|
||||
} else {
|
||||
@@ -146,7 +146,7 @@ pub fn init(device: &mut device::Device, rom_file: Vec<u8>) {
|
||||
|
||||
device.cart.sc64.cfg[device::cart::sc64::SC64_BOOTLOADER_SWITCH as usize] = 1;
|
||||
|
||||
device.cart.rom = rom_file;
|
||||
device.cart.rom = rom_file.to_vec();
|
||||
device.cart.pal = is_system_pal(device.cart.rom[0x3E]);
|
||||
set_cic(device);
|
||||
|
||||
@@ -200,5 +200,9 @@ fn set_cic(device: &mut device::Device) {
|
||||
pub fn calculate_hash(data: &[u8]) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(data);
|
||||
format!("{:X}", hasher.finalize())
|
||||
hasher
|
||||
.finalize()
|
||||
.iter()
|
||||
.map(|b| format!("{:02X}", b))
|
||||
.collect()
|
||||
}
|
||||
|
||||
+130
-5
@@ -28,6 +28,7 @@ pub struct Sc64 {
|
||||
pub cfg: [u32; SC64_CFG_COUNT as usize],
|
||||
pub sector: u32,
|
||||
pub writeback_sector: Vec<u32>,
|
||||
pub usb_buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
fn format_sdcard(device: &mut device::Device) {
|
||||
@@ -207,10 +208,134 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
.copy_from_slice(&data);
|
||||
i += 4;
|
||||
}
|
||||
device.ui.storage.saves.sdcard.written = true;
|
||||
ui::storage::schedule_save(device, ui::storage::SaveTypes::Sdcard);
|
||||
}
|
||||
'U' => {
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0;
|
||||
}
|
||||
'u' => {
|
||||
// used to notify the game that there is data to read
|
||||
if let Some(cart_rx) = device.ui.usb.cart_rx.as_mut() {
|
||||
match cart_rx.try_recv() {
|
||||
Ok(data) => {
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] =
|
||||
data.data_type & 0xFF; // read_status/type
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize] =
|
||||
data.data_size & 0xFFFFFF; // length
|
||||
device.cart.sc64.usb_buffer = data.data; // store the data to be read
|
||||
}
|
||||
Err(err) => {
|
||||
match err {
|
||||
tokio::sync::broadcast::error::TryRecvError::Lagged(_) => {
|
||||
panic!("cart_rx lagged: {err}");
|
||||
}
|
||||
_ => {
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0; // read_status/type
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize] = 0; // length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0; // read_status/type
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize] = 0; // length
|
||||
}
|
||||
}
|
||||
'M' => {
|
||||
// Send data from from flashcart to USB
|
||||
let address =
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64 & 0x1FFFFFFF;
|
||||
let length =
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize] as usize & 0xFFFFFF;
|
||||
|
||||
if let Some(usb_tx) = device.ui.usb.usb_tx.as_mut() {
|
||||
let mut usb_buffer = vec![0; length];
|
||||
|
||||
let mut i = 0;
|
||||
|
||||
if address < device.rdram.size as u64 {
|
||||
while i < length {
|
||||
*usb_buffer.get_mut(i).unwrap_or(&mut 0) = *device
|
||||
.rdram
|
||||
.mem
|
||||
.get((address as usize + i) ^ device.byte_swap)
|
||||
.unwrap_or(&0);
|
||||
i += 1;
|
||||
}
|
||||
} else if address >= device::memory::MM_CART_ROM as u64
|
||||
&& address < device::memory::MM_PIF_MEM as u64
|
||||
{
|
||||
while i < length {
|
||||
*usb_buffer.get_mut(i).unwrap_or(&mut 0) = *device
|
||||
.ui
|
||||
.storage
|
||||
.saves
|
||||
.romsave
|
||||
.data
|
||||
.get(
|
||||
&(((address as usize + i)
|
||||
& device::cart::rom::CART_MASK)
|
||||
as u32),
|
||||
)
|
||||
.unwrap_or(
|
||||
device
|
||||
.cart
|
||||
.rom
|
||||
.get(
|
||||
(address as usize + i)
|
||||
& device::cart::rom::CART_MASK,
|
||||
)
|
||||
.unwrap_or(&0),
|
||||
);
|
||||
i += 1;
|
||||
}
|
||||
} else {
|
||||
panic!("Unknown address {address:#x} for SC64 M command");
|
||||
}
|
||||
|
||||
ui::usb::send_to_usb(
|
||||
usb_tx,
|
||||
ui::usb::UsbData {
|
||||
data: usb_buffer,
|
||||
data_type: device.cart.sc64.regs[SC64_DATA1_REG as usize] >> 24,
|
||||
data_size: device.cart.sc64.regs[SC64_DATA1_REG as usize]
|
||||
& 0xFFFFFF,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
'm' => {
|
||||
// Receive data from USB to flashcart
|
||||
let address =
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64 & 0x1FFFFFFF;
|
||||
let length = device.cart.sc64.regs[SC64_DATA1_REG as usize] as usize;
|
||||
|
||||
let mut i = 0;
|
||||
|
||||
if address < device.rdram.size as u64 {
|
||||
while i < length {
|
||||
*device
|
||||
.rdram
|
||||
.mem
|
||||
.get_mut((address as usize + i) ^ device.byte_swap)
|
||||
.unwrap_or(&mut 0) =
|
||||
*device.cart.sc64.usb_buffer.get(i).unwrap_or(&0);
|
||||
i += 1;
|
||||
}
|
||||
} else if address >= device::memory::MM_CART_ROM as u64
|
||||
&& address < device::memory::MM_PIF_MEM as u64
|
||||
{
|
||||
while i < length {
|
||||
device.ui.storage.saves.romsave.data.insert(
|
||||
((address as usize + i) & device::cart::rom::CART_MASK) as u32,
|
||||
*device.cart.sc64.usb_buffer.get(i).unwrap_or(&0),
|
||||
);
|
||||
i += 1;
|
||||
}
|
||||
} else {
|
||||
panic!("Unknown address {address:#x} for SC64 m command");
|
||||
}
|
||||
}
|
||||
'U' | 'u' => {} // USB status, ignored
|
||||
'M' | 'm' => {} // USB read/write, ignored
|
||||
'w' => {
|
||||
// SD card writeback pending
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0;
|
||||
@@ -280,7 +405,7 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
|
||||
device::memory::masked_write_32(&mut data, value, mask);
|
||||
device.ui.storage.saves.eeprom.data[masked_address..masked_address + 4]
|
||||
.copy_from_slice(&data.to_be_bytes());
|
||||
device.ui.storage.saves.eeprom.written = true
|
||||
ui::storage::schedule_save(device, ui::storage::SaveTypes::Eeprom4k);
|
||||
} else {
|
||||
let masked_address = address as usize & SC64_BUFFER_MASK;
|
||||
let mut data = u32::from_be_bytes(
|
||||
@@ -304,7 +429,7 @@ pub fn dma_read(
|
||||
let buffer = if cart_addr & 0x2000 != 0 {
|
||||
device::cart::format_eeprom(device);
|
||||
cart_addr &= SC64_EEPROM_MASK as u32;
|
||||
device.ui.storage.saves.eeprom.written = true;
|
||||
ui::storage::schedule_save(device, ui::storage::SaveTypes::Eeprom4k);
|
||||
&mut device.ui.storage.saves.eeprom.data
|
||||
} else {
|
||||
cart_addr &= SC64_BUFFER_MASK as u32;
|
||||
|
||||
@@ -107,7 +107,7 @@ fn write_mem_sram(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
device.ui.storage.saves.sram.data[masked_address..masked_address + 4]
|
||||
.copy_from_slice(&data.to_be_bytes());
|
||||
|
||||
device.ui.storage.saves.sram.written = true
|
||||
ui::storage::schedule_save(device, ui::storage::SaveTypes::Sram);
|
||||
}
|
||||
|
||||
fn write_mem_flash(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
@@ -167,7 +167,7 @@ fn dma_read_sram(device: &mut device::Device, mut cart_addr: u32, mut dram_addr:
|
||||
j += 1;
|
||||
}
|
||||
|
||||
device.ui.storage.saves.sram.written = true
|
||||
ui::storage::schedule_save(device, ui::storage::SaveTypes::Sram);
|
||||
}
|
||||
|
||||
fn dma_read_flash(device: &mut device::Device, cart_addr: u32, dram_addr: u32, length: u32) {
|
||||
@@ -331,12 +331,12 @@ fn flashram_command(device: &mut device::Device, command: u32) {
|
||||
for i in 0..128 * 128 {
|
||||
device.ui.storage.saves.flash.data[offset + i] = 0xFF;
|
||||
}
|
||||
device.ui.storage.saves.flash.written = true
|
||||
ui::storage::schedule_save(device, ui::storage::SaveTypes::Flash);
|
||||
} else if device.cart.flashram.mode == FlashramMode::ChipErase {
|
||||
for i in 0..FLASHRAM_SIZE {
|
||||
device.ui.storage.saves.flash.data[i] = 0xFF;
|
||||
}
|
||||
device.ui.storage.saves.flash.written = true
|
||||
ui::storage::schedule_save(device, ui::storage::SaveTypes::Flash);
|
||||
} else {
|
||||
panic!("Unexpected flash erase command")
|
||||
}
|
||||
@@ -356,7 +356,7 @@ fn flashram_command(device: &mut device::Device, command: u32) {
|
||||
for i in 0..128 {
|
||||
device.ui.storage.saves.flash.data[offset + i] = device.cart.flashram.page_buf[i];
|
||||
}
|
||||
device.ui.storage.saves.flash.written = true;
|
||||
ui::storage::schedule_save(device, ui::storage::SaveTypes::Flash);
|
||||
|
||||
/* clear program busy flag, set program success flag, transition to status mode */
|
||||
device.cart.flashram.status &= !0x01;
|
||||
|
||||
+22
-10
@@ -56,15 +56,14 @@ pub fn process(device: &mut device::Device, channel: usize) {
|
||||
}
|
||||
JCMD_CONTROLLER_READ => {
|
||||
let offset = device.pif.channels[channel].rx_buf.unwrap();
|
||||
let input = if device.netplay.is_none() {
|
||||
ui::input::get(&device.ui, channel)
|
||||
} else {
|
||||
if device.netplay.as_ref().unwrap().player_number as usize == channel {
|
||||
let input = if let Some(netplay) = &mut device.netplay {
|
||||
if netplay.player_number as usize == channel {
|
||||
let local_input = ui::input::get(&device.ui, 0);
|
||||
netplay::send_input(device.netplay.as_ref().unwrap(), local_input);
|
||||
netplay::send_input(netplay, local_input);
|
||||
}
|
||||
|
||||
netplay::get_input(device, channel)
|
||||
} else {
|
||||
ui::input::get(&device.ui, channel)
|
||||
};
|
||||
|
||||
device.pif.ram[offset..offset + 4].copy_from_slice(&input.data.to_ne_bytes());
|
||||
@@ -162,10 +161,12 @@ pub fn pak_switch_event(device: &mut device::Device) {
|
||||
for (i, channel) in device.pif.channels.iter_mut().enumerate() {
|
||||
if channel.change_pak != PakType::None {
|
||||
//stop rumble if it is on
|
||||
if device.netplay.is_none() {
|
||||
if let Some(netplay) = &device.netplay {
|
||||
if netplay.player_number as usize == i {
|
||||
device::ui::input::set_rumble(&device.ui, 0, 0);
|
||||
}
|
||||
} else {
|
||||
device::ui::input::set_rumble(&device.ui, i, 0);
|
||||
} else if device.netplay.as_ref().unwrap().player_number as usize == i {
|
||||
device::ui::input::set_rumble(&device.ui, 0, 0);
|
||||
}
|
||||
|
||||
let new_pak_type = match channel.change_pak {
|
||||
@@ -189,20 +190,31 @@ pub fn pak_switch_event(device: &mut device::Device) {
|
||||
write: device::controller::mempak::write,
|
||||
pak_type: new_pak_type,
|
||||
});
|
||||
ui::video::onscreen_message(
|
||||
"MemPak selected",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
} else if new_pak_type == PakType::RumblePak {
|
||||
channel.pak_handler = Some(device::controller::PakHandler {
|
||||
read: device::controller::rumble::read,
|
||||
write: device::controller::rumble::write,
|
||||
pak_type: new_pak_type,
|
||||
});
|
||||
ui::video::onscreen_message(
|
||||
"RumblePak selected",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
} else if new_pak_type == PakType::TransferPak {
|
||||
channel.pak_handler = Some(device::controller::PakHandler {
|
||||
read: device::controller::transferpak::read,
|
||||
write: device::controller::transferpak::write,
|
||||
pak_type: new_pak_type,
|
||||
});
|
||||
ui::video::onscreen_message(
|
||||
"TransferPak selected",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
}
|
||||
ui::audio::play_pak_switch(&device.ui, new_pak_type);
|
||||
channel.change_pak = PakType::None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ fn write_mbc3(
|
||||
if address < 0x2000 {
|
||||
cart.ram_enabled = value & 0xf == 0xa;
|
||||
} else if address < 0x4000 {
|
||||
let bank = value & 0x7f;
|
||||
let bank = value & if cart.rom.len() > 2097152 { 0xff } else { 0x7f };
|
||||
cart.rom_bank = bank as u32;
|
||||
if cart.rom_bank == 0 {
|
||||
cart.rom_bank = 1;
|
||||
@@ -182,16 +182,12 @@ fn read_mbc3(
|
||||
return;
|
||||
}
|
||||
if cart.ram_bank < 0x8 {
|
||||
if cart.ram_bank > 3 {
|
||||
for i in 0..size {
|
||||
pif_ram[data + i] = 0;
|
||||
}
|
||||
} else {
|
||||
let banked_address = address - 0xA000 + (cart.ram_bank * 0x2000);
|
||||
pif_ram[data..data + size].copy_from_slice(
|
||||
&cart.ram[banked_address as usize..banked_address as usize + size],
|
||||
);
|
||||
}
|
||||
let banked_address = address - 0xA000 + (cart.ram_bank * 0x2000);
|
||||
pif_ram[data..data + size].copy_from_slice(
|
||||
cart.ram
|
||||
.get(banked_address as usize..banked_address as usize + size)
|
||||
.unwrap_or(&vec![0; size]),
|
||||
);
|
||||
} else {
|
||||
let latch = match cart.ram_bank {
|
||||
0x8 => cart.latch_second,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
|
||||
pub const MEMPAK_SIZE: usize = 0x8000;
|
||||
const MPK_PAGE_SIZE: usize = 256;
|
||||
@@ -96,6 +97,6 @@ pub fn write(device: &mut device::Device, channel: usize, address: u16, data: us
|
||||
device.ui.storage.saves.mempak.data[offset..offset + size]
|
||||
.copy_from_slice(&device.pif.ram[data..data + size]);
|
||||
|
||||
device.ui.storage.saves.mempak.written = true
|
||||
ui::storage::schedule_save(device, ui::storage::SaveTypes::Mempak);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,13 @@ pub fn read(device: &mut device::Device, _channel: usize, address: u16, data: us
|
||||
pub fn write(device: &mut device::Device, channel: usize, address: u16, data: usize, size: usize) {
|
||||
if address == 0xc000 {
|
||||
let rumble = device.pif.ram[data + size - 1];
|
||||
if device.netplay.is_none() {
|
||||
|
||||
if let Some(netplay) = &device.netplay {
|
||||
if netplay.player_number as usize == channel {
|
||||
device::ui::input::set_rumble(&device.ui, 0, rumble);
|
||||
}
|
||||
} else {
|
||||
device::ui::input::set_rumble(&device.ui, channel, rumble);
|
||||
} else if device.netplay.as_ref().unwrap().player_number as usize == channel {
|
||||
device::ui::input::set_rumble(&device.ui, 0, rumble);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::{device, ui};
|
||||
use crate::device;
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::ui;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const JCMD_VRU_READ: u8 = 0x09;
|
||||
@@ -29,14 +31,6 @@ pub struct Vru {
|
||||
pub word_mappings: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct VruWindow {
|
||||
#[serde(skip)]
|
||||
pub window_notifier: Option<tokio::sync::mpsc::Sender<Option<Vec<String>>>>,
|
||||
#[serde(skip)]
|
||||
pub word_receiver: Option<tokio::sync::mpsc::Receiver<String>>,
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
reset_vru(device);
|
||||
create_word_mappings(device);
|
||||
@@ -187,15 +181,10 @@ pub fn process(device: &mut device::Device, channel: usize) {
|
||||
device.pif.ram[device.pif.channels[channel].rx_buf.unwrap()] = 0;
|
||||
}
|
||||
JCMD_VRU_READ => {
|
||||
let index = if device.vru_window.window_notifier.is_some() {
|
||||
ui::vru::prompt_for_match(
|
||||
&device.vru.words,
|
||||
device.vru_window.window_notifier.as_ref().unwrap(),
|
||||
device.vru_window.word_receiver.as_mut().unwrap(),
|
||||
)
|
||||
} else {
|
||||
0x7FFF
|
||||
};
|
||||
#[cfg(feature = "gui")]
|
||||
let index = ui::vru::prompt_for_match(&device.vru.words, device.vi.frame_time);
|
||||
#[cfg(not(feature = "gui"))]
|
||||
let index = 0x7FFF;
|
||||
let num_results = if index == 0x7FFF { 0 } else { 1 };
|
||||
let data: HashMap<usize, u16> = HashMap::from([
|
||||
(0, 0x8000),
|
||||
|
||||
+47
-41
@@ -30,13 +30,15 @@ pub const COP0_XCONTEXT_REG: u32 = 20;
|
||||
//const COP0_CACHEERR_REG: u32 = 27;
|
||||
pub const COP0_TAGLO_REG: u32 = 28;
|
||||
//const COP0_TAGHI_REG: u32 = 29;
|
||||
const COP0_ERROREPC_REG: u32 = 30;
|
||||
pub const COP0_ERROREPC_REG: u32 = 30;
|
||||
//const COP0_UNUSED_31: u32 = 31;
|
||||
pub const COP0_REGS_COUNT: u32 = 32;
|
||||
|
||||
pub const COP0_STATUS_IE: u64 = 1 << 0;
|
||||
pub const COP0_STATUS_EXL: u64 = 1 << 1;
|
||||
pub const COP0_STATUS_ERL: u64 = 1 << 2;
|
||||
pub const COP0_STATUS_SR: u64 = 1 << 20;
|
||||
pub const COP0_STATUS_TS: u64 = 1 << 21;
|
||||
pub const COP0_STATUS_BEV: u64 = 1 << 22;
|
||||
pub const COP0_STATUS_FR: u64 = 1 << 26;
|
||||
pub const COP0_STATUS_CU1: u64 = 1 << 29;
|
||||
@@ -55,6 +57,7 @@ pub const COP0_CAUSE_EXCCODE_CPU: u64 = 11 << 2;
|
||||
pub const COP0_CAUSE_EXCCODE_TR: u64 = 13 << 2;
|
||||
pub const COP0_CAUSE_EXCCODE_FPE: u64 = 15 << 2;
|
||||
pub const COP0_CAUSE_IP2: u64 = 1 << 10;
|
||||
pub const COP0_CAUSE_IP4: u64 = 1 << 12;
|
||||
pub const COP0_CAUSE_IP7: u64 = 1 << 15;
|
||||
pub const COP0_CAUSE_BD: u64 = 1 << 31;
|
||||
|
||||
@@ -62,7 +65,7 @@ pub const COP0_CAUSE_CE1: u64 = 1 << 28;
|
||||
pub const COP0_CAUSE_CE2: u64 = 1 << 29;
|
||||
|
||||
pub const COP0_CAUSE_IP_MASK: u64 = 0b00000000000000001111111100000000;
|
||||
//pub const COP0_CAUSE_EXCCODE_MASK: u64 = 0x1F << 2;
|
||||
pub const COP0_CAUSE_EXCCODE_MASK: u64 = 0x1F << 2;
|
||||
pub const COP0_CONTEXT_BADVPN2_MASK: u64 = 0b00000000011111111111111111110000;
|
||||
pub const COP0_XCONTEXT_BADVPN2_MASK: u64 = 0b01111111111111111111111111110000;
|
||||
pub const COP0_XCONTEXT_REGION_MASK: u64 = 0b110000000000000000000000000000000;
|
||||
@@ -101,7 +104,6 @@ pub struct Cop0 {
|
||||
pub tlb_lut_w: Vec<device::tlb::TlbLut>,
|
||||
pub tlb_entries: [device::tlb::TlbEntry; 32],
|
||||
pub is_event: bool,
|
||||
pub pending_compare_interrupt: bool,
|
||||
}
|
||||
|
||||
fn mfc0(device: &mut device::Device, opcode: u32) {
|
||||
@@ -179,6 +181,8 @@ pub fn reserved(device: &mut device::Device, _opcode: u32) {
|
||||
device::exceptions::reserved_exception(device, 0);
|
||||
}
|
||||
|
||||
pub fn emux_xdetect(_device: &mut device::Device, _opcode: u32) {}
|
||||
|
||||
fn get_control_registers(device: &device::Device, index: u32) -> u64 {
|
||||
match index {
|
||||
COP0_COUNT_REG => device.cpu.cop0.regs[index as usize] >> 1,
|
||||
@@ -218,12 +222,11 @@ fn set_control_registers(device: &mut device::Device, index: u32, mut data: u64)
|
||||
(compare_event_diff as u64) << 1,
|
||||
);
|
||||
device.cpu.cop0.regs[COP0_CAUSE_REG as usize] &= !COP0_CAUSE_IP7;
|
||||
device.cpu.cop0.pending_compare_interrupt = false;
|
||||
}
|
||||
COP0_STATUS_REG => {
|
||||
if data & COP0_STATUS_FR != device.cpu.cop0.regs[index as usize] & COP0_STATUS_FR {
|
||||
device::cop1::set_fgr_registers(device, data)
|
||||
}
|
||||
COP0_STATUS_REG
|
||||
if data & COP0_STATUS_FR != device.cpu.cop0.regs[index as usize] & COP0_STATUS_FR =>
|
||||
{
|
||||
device::cop1::set_fgr_registers(device, data)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -236,7 +239,10 @@ fn set_control_registers(device: &mut device::Device, index: u32, mut data: u64)
|
||||
}
|
||||
|
||||
pub fn compare_event(device: &mut device::Device) {
|
||||
device.cpu.cop0.pending_compare_interrupt = true;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] |= device::cop0::COP0_CAUSE_IP7;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
|
||||
!device::cop0::COP0_CAUSE_EXCCODE_MASK;
|
||||
|
||||
device::events::create_event_at(
|
||||
device,
|
||||
device::events::EVENT_TYPE_COMPARE,
|
||||
@@ -261,38 +267,38 @@ pub fn add_cycles(device: &mut device::Device, cycles: u64) {
|
||||
|
||||
pub fn map_instructions(device: &mut device::Device) {
|
||||
device.cpu.cop0.instrs = [
|
||||
device::cop0::mfc0, // 0
|
||||
device::cop0::dmfc0, // 1
|
||||
device::cop0::reserved, // 2
|
||||
device::cop0::reserved, // 3
|
||||
device::cop0::mtc0, // 4
|
||||
device::cop0::dmtc0, // 5
|
||||
device::cop0::reserved, // 6
|
||||
device::cop0::reserved, // 7
|
||||
device::cop0::reserved, // 8
|
||||
device::cop0::reserved, // 9
|
||||
device::cop0::reserved, // 10
|
||||
device::cop0::reserved, // 11
|
||||
device::cop0::reserved, // 12
|
||||
device::cop0::reserved, // 13
|
||||
device::cop0::reserved, // 14
|
||||
device::cop0::reserved, // 15
|
||||
device::cop0::execute_cp0, // 16
|
||||
device::cop0::reserved, // 17
|
||||
device::cop0::reserved, // 18
|
||||
device::cop0::reserved, // 19
|
||||
device::cop0::reserved, // 20
|
||||
device::cop0::reserved, // 21
|
||||
device::cop0::reserved, // 22
|
||||
device::cop0::reserved, // 23
|
||||
device::cop0::reserved, // 24
|
||||
device::cop0::reserved, // 25
|
||||
device::cop0::reserved, // 26
|
||||
device::cop0::reserved, // 27
|
||||
device::cop0::reserved, // 28
|
||||
device::cop0::reserved, // 29
|
||||
device::cop0::reserved, // 30
|
||||
device::cop0::reserved, // 31
|
||||
device::cop0::mfc0, // 0
|
||||
device::cop0::dmfc0, // 1
|
||||
device::cop0::reserved, // 2
|
||||
device::cop0::reserved, // 3
|
||||
device::cop0::mtc0, // 4
|
||||
device::cop0::dmtc0, // 5
|
||||
device::cop0::reserved, // 6
|
||||
device::cop0::reserved, // 7
|
||||
device::cop0::reserved, // 8
|
||||
device::cop0::reserved, // 9
|
||||
device::cop0::reserved, // 10
|
||||
device::cop0::reserved, // 11
|
||||
device::cop0::reserved, // 12
|
||||
device::cop0::reserved, // 13
|
||||
device::cop0::reserved, // 14
|
||||
device::cop0::reserved, // 15
|
||||
device::cop0::execute_cp0, // 16
|
||||
device::cop0::reserved, // 17
|
||||
device::cop0::reserved, // 18
|
||||
device::cop0::reserved, // 19
|
||||
device::cop0::emux_xdetect, // 20
|
||||
device::cop0::reserved, // 21
|
||||
device::cop0::reserved, // 22
|
||||
device::cop0::reserved, // 23
|
||||
device::cop0::reserved, // 24
|
||||
device::cop0::reserved, // 25
|
||||
device::cop0::reserved, // 26
|
||||
device::cop0::reserved, // 27
|
||||
device::cop0::reserved, // 28
|
||||
device::cop0::reserved, // 29
|
||||
device::cop0::reserved, // 30
|
||||
device::cop0::reserved, // 31
|
||||
];
|
||||
|
||||
device.cpu.cop0.instrs2 = [
|
||||
|
||||
+5
-2
@@ -1,4 +1,4 @@
|
||||
use crate::{device, savestates};
|
||||
use crate::{device, savestates, ui};
|
||||
|
||||
#[derive(PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum State {
|
||||
@@ -227,7 +227,10 @@ pub fn init(device: &mut device::Device) {
|
||||
device.cpu.clock_rate = if !device.cpu.overclock {
|
||||
93750000
|
||||
} else {
|
||||
println!("Overclocking enabled, setting clock rate to 125 MHz");
|
||||
ui::video::onscreen_message(
|
||||
"Overclocking enabled, setting clock rate to 125 MHz",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
125000000
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
|
||||
pub const EVENT_TYPE_NONE: usize = 0;
|
||||
pub const EVENT_TYPE_AI: usize = 1;
|
||||
@@ -12,7 +13,9 @@ pub const EVENT_TYPE_SPDMA: usize = 8;
|
||||
pub const EVENT_TYPE_COMPARE: usize = 9;
|
||||
pub const EVENT_TYPE_VRU: usize = 10;
|
||||
pub const EVENT_TYPE_PAK: usize = 11;
|
||||
pub const EVENT_TYPE_COUNT: usize = 12;
|
||||
pub const EVENT_TYPE_SAVE: usize = 12;
|
||||
pub const EVENT_TYPE_NMI: usize = 13;
|
||||
pub const EVENT_TYPE_COUNT: usize = 14;
|
||||
|
||||
#[derive(PartialEq, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Event {
|
||||
@@ -60,6 +63,8 @@ fn get_event_handler(name: usize) -> fn(&mut device::Device) {
|
||||
EVENT_TYPE_COMPARE => device::cop0::compare_event,
|
||||
EVENT_TYPE_VRU => device::controller::vru::vru_talking_event,
|
||||
EVENT_TYPE_PAK => device::controller::pak_switch_event,
|
||||
EVENT_TYPE_SAVE => ui::storage::save_event,
|
||||
EVENT_TYPE_NMI => device::exceptions::reset_event,
|
||||
_ => dummy_event,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
|
||||
pub fn check_pending_interrupts(device: &mut device::Device) {
|
||||
if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize]
|
||||
@@ -15,10 +16,9 @@ pub fn check_pending_interrupts(device: &mut device::Device) {
|
||||
& device.mi.regs[device::mi::MI_INTR_MASK_REG as usize]
|
||||
!= 0
|
||||
{
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] = device::cop0::COP0_CAUSE_IP2;
|
||||
} else if device.cpu.cop0.pending_compare_interrupt {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] = device::cop0::COP0_CAUSE_IP7;
|
||||
device.cpu.cop0.pending_compare_interrupt = false;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] |= device::cop0::COP0_CAUSE_IP2;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
|
||||
!device::cop0::COP0_CAUSE_EXCCODE_MASK;
|
||||
}
|
||||
|
||||
if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize]
|
||||
@@ -151,6 +151,24 @@ pub fn tlb_miss_exception(
|
||||
exception_general(device, vector_offset)
|
||||
}
|
||||
|
||||
pub fn reset_event(device: &mut device::Device) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &= !device::cop0::COP0_CAUSE_IP4;
|
||||
|
||||
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] |= device::cop0::COP0_STATUS_ERL
|
||||
| device::cop0::COP0_STATUS_SR
|
||||
| device::cop0::COP0_STATUS_BEV;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] &= !device::cop0::COP0_STATUS_TS;
|
||||
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ERROREPC_REG as usize] = device.cpu.pc;
|
||||
device.cpu.pc = 0xBFC00000;
|
||||
device.cpu.branch_state.state = device::cpu::State::Step;
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] = 0;
|
||||
|
||||
device::pif::reset_pif(device, true);
|
||||
|
||||
ui::video::onscreen_message("Game reset", ui::video::MESSAGE_LENGTH_MESSAGE_SHORT);
|
||||
}
|
||||
|
||||
fn exception_general(device: &mut device::Device, vector_offset: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_EXL
|
||||
== 0
|
||||
|
||||
@@ -22,8 +22,8 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
|
||||
let data = std::str::from_utf8(
|
||||
&device.cart.is_viewer_buffer[0x20_usize..(0x20 + length) as usize],
|
||||
);
|
||||
if data.is_ok() {
|
||||
print!("{}", data.unwrap());
|
||||
if let Ok(data) = data {
|
||||
print!("{}", data);
|
||||
}
|
||||
} else {
|
||||
let mut data = u32::from_be_bytes(
|
||||
|
||||
@@ -62,11 +62,16 @@ pub fn masked_write_64(dst: &mut u64, value: u64, mask: u64) {
|
||||
|
||||
pub fn translate_address(
|
||||
device: &mut device::Device,
|
||||
address: u64,
|
||||
mut address: u64,
|
||||
access_type: AccessType,
|
||||
) -> (u64, bool, bool) {
|
||||
if (address & 0xc0000000) != 0x80000000 {
|
||||
return device::tlb::get_physical_address(device, address, access_type);
|
||||
if (address & 0xFFFFFFFF00000000) == 0x9000000000000000 {
|
||||
// this is a hack to support 64-bit addresses used by some homebrew
|
||||
address |= 0xA0000000;
|
||||
} else {
|
||||
return device::tlb::get_physical_address(device, address, access_type);
|
||||
}
|
||||
}
|
||||
(address & 0x1FFFFFFF, address & 0x20000000 == 0, false)
|
||||
}
|
||||
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
use crate::device;
|
||||
|
||||
const MI_INIT_MODE_REG: u32 = 0;
|
||||
pub const MI_INIT_MODE_REG: u32 = 0;
|
||||
const MI_VERSION_REG: u32 = 1;
|
||||
pub const MI_INTR_REG: u32 = 2;
|
||||
pub const MI_INTR_MASK_REG: u32 = 3;
|
||||
@@ -29,7 +29,7 @@ const MI_CLR_DP: u32 = 1 << 10;
|
||||
const MI_SET_DP: u32 = 1 << 11;
|
||||
|
||||
/* mode read */
|
||||
const MI_INIT_MODE: u32 = 1 << 7;
|
||||
pub const MI_INIT_MODE: u32 = 1 << 7;
|
||||
const MI_EBUS_MODE: u32 = 1 << 8;
|
||||
const MI_RDRAM_MODE: u32 = 1 << 9;
|
||||
|
||||
@@ -42,7 +42,7 @@ const MI_CLR_DP_INTR: u32 = 1 << 11;
|
||||
const MI_CLR_RDRAM: u32 = 1 << 12;
|
||||
const MI_SET_RDRAM: u32 = 1 << 13;
|
||||
|
||||
const MI_INIT_LENGTH_MASK: u32 = 0b1111111;
|
||||
pub const MI_INIT_LENGTH_MASK: u32 = 0b1111111;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Mi {
|
||||
@@ -78,9 +78,15 @@ fn update_init_mode(device: &mut device::Device, w: u32) {
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] |= w & MI_INIT_LENGTH_MASK;
|
||||
|
||||
if w & MI_CLR_INIT != 0 {
|
||||
for i in 0..(0x3F00000 >> 16) {
|
||||
device.memory.memory_map_write[i] = device::rdram::write_mem;
|
||||
}
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] &= !MI_INIT_MODE
|
||||
}
|
||||
if w & MI_SET_INIT != 0 {
|
||||
for i in 0..(0x3F00000 >> 16) {
|
||||
device.memory.memory_map_write[i] = device::rdram::write_mem_repeat;
|
||||
}
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] |= MI_INIT_MODE
|
||||
}
|
||||
if w & MI_CLR_EBUS != 0 {
|
||||
|
||||
+6
-2
@@ -1,5 +1,6 @@
|
||||
use crate::device;
|
||||
use rand_chacha::rand_core::RngCore;
|
||||
use crate::ui;
|
||||
use rand::Rng;
|
||||
|
||||
const PI_DRAM_ADDR_REG: u32 = 0;
|
||||
const PI_CART_ADDR_REG: u32 = 1;
|
||||
@@ -92,6 +93,9 @@ fn dma_write(device: &mut device::Device) {
|
||||
length -= dram_addr & 0x7;
|
||||
}
|
||||
|
||||
// check RDP before PI writes to RDRAM
|
||||
ui::video::check_framebuffers(dram_addr, length);
|
||||
|
||||
let cycles = (handler.write)(device, cart_addr, dram_addr, length);
|
||||
|
||||
device::events::create_event(device, device::events::EVENT_TYPE_PI, cycles);
|
||||
@@ -165,7 +169,7 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
}
|
||||
}
|
||||
|
||||
fn randomize_interrupt_time(rng: &mut rand_chacha::ChaCha8Rng) -> u64 {
|
||||
fn randomize_interrupt_time(rng: &mut rand::rngs::Xoshiro256PlusPlus) -> u64 {
|
||||
rng.next_u64() % 0x10
|
||||
}
|
||||
|
||||
|
||||
+24
-15
@@ -218,11 +218,11 @@ pub fn process_ram(device: &mut device::Device) {
|
||||
|
||||
pub fn connect_pif_channels(device: &mut device::Device) {
|
||||
for i in 0..4 {
|
||||
if device.netplay.is_none() {
|
||||
if device.ui.config.input.controller_enabled[i] {
|
||||
if let Some(netplay) = &device.netplay {
|
||||
if netplay.player_data[i].reg_id != 0 {
|
||||
device.pif.channels[i].process = Some(device::controller::process);
|
||||
}
|
||||
} else if device.netplay.as_ref().unwrap().player_data[i].reg_id != 0 {
|
||||
} else if device.ui.config.input.controller_enabled[i] {
|
||||
device.pif.channels[i].process = Some(device::controller::process);
|
||||
}
|
||||
}
|
||||
@@ -249,14 +249,23 @@ fn get_default_handler(device: &device::Device) -> device::controller::PakHandle
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
pub fn reset_pif(device: &mut device::Device, is_nmi_reset: bool) {
|
||||
if device.cart.pal {
|
||||
device.pif.rom = rom::PAL_PIF_ROM;
|
||||
} else {
|
||||
device.pif.rom = rom::NTSC_PIF_ROM;
|
||||
}
|
||||
device.pif.ram[0x26] = device.cart.cic_seed;
|
||||
device.pif.ram[0x27] = device.cart.cic_seed;
|
||||
|
||||
let reset_type = u32::from(is_nmi_reset);
|
||||
let rom_type = 0u32;
|
||||
let s7 = 0u32;
|
||||
let cic = u32::from(device.cart.cic_seed);
|
||||
let word = (rom_type << 19) | (s7 << 18) | (reset_type << 17) | (cic << 8) | 0x3f;
|
||||
device.pif.ram[0x24..0x28].copy_from_slice(&word.to_be_bytes());
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
reset_pif(device, false);
|
||||
|
||||
let default_handler = get_default_handler(device);
|
||||
let tpak_handler = device::controller::PakHandler {
|
||||
@@ -268,16 +277,16 @@ pub fn init(device: &mut device::Device) {
|
||||
connect_pif_channels(device);
|
||||
|
||||
for i in 0..4 {
|
||||
if device.netplay.is_none() {
|
||||
if device.ui.config.input.controller_enabled[i] {
|
||||
if device.transferpaks[i].cart.rom.is_empty() {
|
||||
device.pif.channels[i].pak_handler = Some(default_handler);
|
||||
} else {
|
||||
device.pif.channels[i].pak_handler = Some(tpak_handler);
|
||||
}
|
||||
if let Some(netplay) = &device.netplay {
|
||||
if netplay.player_data[i].reg_id != 0 {
|
||||
device.pif.channels[i].pak_handler = Some(default_handler);
|
||||
}
|
||||
} else if device.ui.config.input.controller_enabled[i] {
|
||||
if device.transferpaks[i].cart.rom.is_empty() {
|
||||
device.pif.channels[i].pak_handler = Some(default_handler);
|
||||
} else {
|
||||
device.pif.channels[i].pak_handler = Some(tpak_handler);
|
||||
}
|
||||
} else if device.netplay.as_ref().unwrap().player_data[i].reg_id != 0 {
|
||||
device.pif.channels[i].pak_handler = Some(default_handler);
|
||||
}
|
||||
}
|
||||
if device.ui.config.input.emulate_vru && device.netplay.is_none() {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::device;
|
||||
use crate::retroachievements;
|
||||
use crate::ui;
|
||||
use std::alloc::{Layout, alloc_zeroed};
|
||||
|
||||
//const RDRAM_CONFIG_REG: u32 = 0;
|
||||
@@ -50,6 +52,8 @@ pub fn read_mem(
|
||||
);
|
||||
let masked_address = address as usize & RDRAM_MASK;
|
||||
|
||||
ui::video::check_framebuffers(masked_address as u32, 4);
|
||||
|
||||
u32::from_ne_bytes(
|
||||
device
|
||||
.rdram
|
||||
@@ -62,6 +66,8 @@ pub fn read_mem(
|
||||
}
|
||||
|
||||
pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
ui::video::check_framebuffers(address as u32, 4);
|
||||
|
||||
let mut data = u32::from_ne_bytes(
|
||||
device
|
||||
.rdram
|
||||
@@ -80,6 +86,39 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
|
||||
.copy_from_slice(&data.to_ne_bytes());
|
||||
}
|
||||
|
||||
pub fn write_mem_repeat(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
if mask != 0xFFFFFFFF {
|
||||
panic!("RDRAM write_mem_repeat called with mask {:#x}", mask);
|
||||
}
|
||||
|
||||
let repeat_length = (device.mi.regs[device::mi::MI_INIT_MODE_REG as usize]
|
||||
& device::mi::MI_INIT_LENGTH_MASK)
|
||||
+ 1;
|
||||
|
||||
if !repeat_length.is_multiple_of(4) {
|
||||
panic!(
|
||||
"RDRAM write_mem_repeat called with non-word-aligned length {}",
|
||||
repeat_length
|
||||
);
|
||||
}
|
||||
|
||||
ui::video::check_framebuffers(address as u32, repeat_length);
|
||||
|
||||
for i in (0..repeat_length as u64).step_by(4) {
|
||||
device
|
||||
.rdram
|
||||
.mem
|
||||
.get_mut((address + i) as usize..(address + i) as usize + 4)
|
||||
.unwrap_or(&mut [0; 4])
|
||||
.copy_from_slice(&value.to_ne_bytes());
|
||||
}
|
||||
|
||||
device.mi.regs[device::mi::MI_INIT_MODE_REG as usize] &= !device::mi::MI_INIT_MODE;
|
||||
for i in 0..(0x3F00000 >> 16) {
|
||||
device.memory.memory_map_write[i] = device::rdram::write_mem;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_regs(
|
||||
device: &mut device::Device,
|
||||
address: u64,
|
||||
@@ -113,6 +152,8 @@ pub fn init(device: &mut device::Device) {
|
||||
device.rdram.mem =
|
||||
unsafe { Vec::from_raw_parts(ptr, device.rdram.size as usize, device.rdram.size as usize) };
|
||||
|
||||
retroachievements::set_dmem(device.rdram.mem.as_ptr(), device.rdram.size as usize);
|
||||
|
||||
// hack, skip RDRAM initialization
|
||||
device
|
||||
.rdram
|
||||
|
||||
+14
-10
@@ -1,4 +1,5 @@
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
|
||||
const SP_MEM_ADDR_REG: u32 = 0;
|
||||
const SP_DRAM_ADDR_REG: u32 = 1;
|
||||
@@ -146,6 +147,7 @@ fn do_dma(device: &mut device::Device, dma: RspDma) {
|
||||
let mut dram_addr = dma.dramaddr & 0xfffff8;
|
||||
let offset = dma.memaddr & 0x1000;
|
||||
|
||||
ui::video::check_framebuffers(dram_addr, count * (length + skip) - skip);
|
||||
if dma.dir == DmaDir::Read {
|
||||
let mut j = 0;
|
||||
while j < count {
|
||||
@@ -219,6 +221,8 @@ fn fifo_push(device: &mut device::Device, dir: DmaDir) {
|
||||
panic!("RSP DMA already full")
|
||||
}
|
||||
|
||||
device.rsp.cpu.sync_point = true;
|
||||
|
||||
if device.rsp.regs[SP_DMA_BUSY_REG as usize] != 0 {
|
||||
device.rsp.fifo[1].dir = dir;
|
||||
if dir == DmaDir::Read {
|
||||
@@ -276,21 +280,20 @@ pub fn read_regs(
|
||||
}
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
SP_DMA_BUSY_REG | SP_DMA_FULL_REG => {
|
||||
if device.rsp.regs[reg as usize] != 0 {
|
||||
device.rsp.cpu.sync_point = true;
|
||||
}
|
||||
device.rsp.regs[reg as usize]
|
||||
}
|
||||
SP_STATUS_REG => {
|
||||
let value = device.rsp.regs[reg as usize]
|
||||
& !(SP_STATUS_HALT | SP_STATUS_BROKE | SP_STATUS_INTR_BREAK);
|
||||
& (SP_STATUS_SIG0
|
||||
| SP_STATUS_SIG1
|
||||
| SP_STATUS_SIG2
|
||||
| SP_STATUS_SIG3
|
||||
| SP_STATUS_SIG4
|
||||
| SP_STATUS_SIG5
|
||||
| SP_STATUS_SIG6
|
||||
| SP_STATUS_SIG7);
|
||||
if value == device.rsp.last_status_value && value != 0 {
|
||||
device.rsp.cpu.sync_point = true;
|
||||
}
|
||||
if value & (SP_STATUS_DMA_BUSY | SP_STATUS_DMA_FULL) != 0 {
|
||||
device.rsp.cpu.sync_point = true;
|
||||
}
|
||||
|
||||
device.rsp.last_status_value = value;
|
||||
device.rsp.regs[reg as usize]
|
||||
}
|
||||
@@ -464,6 +467,7 @@ fn update_sp_status(device: &mut device::Device, w: u32) {
|
||||
fn do_task(device: &mut device::Device) {
|
||||
device.rsp.cpu.sync_point = false;
|
||||
device.rsp.last_status_value = 0;
|
||||
device.rdp.last_status_value = 0;
|
||||
if device.rsp.regs[SP_DMA_BUSY_REG as usize] == 1 {
|
||||
device.rsp.run_after_dma = true
|
||||
} else {
|
||||
|
||||
@@ -304,25 +304,44 @@ pub fn lh(device: &mut device::Device, opcode: u32) {
|
||||
let address =
|
||||
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
|
||||
|
||||
let mut w = [0; 2];
|
||||
w[0] = device.rsp.mem[address as usize & 0xFFF];
|
||||
w[1] = device.rsp.mem[(address as usize + 1) & 0xFFF];
|
||||
// Optimize for aligned access when possible
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = if addr_masked <= 0xFFE {
|
||||
u16::from_be_bytes(
|
||||
device.rsp.mem[addr_masked..addr_masked + 2]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
let w0 = device.rsp.mem[addr_masked];
|
||||
let w1 = device.rsp.mem[(addr_masked + 1) & 0xFFF];
|
||||
((w0 as u16) << 8) | w1 as u16
|
||||
};
|
||||
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] =
|
||||
(((w[0] as u16) << 8) | w[1] as u16) as i16 as i32 as u32
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] = value as i16 as i32 as u32;
|
||||
}
|
||||
|
||||
pub fn lw(device: &mut device::Device, opcode: u32) {
|
||||
let address =
|
||||
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
|
||||
|
||||
let mut w = [0; 4];
|
||||
for (i, item) in w.iter_mut().enumerate() {
|
||||
*item = device.rsp.mem[(address as usize + i) & 0xFFF];
|
||||
}
|
||||
// Optimize for aligned access when possible
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = if addr_masked <= 0xFFC {
|
||||
u32::from_be_bytes(
|
||||
device.rsp.mem[addr_masked..addr_masked + 4]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
let mut value = 0;
|
||||
for i in 0..4 {
|
||||
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u32) << (8 * (3 - i));
|
||||
}
|
||||
value
|
||||
};
|
||||
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] =
|
||||
((w[0] as u32) << 24) | ((w[1] as u32) << 16) | ((w[2] as u32) << 8) | (w[3] as u32)
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] = value;
|
||||
}
|
||||
|
||||
pub fn lbu(device: &mut device::Device, opcode: u32) {
|
||||
@@ -336,24 +355,44 @@ pub fn lhu(device: &mut device::Device, opcode: u32) {
|
||||
let address =
|
||||
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
|
||||
|
||||
let mut w = [0; 2];
|
||||
w[0] = device.rsp.mem[address as usize & 0xFFF];
|
||||
w[1] = device.rsp.mem[(address as usize + 1) & 0xFFF];
|
||||
// Optimize for aligned access when possible
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = if addr_masked <= 0xFFE {
|
||||
u16::from_be_bytes(
|
||||
device.rsp.mem[addr_masked..addr_masked + 2]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
let w0 = device.rsp.mem[addr_masked];
|
||||
let w1 = device.rsp.mem[(addr_masked + 1) & 0xFFF];
|
||||
((w0 as u16) << 8) | w1 as u16
|
||||
};
|
||||
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] = (((w[0] as u16) << 8) | w[1] as u16) as u32
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] = value as u32;
|
||||
}
|
||||
|
||||
pub fn lwu(device: &mut device::Device, opcode: u32) {
|
||||
let address =
|
||||
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
|
||||
|
||||
let mut w = [0; 4];
|
||||
for (i, item) in w.iter_mut().enumerate() {
|
||||
*item = device.rsp.mem[(address as usize + i) & 0xFFF];
|
||||
}
|
||||
// Optimize for aligned access when possible
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = if addr_masked <= 0xFFC {
|
||||
u32::from_be_bytes(
|
||||
device.rsp.mem[addr_masked..addr_masked + 4]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
let mut value = 0;
|
||||
for i in 0..4 {
|
||||
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u32) << (8 * (3 - i));
|
||||
}
|
||||
value
|
||||
};
|
||||
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] =
|
||||
((w[0] as u32) << 24) | ((w[1] as u32) << 16) | ((w[2] as u32) << 8) | (w[3] as u32)
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] = value;
|
||||
}
|
||||
|
||||
pub fn sb(device: &mut device::Device, opcode: u32) {
|
||||
@@ -367,18 +406,30 @@ pub fn sh(device: &mut device::Device, opcode: u32) {
|
||||
let address =
|
||||
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
|
||||
|
||||
device.rsp.mem[address as usize & 0xFFF] = (device.rsp.cpu.gpr[rt(opcode) as usize] >> 8) as u8;
|
||||
device.rsp.mem[(address as usize + 1) & 0xFFF] =
|
||||
(device.rsp.cpu.gpr[rt(opcode) as usize]) as u8;
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = device.rsp.cpu.gpr[rt(opcode) as usize] as u16;
|
||||
// Optimize for aligned access when possible
|
||||
if addr_masked <= 0xFFE {
|
||||
device.rsp.mem[addr_masked..addr_masked + 2].copy_from_slice(&value.to_be_bytes());
|
||||
} else {
|
||||
device.rsp.mem[addr_masked] = (value >> 8) as u8;
|
||||
device.rsp.mem[(addr_masked + 1) & 0xFFF] = value as u8;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sw(device: &mut device::Device, opcode: u32) {
|
||||
let address =
|
||||
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
|
||||
|
||||
for i in 0..4 {
|
||||
device.rsp.mem[(address as usize + i) & 0xFFF] =
|
||||
(device.rsp.cpu.gpr[rt(opcode) as usize] >> ((3 - i) * 8)) as u8;
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = device.rsp.cpu.gpr[rt(opcode) as usize];
|
||||
// Optimize for aligned access when possible
|
||||
if addr_masked <= 0xFFC {
|
||||
device.rsp.mem[addr_masked..addr_masked + 4].copy_from_slice(&value.to_be_bytes());
|
||||
} else {
|
||||
for i in 0..4 {
|
||||
device.rsp.mem[(addr_masked + i) & 0xFFF] = (value >> ((3 - i) * 8)) as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -737,10 +788,21 @@ pub fn llv(device: &mut device::Device, opcode: u32) {
|
||||
let mut element = velement(opcode);
|
||||
|
||||
if element.is_multiple_of(4) {
|
||||
let mut value = 0;
|
||||
for i in 0..4 {
|
||||
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u32) << (8 * (3 - i));
|
||||
}
|
||||
// Optimize for aligned access when possible
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = if addr_masked <= 0xFFC {
|
||||
u32::from_be_bytes(
|
||||
device.rsp.mem[addr_masked..addr_masked + 4]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
let mut value = 0;
|
||||
for i in 0..4 {
|
||||
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u32) << (8 * (3 - i));
|
||||
}
|
||||
value
|
||||
};
|
||||
modify_vpr32(
|
||||
&mut device.rsp.cpu.vpr[rt(opcode) as usize],
|
||||
element / 4,
|
||||
@@ -767,10 +829,21 @@ pub fn ldv(device: &mut device::Device, opcode: u32) {
|
||||
let mut element = velement(opcode);
|
||||
|
||||
if element.is_multiple_of(8) {
|
||||
let mut value = 0;
|
||||
for i in 0..8 {
|
||||
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u64) << (8 * (7 - i));
|
||||
}
|
||||
// Optimize for aligned access when possible
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = if addr_masked <= 0xFF8 {
|
||||
u64::from_be_bytes(
|
||||
device.rsp.mem[addr_masked..addr_masked + 8]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
let mut value = 0;
|
||||
for i in 0..8 {
|
||||
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u64) << (8 * (7 - i));
|
||||
}
|
||||
value
|
||||
};
|
||||
modify_vpr64(
|
||||
&mut device.rsp.cpu.vpr[rt(opcode) as usize],
|
||||
element / 8,
|
||||
@@ -797,10 +870,21 @@ pub fn lqv(device: &mut device::Device, opcode: u32) {
|
||||
let mut element = velement(opcode);
|
||||
|
||||
if element == 0 && address.is_multiple_of(16) {
|
||||
let mut value = 0;
|
||||
for i in 0..16 {
|
||||
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u128) << (8 * (15 - i));
|
||||
}
|
||||
// Optimize for aligned 16-byte access
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = if addr_masked <= 0xFF0 {
|
||||
u128::from_be_bytes(
|
||||
device.rsp.mem[addr_masked..addr_masked + 16]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
let mut value = 0;
|
||||
for i in 0..16 {
|
||||
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u128) << (8 * (15 - i));
|
||||
}
|
||||
value
|
||||
};
|
||||
modify_vpr128(&mut device.rsp.cpu.vpr[rt(opcode) as usize], value);
|
||||
} else {
|
||||
let end = std::cmp::min(16 + element - ((address & 15) as u8), 16);
|
||||
@@ -823,10 +907,21 @@ pub fn lrv(device: &mut device::Device, opcode: u32) {
|
||||
let mut element = 16u8.wrapping_sub(((address & 15) as u8).wrapping_sub(velement(opcode)));
|
||||
address &= !15;
|
||||
if element == 0 {
|
||||
let mut value = 0;
|
||||
for i in 0..16 {
|
||||
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u128) << (8 * (15 - i));
|
||||
}
|
||||
// Optimize for aligned 16-byte access
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = if addr_masked <= 0xFF0 {
|
||||
u128::from_be_bytes(
|
||||
device.rsp.mem[addr_masked..addr_masked + 16]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
let mut value = 0;
|
||||
for i in 0..16 {
|
||||
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u128) << (8 * (15 - i));
|
||||
}
|
||||
value
|
||||
};
|
||||
modify_vpr128(&mut device.rsp.cpu.vpr[rt(opcode) as usize], value);
|
||||
} else {
|
||||
while element < 16 {
|
||||
|
||||
+433
-449
File diff suppressed because it is too large
Load Diff
+5
-3
@@ -1,6 +1,6 @@
|
||||
use rand_chacha::rand_core::RngCore;
|
||||
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
use rand::Rng;
|
||||
|
||||
const SI_DRAM_ADDR_REG: u32 = 0;
|
||||
const SI_PIF_ADDR_RD64B_REG: u32 = 1;
|
||||
@@ -38,7 +38,7 @@ pub fn read_regs(
|
||||
device.si.regs[((address & 0xFFFF) >> 2) as usize]
|
||||
}
|
||||
|
||||
fn randomize_interrupt_time(rng: &mut rand_chacha::ChaCha8Rng) -> u64 {
|
||||
fn randomize_interrupt_time(rng: &mut rand::rngs::Xoshiro256PlusPlus) -> u64 {
|
||||
rng.next_u64() % 0x100
|
||||
}
|
||||
|
||||
@@ -98,6 +98,8 @@ fn copy_pif_rdram(device: &mut device::Device) {
|
||||
i += 4;
|
||||
}
|
||||
} else if device.si.dma_dir == DmaDir::Read {
|
||||
// check RDP before SI writes to RDRAM
|
||||
ui::video::check_framebuffers(dram_addr as u32, device::pif::PIF_RAM_SIZE as u32);
|
||||
let mut i = 0;
|
||||
while i < device::pif::PIF_RAM_SIZE {
|
||||
let data = u32::from_be_bytes(device.pif.ram[i..i + 4].try_into().unwrap());
|
||||
|
||||
+72
-56
@@ -1,5 +1,4 @@
|
||||
use crate::{cheats, device, netplay, ui};
|
||||
use governor::clock::Clock;
|
||||
use crate::{cheats, device, netplay, retroachievements, ui};
|
||||
|
||||
const VI_STATUS_REG: u32 = 0;
|
||||
const VI_ORIGIN_REG: u32 = 1;
|
||||
@@ -24,12 +23,10 @@ pub struct Vi {
|
||||
pub delay: u64,
|
||||
pub field: u32,
|
||||
#[serde(skip)]
|
||||
pub limiter: Option<governor::DefaultDirectRateLimiter>,
|
||||
pub next_pace_deadline: Option<std::time::Instant>,
|
||||
pub count_per_scanline: u64,
|
||||
pub enable_speed_limiter: bool,
|
||||
pub vi_counter: u64,
|
||||
pub last_origin: u32,
|
||||
pub internal_frame_counter: u64,
|
||||
pub min_wait_time: std::time::Duration,
|
||||
pub frame_time: f64,
|
||||
pub limit_freq: u64,
|
||||
@@ -50,17 +47,11 @@ pub fn set_expected_refresh_rate(device: &mut device::Device) {
|
||||
device.vi.count_per_scanline =
|
||||
device.vi.delay / (device.vi.regs[VI_V_SYNC_REG as usize] + 1) as u64;
|
||||
|
||||
create_limiter(device);
|
||||
reset_pace_deadline(device);
|
||||
}
|
||||
|
||||
fn create_limiter(device: &mut device::Device) {
|
||||
let quota = governor::Quota::with_period(std::time::Duration::from_secs_f64(
|
||||
device.vi.frame_time * device.vi.limit_freq as f64,
|
||||
))
|
||||
.unwrap();
|
||||
device.vi.limiter = Some(governor::RateLimiter::direct(quota));
|
||||
let _ = device.vi.limiter.as_ref().unwrap().check();
|
||||
//println!("new limit freq: {}", device.vi.limit_freq);
|
||||
fn reset_pace_deadline(device: &mut device::Device) {
|
||||
device.vi.next_pace_deadline = None;
|
||||
}
|
||||
|
||||
fn set_vertical_interrupt(device: &mut device::Device) {
|
||||
@@ -70,13 +61,10 @@ fn set_vertical_interrupt(device: &mut device::Device) {
|
||||
}
|
||||
|
||||
fn set_current_line(device: &mut device::Device) {
|
||||
let delay = device.vi.delay;
|
||||
let next_vi = device::events::get_event(device, device::events::EVENT_TYPE_VI);
|
||||
if let Some(next_vi) = next_vi {
|
||||
device.vi.regs[VI_CURRENT_REG as usize] = ((delay
|
||||
- (next_vi.count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize]))
|
||||
/ device.vi.count_per_scanline)
|
||||
as u32;
|
||||
if let Some(next_vi) = device::events::get_event(device, device::events::EVENT_TYPE_VI) {
|
||||
device.vi.regs[VI_CURRENT_REG as usize] = ((device.vi.delay.saturating_sub(
|
||||
next_vi.count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize],
|
||||
)) / device.vi.count_per_scanline) as u32;
|
||||
|
||||
// wrap around VI_CURRENT_REG if needed
|
||||
if device.vi.regs[VI_CURRENT_REG as usize] >= device.vi.regs[VI_V_SYNC_REG as usize] {
|
||||
@@ -120,10 +108,10 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
}
|
||||
}
|
||||
VI_ORIGIN_REG => {
|
||||
let current_origin = device.vi.regs[reg as usize];
|
||||
device::memory::masked_write_32(&mut device.vi.regs[reg as usize], value, mask);
|
||||
if device.vi.regs[reg as usize] != device.vi.last_origin {
|
||||
device.vi.last_origin = device.vi.regs[reg as usize];
|
||||
device.vi.internal_frame_counter += 1;
|
||||
if current_origin != device.vi.regs[reg as usize] {
|
||||
let _ = device.ui.video.fps_tx.try_send(true);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -137,29 +125,41 @@ pub fn vertical_interrupt_event(device: &mut device::Device) {
|
||||
if device.cheats.enabled {
|
||||
cheats::execute_cheats(device, device.cheats.cheats.clone());
|
||||
}
|
||||
let mut speed_limiter_toggled = ui::video::check_callback(device);
|
||||
|
||||
if device.netplay.is_some() {
|
||||
netplay::send_sync_check(device);
|
||||
if device.vi.enable_speed_limiter == device.netplay.as_ref().unwrap().fast_forward {
|
||||
ui::video::render_frame();
|
||||
let _ = device.ui.video.vis_tx.try_send(true);
|
||||
|
||||
retroachievements::do_frame();
|
||||
|
||||
let (mut speed_limiter_toggled, paused) = ui::video::check_callback(device);
|
||||
|
||||
if let Some(netplay) = &mut device.netplay {
|
||||
netplay::send_sync_check(netplay, device.cpu.cop0.regs.as_ref());
|
||||
if device.vi.enable_speed_limiter == netplay.fast_forward {
|
||||
speed_limiter_toggled = true;
|
||||
device.vi.enable_speed_limiter = !device.netplay.as_ref().unwrap().fast_forward;
|
||||
device.vi.enable_speed_limiter = !netplay.fast_forward;
|
||||
}
|
||||
}
|
||||
|
||||
device.vi.vi_counter += 1;
|
||||
if device.vi.vi_counter.is_multiple_of(device.vi.limit_freq) && device.vi.enable_speed_limiter {
|
||||
speed_limiter(device, speed_limiter_toggled);
|
||||
}
|
||||
ui::video::update_screen();
|
||||
|
||||
/*
|
||||
let vis = if device.cart.pal { 50 } else { 60 };
|
||||
if device.vi.vi_counter % vis == 0 {
|
||||
println!("FPS: {}", device.vi.internal_frame_counter);
|
||||
device.vi.internal_frame_counter = 0;
|
||||
ui::video::update_screen();
|
||||
device.vi.vi_counter += 1;
|
||||
|
||||
if device.netplay.is_none() && paused {
|
||||
if retroachievements::get_hardcore() {
|
||||
ui::video::onscreen_message(
|
||||
"Cannot pause in RA hardcore mode",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
} else {
|
||||
ui::video::pause_loop(device.vi.frame_time);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
unsafe { sdl3_sys::events::SDL_PumpEvents() };
|
||||
|
||||
/* toggle vi field if in interlaced mode */
|
||||
device.vi.field ^= (device.vi.regs[VI_STATUS_REG as usize] >> 6) & 0x1;
|
||||
@@ -181,35 +181,51 @@ pub fn init(device: &mut device::Device) {
|
||||
}
|
||||
}
|
||||
|
||||
fn speed_limiter(device: &mut device::Device, speed_limiter_toggled: bool) {
|
||||
let result = device.vi.limiter.as_ref().unwrap().check();
|
||||
if let Err(outcome) = result {
|
||||
let dur = outcome.wait_time_from(governor::clock::DefaultClock::default().now());
|
||||
spin_sleep::sleep(dur);
|
||||
if dur < device.vi.min_wait_time {
|
||||
device.vi.min_wait_time = dur;
|
||||
}
|
||||
fn speed_limiter(device: &mut device::Device, mut speed_limiter_toggled: bool) {
|
||||
let interval =
|
||||
std::time::Duration::from_secs_f64(device.vi.frame_time * device.vi.limit_freq as f64);
|
||||
|
||||
let _ = device.vi.limiter.as_ref().unwrap().check();
|
||||
} else {
|
||||
device.vi.min_wait_time = std::time::Duration::from_secs(0);
|
||||
let now = std::time::Instant::now();
|
||||
match device.vi.next_pace_deadline {
|
||||
None => {
|
||||
device.vi.next_pace_deadline = Some(now + interval);
|
||||
speed_limiter_toggled = true;
|
||||
}
|
||||
Some(deadline) => {
|
||||
if now < deadline {
|
||||
let dur = deadline - now;
|
||||
spin_sleep::sleep(dur);
|
||||
if dur < device.vi.min_wait_time {
|
||||
device.vi.min_wait_time = dur;
|
||||
}
|
||||
} else {
|
||||
//println!("did not sleep");
|
||||
device.vi.min_wait_time = std::time::Duration::from_secs(0);
|
||||
}
|
||||
let mut next = deadline + interval;
|
||||
let t = std::time::Instant::now();
|
||||
while next <= t {
|
||||
next += interval;
|
||||
}
|
||||
device.vi.next_pace_deadline = Some(next);
|
||||
}
|
||||
}
|
||||
|
||||
if std::time::Instant::now()
|
||||
.duration_since(device.vi.limit_freq_check)
|
||||
.as_secs_f64()
|
||||
> 1.0
|
||||
if std::time::Instant::now().duration_since(device.vi.limit_freq_check)
|
||||
> std::time::Duration::from_secs(1)
|
||||
{
|
||||
if !speed_limiter_toggled {
|
||||
if device.vi.min_wait_time.as_secs_f64() == 0.0 && device.vi.limit_freq < MAX_LIMIT_FREQ
|
||||
if device.vi.min_wait_time == std::time::Duration::from_secs(0)
|
||||
&& device.vi.limit_freq < MAX_LIMIT_FREQ
|
||||
{
|
||||
device.vi.limit_freq += 1;
|
||||
create_limiter(device);
|
||||
} else if device.vi.min_wait_time.as_secs_f64() > device.vi.frame_time
|
||||
reset_pace_deadline(device);
|
||||
} else if device.vi.min_wait_time
|
||||
> std::time::Duration::from_secs_f64(device.vi.frame_time)
|
||||
&& device.vi.limit_freq > 1
|
||||
{
|
||||
device.vi.limit_freq -= 1;
|
||||
create_limiter(device);
|
||||
reset_pace_deadline(device);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+221
-98
@@ -6,172 +6,295 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
mod cheats;
|
||||
mod device;
|
||||
mod netplay;
|
||||
mod retroachievements;
|
||||
mod savestates;
|
||||
mod ui;
|
||||
use clap::Parser;
|
||||
use ui::gui;
|
||||
use std::io::Error;
|
||||
|
||||
/// N64 emulator
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[command(author, version=env!("GIT_DESCRIBE"), about, long_about = None, arg_required_else_help = if cfg!(feature = "gui") { false } else { true })]
|
||||
struct Args {
|
||||
game: Option<String>,
|
||||
#[arg(short, long)]
|
||||
fullscreen: bool,
|
||||
#[arg(long)]
|
||||
overclock: Option<bool>,
|
||||
#[arg(long)]
|
||||
disable_expansion_pak: Option<bool>,
|
||||
#[arg(long, value_name = "CHEATS_FILE", hide = true)]
|
||||
cheats: Option<String>,
|
||||
#[arg(long, value_name = "NETPLAY_PEER_ADDR", hide = true)]
|
||||
netplay_peer_addr: Option<String>,
|
||||
#[arg(long, value_name = "NETPLAY_PLAYER_NUMBER", hide = true)]
|
||||
netplay_player_number: Option<u8>,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "PROFILE_NAME",
|
||||
help = "Create a new input profile (keyboard/gamepad mappings)."
|
||||
help = "Create a new input profile (keyboard/gamepad mappings)"
|
||||
)]
|
||||
configure_input_profile: Option<String>,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = "Use DirectInput when configuring a new input profile."
|
||||
)]
|
||||
#[arg(long, help = "Use DirectInput when configuring a new input profile")]
|
||||
use_dinput: bool,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "DEADZONE_PERCENTAGE",
|
||||
help = "Used along with --configure-input-profile to set the deadzone for analog sticks"
|
||||
)]
|
||||
deadzone: Option<i32>,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PROFILE_NAME",
|
||||
help = "Must also specify --port. Used to bind a previously created profile to a port"
|
||||
)]
|
||||
bind_input_profile: Option<String>,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = "Lists connected controllers which can be used in --assign-controller"
|
||||
)]
|
||||
list_controllers: bool,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "CONTROLLER_NUMBER",
|
||||
help = "Must also specify --port. Used to assign a controller listed in --list-controllers to a port"
|
||||
)]
|
||||
assign_controller: Option<i32>,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "PORT",
|
||||
help = "Valid values: 1-4. To be used alongside --bind-input-profile and --assign-controller"
|
||||
)]
|
||||
port: Option<usize>,
|
||||
#[arg(
|
||||
short = 'z',
|
||||
long,
|
||||
help = "Clear all input profile bindings and controller assignments"
|
||||
)]
|
||||
clear_input_bindings: bool,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "SLOT",
|
||||
help = "Load savestate from slot 0-9 when starting the game"
|
||||
)]
|
||||
load_state: Option<u32>,
|
||||
#[arg(
|
||||
long = "ra-username",
|
||||
value_name = "USERNAME",
|
||||
help = "Username for RetroAchievements"
|
||||
)]
|
||||
ra_username: Option<String>,
|
||||
#[arg(
|
||||
long = "ra-token",
|
||||
value_name = "TOKEN",
|
||||
help = "Token for RetroAchievements",
|
||||
hide = true
|
||||
)]
|
||||
ra_token: Option<String>,
|
||||
#[arg(
|
||||
long = "ra-password",
|
||||
value_name = "PASSWORD",
|
||||
help = "Password for RetroAchievements"
|
||||
)]
|
||||
ra_password: Option<String>,
|
||||
#[arg(
|
||||
long = "ra-hardcore",
|
||||
help = "Enable Hardcore mode for RetroAchievements"
|
||||
)]
|
||||
ra_hardcore: bool,
|
||||
#[arg(
|
||||
long = "ra-challenge",
|
||||
help = "Enable Challenge Indicators for RetroAchievements"
|
||||
)]
|
||||
ra_challenge: bool,
|
||||
#[arg(
|
||||
long = "ra-leaderboard",
|
||||
help = "Enable Leaderboard Trackers for RetroAchievements"
|
||||
)]
|
||||
ra_leaderboard: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
#[tokio::main(worker_threads = 4)]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let dirs = ui::get_dirs();
|
||||
|
||||
let mut result = std::fs::create_dir_all(dirs.config_dir);
|
||||
if result.is_err() {
|
||||
panic!("could not create config dir: {}", result.err().unwrap())
|
||||
}
|
||||
result = std::fs::create_dir_all(dirs.data_dir.join("saves"));
|
||||
if result.is_err() {
|
||||
panic!("could not create save dir: {}", result.err().unwrap())
|
||||
}
|
||||
result = std::fs::create_dir_all(dirs.data_dir.join("states"));
|
||||
if result.is_err() {
|
||||
panic!("could not create state dir: {}", result.err().unwrap())
|
||||
}
|
||||
|
||||
std::fs::create_dir_all(dirs.config_dir)?;
|
||||
std::fs::create_dir_all(dirs.cache_dir)?;
|
||||
std::fs::create_dir_all(dirs.data_dir.join("saves"))?;
|
||||
std::fs::create_dir_all(dirs.data_dir.join("states"))?;
|
||||
let args = Args::parse();
|
||||
let args_as_strings: Vec<String> = std::env::args().collect();
|
||||
let args_count = args_as_strings.len();
|
||||
if args_count > 1 && args.game.is_none() {
|
||||
if let Some(game) = args.game {
|
||||
let file_path = std::path::Path::new(&game);
|
||||
let Some(rom_contents) = device::get_rom_contents(file_path) else {
|
||||
return Err(Error::other(format!(
|
||||
"Could not read ROM file: {}",
|
||||
file_path.display()
|
||||
)));
|
||||
};
|
||||
|
||||
if let Some(slot) = args.load_state
|
||||
&& slot > 9
|
||||
{
|
||||
return Err(Error::other("Savestate slot must be between 0 and 9"));
|
||||
}
|
||||
|
||||
let mut device = device::Device::new();
|
||||
|
||||
device.ui.config.recent_roms.retain(|x| *x != game);
|
||||
device.ui.config.recent_roms.insert(0, game);
|
||||
device.ui.config.recent_roms.truncate(5);
|
||||
|
||||
if args.fullscreen {
|
||||
device.ui.video.fullscreen = true;
|
||||
} else {
|
||||
device.ui.video.fullscreen = device.ui.config.video.fullscreen;
|
||||
}
|
||||
let overclock = args
|
||||
.overclock
|
||||
.unwrap_or(device.ui.config.emulation.overclock);
|
||||
let disable_expansion_pak = args
|
||||
.disable_expansion_pak
|
||||
.unwrap_or(device.ui.config.emulation.disable_expansion_pak);
|
||||
let cheats = if let Some(cheats_file) = args.cheats
|
||||
&& let Ok(data) = std::fs::read(cheats_file)
|
||||
&& let Ok(cheats) = serde_json::from_slice(&data)
|
||||
{
|
||||
cheats
|
||||
} else {
|
||||
let game_crc = ui::storage::get_game_crc(&rom_contents);
|
||||
ui::config::Cheats::new()
|
||||
.cheats
|
||||
.get(&game_crc)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let mut shutdown_tx = None;
|
||||
|
||||
if let Some(peer_addr) = args.netplay_peer_addr
|
||||
&& let Some(player_number) = args.netplay_player_number
|
||||
{
|
||||
device.netplay = Some(netplay::init(peer_addr.parse().unwrap(), player_number));
|
||||
} else {
|
||||
for i in 0..4 {
|
||||
if device.ui.config.input.transfer_pak[i]
|
||||
&& !device.ui.config.input.gb_rom_path[i].is_empty()
|
||||
&& !device.ui.config.input.gb_ram_path[i].is_empty()
|
||||
{
|
||||
device.transferpaks[i].cart.rom =
|
||||
std::fs::read(&device.ui.config.input.gb_rom_path[i]).unwrap();
|
||||
device.transferpaks[i].cart.ram =
|
||||
std::fs::read(&device.ui.config.input.gb_ram_path[i]).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if device.ui.config.emulation.usb {
|
||||
(shutdown_tx, device.ui.usb) = ui::usb::init();
|
||||
}
|
||||
|
||||
if let Some(username) = args.ra_username {
|
||||
retroachievements::init_client(
|
||||
if cfg!(ra_hardcore_enabled) {
|
||||
args.ra_hardcore
|
||||
} else {
|
||||
false
|
||||
},
|
||||
args.ra_challenge,
|
||||
args.ra_leaderboard,
|
||||
);
|
||||
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
|
||||
if let Some(password) = args.ra_password {
|
||||
retroachievements::login_user(username, password, tx);
|
||||
} else if let Some(token) = args.ra_token {
|
||||
retroachievements::login_token_user(username, token, tx);
|
||||
} else {
|
||||
tx.send(false).unwrap();
|
||||
}
|
||||
|
||||
rx.await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
retroachievements::load_game(&rom_contents, rom_contents.len()).await;
|
||||
device::run_game(
|
||||
&mut device,
|
||||
&rom_contents,
|
||||
ui::GameSettings {
|
||||
overclock,
|
||||
disable_expansion_pak,
|
||||
cheats,
|
||||
load_savestate_slot: args.load_state,
|
||||
},
|
||||
);
|
||||
retroachievements::shutdown_client();
|
||||
|
||||
if device.netplay.is_some() {
|
||||
netplay::close(&mut device);
|
||||
} else {
|
||||
for i in 0..4 {
|
||||
if device.ui.config.input.transfer_pak[i]
|
||||
&& !device.ui.config.input.gb_ram_path[i].is_empty()
|
||||
{
|
||||
tokio::fs::write(
|
||||
&device.ui.config.input.gb_ram_path[i],
|
||||
&device.transferpaks[i].cart.ram,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(shutdown_tx) = &shutdown_tx {
|
||||
ui::usb::close(shutdown_tx);
|
||||
}
|
||||
} else if std::env::args().count() > 1 {
|
||||
let mut ui = ui::Ui::new();
|
||||
|
||||
if args.clear_input_bindings {
|
||||
ui::input::clear_bindings(&mut ui);
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
if args.port.is_some() {
|
||||
let port = args.port.unwrap();
|
||||
if !(1..=4).contains(&port) {
|
||||
println!("Port must be betwen 1 and 4");
|
||||
return;
|
||||
}
|
||||
if let Some(port) = args.port
|
||||
&& !(1..=4).contains(&port)
|
||||
{
|
||||
return Err(Error::other("Port must be between 1 and 4"));
|
||||
}
|
||||
if args.list_controllers {
|
||||
let controllers = ui::input::get_controller_names(&ui);
|
||||
for (i, controller) in controllers.iter().enumerate() {
|
||||
println!("Controller {i}: {controller}");
|
||||
}
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
if args.configure_input_profile.is_some() {
|
||||
if let Some(profile) = args.configure_input_profile {
|
||||
ui::input::configure_input_profile(
|
||||
&mut ui,
|
||||
args.configure_input_profile.unwrap(),
|
||||
profile,
|
||||
args.use_dinput,
|
||||
args.deadzone.unwrap_or(ui::input::DEADZONE_DEFAULT),
|
||||
);
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
if args.assign_controller.is_some() {
|
||||
if args.port.is_none() {
|
||||
println!("Must specify port number");
|
||||
return;
|
||||
}
|
||||
ui::input::assign_controller(
|
||||
&mut ui,
|
||||
args.assign_controller.unwrap(),
|
||||
args.port.unwrap(),
|
||||
);
|
||||
if let Some(assign_controller) = args.assign_controller {
|
||||
let Some(port) = args.port else {
|
||||
return Err(Error::other("Must specify port number"));
|
||||
};
|
||||
ui::input::assign_controller(&mut ui, assign_controller, port);
|
||||
}
|
||||
if args.bind_input_profile.is_some() {
|
||||
if args.port.is_none() {
|
||||
println!("Must specify port number");
|
||||
return;
|
||||
}
|
||||
ui::input::bind_input_profile(
|
||||
&mut ui,
|
||||
args.bind_input_profile.unwrap(),
|
||||
args.port.unwrap(),
|
||||
);
|
||||
}
|
||||
} else if args.game.is_some() {
|
||||
let file_path = std::path::Path::new(args.game.as_ref().unwrap());
|
||||
if let Some(rom_contents) = device::get_rom_contents(file_path) {
|
||||
let handle = std::thread::Builder::new()
|
||||
.name("n64".to_string())
|
||||
.stack_size(env!("N64_STACK_SIZE").parse().unwrap())
|
||||
.spawn(move || {
|
||||
let mut device = device::Device::new();
|
||||
let overclock = device.ui.config.emulation.overclock;
|
||||
let disable_expansion_pak = device.ui.config.emulation.disable_expansion_pak;
|
||||
|
||||
let game_cheats = {
|
||||
let game_crc = ui::storage::get_game_crc(&rom_contents);
|
||||
let cheats = ui::config::Cheats::new();
|
||||
cheats.cheats.get(&game_crc).cloned().unwrap_or_default()
|
||||
};
|
||||
device::run_game(
|
||||
&mut device,
|
||||
rom_contents,
|
||||
ui::gui::GameSettings {
|
||||
fullscreen: args.fullscreen,
|
||||
overclock,
|
||||
disable_expansion_pak,
|
||||
cheats: game_cheats,
|
||||
},
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
handle.join().unwrap();
|
||||
} else {
|
||||
println!("Could not read rom file");
|
||||
return;
|
||||
if let Some(profile) = args.bind_input_profile {
|
||||
let Some(port) = args.port else {
|
||||
return Err(Error::other("Must specify port number"));
|
||||
};
|
||||
ui::input::bind_input_profile(&mut ui, profile, port);
|
||||
}
|
||||
} else {
|
||||
gui::app_window();
|
||||
#[cfg(feature = "gui")]
|
||||
{
|
||||
retroachievements::init_client(false, false, false);
|
||||
ui::gui::app_window();
|
||||
retroachievements::shutdown_client();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+17
-20
@@ -46,14 +46,6 @@ struct InputEvent {
|
||||
plugin: u8,
|
||||
}
|
||||
|
||||
pub const NETPLAY_ERROR_DESYNC: u32 = 0;
|
||||
pub const NETPLAY_ERROR_LOST_CONNECTION: u32 = 1;
|
||||
pub const NETPLAY_ERROR_PLAYER_DISCONNECTED: u32 = 2;
|
||||
pub const NETPLAY_ERROR_PLAYER_1_DISCONNECTED: u32 = 3;
|
||||
pub const NETPLAY_ERROR_PLAYER_2_DISCONNECTED: u32 = 4;
|
||||
pub const NETPLAY_ERROR_PLAYER_3_DISCONNECTED: u32 = 5;
|
||||
pub const NETPLAY_ERROR_PLAYER_4_DISCONNECTED: u32 = 6;
|
||||
|
||||
pub fn send_rtc(netplay: &mut Netplay, rtc: i64) {
|
||||
let mut request: Vec<u8> = [TCP_SEND_RTC].to_vec();
|
||||
let size: u32 = 8;
|
||||
@@ -93,7 +85,7 @@ pub fn send_save(netplay: &mut Netplay, save_type: &str, save_data: &[u8], size:
|
||||
request.extend_from_slice(&(size as u32).to_be_bytes());
|
||||
|
||||
if size > 0 {
|
||||
request.extend(save_data.to_owned());
|
||||
request.extend_from_slice(save_data);
|
||||
}
|
||||
netplay.tcp_stream.write_all(&request).unwrap();
|
||||
}
|
||||
@@ -111,14 +103,13 @@ pub fn receive_save(netplay: &mut Netplay, save_type: &str, save_data: &mut Vec<
|
||||
*save_data = response;
|
||||
}
|
||||
|
||||
pub fn send_sync_check(device: &mut device::Device) {
|
||||
let netplay = device.netplay.as_mut().unwrap();
|
||||
pub fn send_sync_check(netplay: &mut Netplay, regs: &[u64]) {
|
||||
if netplay.vi_counter.is_multiple_of(600) {
|
||||
let mut request: Vec<u8> = [UDP_SYNC_DATA].to_vec();
|
||||
request.extend_from_slice(&(netplay.vi_counter).to_be_bytes());
|
||||
|
||||
for i in 0..device::cop0::COP0_REGS_COUNT as usize {
|
||||
request.extend_from_slice(&(device.cpu.cop0.regs[i] as u32).to_be_bytes());
|
||||
for item in regs.iter() {
|
||||
request.extend_from_slice(&(*item as u32).to_be_bytes());
|
||||
}
|
||||
|
||||
netplay.udp_socket.send(&request).unwrap();
|
||||
@@ -144,7 +135,7 @@ pub fn get_input(device: &mut device::Device, channel: usize) -> ui::input::Inpu
|
||||
let timeout = std::time::Instant::now() + std::time::Duration::from_secs(10);
|
||||
let mut request_timer = std::time::Instant::now() - std::time::Duration::from_millis(5);
|
||||
while input.is_none() {
|
||||
process_incoming(netplay, &device.ui); // we execute process_incoming before request_input so that we send an accurate buffer count
|
||||
process_incoming(netplay); // we execute process_incoming before request_input so that we send an accurate buffer count
|
||||
if std::time::Instant::now() > request_timer {
|
||||
// sends a request packet every 5ms
|
||||
request_input(netplay, channel);
|
||||
@@ -155,7 +146,10 @@ pub fn get_input(device: &mut device::Device, channel: usize) -> ui::input::Inpu
|
||||
.remove(&netplay.player_data[channel].count);
|
||||
|
||||
if std::time::Instant::now() > timeout {
|
||||
ui::audio::play_netplay_audio(&device.ui, NETPLAY_ERROR_LOST_CONNECTION);
|
||||
ui::video::onscreen_message(
|
||||
"Lost connection to netplay server",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
input = Some(InputEvent {
|
||||
input: 0,
|
||||
plugin: 0,
|
||||
@@ -186,7 +180,7 @@ fn request_input(netplay: &Netplay, channel: usize) {
|
||||
netplay.udp_socket.send(&request).unwrap();
|
||||
}
|
||||
|
||||
fn process_incoming(netplay: &mut Netplay, ui: &ui::Ui) {
|
||||
fn process_incoming(netplay: &mut Netplay) {
|
||||
let mut buf: [u8; 1024] = [0; 1024];
|
||||
while let Ok(_incoming) = netplay.udp_socket.recv(&mut buf) {
|
||||
match buf[0] {
|
||||
@@ -200,14 +194,17 @@ fn process_incoming(netplay: &mut Netplay, ui: &ui::Ui) {
|
||||
}
|
||||
if current_status != netplay.status {
|
||||
if ((current_status & 0x1) ^ (netplay.status & 0x1)) != 0 {
|
||||
ui::audio::play_netplay_audio(ui, NETPLAY_ERROR_DESYNC);
|
||||
ui::video::onscreen_message(
|
||||
"Netplay desync detected",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_LONG,
|
||||
);
|
||||
}
|
||||
for dis in 1..5 {
|
||||
if ((current_status & (0x1 << dis)) ^ (netplay.status & (0x1 << dis))) != 0
|
||||
{
|
||||
ui::audio::play_netplay_audio(
|
||||
ui,
|
||||
NETPLAY_ERROR_PLAYER_DISCONNECTED + dis,
|
||||
ui::video::onscreen_message(
|
||||
&format!("Player {dis} disconnected"),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
include!(concat!(env!("OUT_DIR"), "/retroachievements_bindings.rs"));
|
||||
|
||||
use crate::ui;
|
||||
|
||||
static WEB_CLIENT: std::sync::LazyLock<reqwest::Client> = std::sync::LazyLock::new(|| {
|
||||
reqwest::Client::builder()
|
||||
.user_agent(format!(
|
||||
"{}/{}",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("GIT_DESCRIBE")
|
||||
))
|
||||
.build()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct RAConfig {
|
||||
pub username: String,
|
||||
pub token: String,
|
||||
pub enabled: bool,
|
||||
pub hardcore: bool,
|
||||
pub challenge: bool,
|
||||
pub leaderboard: bool,
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn notify_load_game(ctx: *mut std::ffi::c_void) {
|
||||
let tx = unsafe { Box::from_raw(ctx as *mut tokio::sync::oneshot::Sender<bool>) };
|
||||
tx.send(true).unwrap();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn store_retroachievements_credentials(
|
||||
c_username: *const std::ffi::c_char,
|
||||
c_token: *const std::ffi::c_char,
|
||||
ctx: *mut std::ffi::c_void,
|
||||
) {
|
||||
let tx = unsafe { Box::from_raw(ctx as *mut tokio::sync::oneshot::Sender<bool>) };
|
||||
|
||||
if c_username.is_null() || c_token.is_null() {
|
||||
tx.send(false).unwrap();
|
||||
return;
|
||||
}
|
||||
|
||||
let file_path = ui::get_dirs().config_dir.join("retroachievements.json");
|
||||
let raconfig = if let Ok(ra_config) = std::fs::read(&file_path)
|
||||
&& let Ok(result) = serde_json::from_slice::<RAConfig>(ra_config.as_ref())
|
||||
{
|
||||
RAConfig {
|
||||
username: unsafe { std::ffi::CStr::from_ptr(c_username).to_str().unwrap() }.to_string(),
|
||||
token: unsafe { std::ffi::CStr::from_ptr(c_token).to_str().unwrap() }.to_string(),
|
||||
enabled: result.enabled,
|
||||
hardcore: result.hardcore,
|
||||
challenge: result.challenge,
|
||||
leaderboard: result.leaderboard,
|
||||
}
|
||||
} else {
|
||||
RAConfig {
|
||||
username: unsafe { std::ffi::CStr::from_ptr(c_username).to_str().unwrap() }.to_string(),
|
||||
token: unsafe { std::ffi::CStr::from_ptr(c_token).to_str().unwrap() }.to_string(),
|
||||
enabled: false,
|
||||
hardcore: false,
|
||||
challenge: false,
|
||||
leaderboard: false,
|
||||
}
|
||||
};
|
||||
let f = std::fs::File::create(&file_path).unwrap();
|
||||
serde_json::to_writer_pretty(f, &raconfig).unwrap();
|
||||
|
||||
tx.send(true).unwrap();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn rust_server_call(
|
||||
c_url: *const std::ffi::c_char,
|
||||
c_post_data: *const std::ffi::c_char,
|
||||
c_content_type: *const std::ffi::c_char,
|
||||
c_callback: *mut std::ffi::c_void,
|
||||
c_callback_data: *mut std::ffi::c_void,
|
||||
) {
|
||||
let url = unsafe { std::ffi::CStr::from_ptr(c_url).to_str().unwrap() }.to_string();
|
||||
|
||||
let task = if !c_post_data.is_null() {
|
||||
let post_data =
|
||||
unsafe { std::ffi::CStr::from_ptr(c_post_data).to_str().unwrap() }.to_string();
|
||||
let content_type =
|
||||
unsafe { std::ffi::CStr::from_ptr(c_content_type).to_str().unwrap() }.to_string();
|
||||
WEB_CLIENT
|
||||
.post(url)
|
||||
.body(post_data)
|
||||
.header(reqwest::header::CONTENT_TYPE, content_type)
|
||||
.send()
|
||||
} else {
|
||||
WEB_CLIENT.get(url).send()
|
||||
};
|
||||
let callback = c_callback.addr();
|
||||
let callback_data = c_callback_data.addr();
|
||||
tokio::spawn(async move {
|
||||
let response = task.await;
|
||||
match response {
|
||||
Ok(response) => {
|
||||
let status = response.status().as_u16() as i32;
|
||||
match response.text().await {
|
||||
Ok(text) => {
|
||||
let c_text = std::ffi::CString::new(text).unwrap();
|
||||
unsafe {
|
||||
ra_http_callback(
|
||||
c_text.as_ptr(),
|
||||
c_text.count_bytes(),
|
||||
status,
|
||||
callback as *mut std::ffi::c_void,
|
||||
callback_data as *mut std::ffi::c_void,
|
||||
)
|
||||
};
|
||||
}
|
||||
Err(error) => {
|
||||
let c_error = std::ffi::CString::new(error.to_string()).unwrap();
|
||||
unsafe {
|
||||
ra_http_callback(
|
||||
c_error.as_ptr(),
|
||||
c_error.count_bytes(),
|
||||
status,
|
||||
callback as *mut std::ffi::c_void,
|
||||
callback_data as *mut std::ffi::c_void,
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
let c_error = std::ffi::CString::new(error.to_string()).unwrap();
|
||||
unsafe {
|
||||
ra_http_callback(
|
||||
c_error.as_ptr(),
|
||||
c_error.count_bytes(),
|
||||
0,
|
||||
callback as *mut std::ffi::c_void,
|
||||
callback_data as *mut std::ffi::c_void,
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn load_game(rom: &[u8], rom_size: usize) {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
|
||||
unsafe {
|
||||
let tx_ptr = Box::into_raw(Box::new(tx)) as *mut std::ffi::c_void;
|
||||
ra_load_game(rom.as_ptr(), rom_size, tx_ptr);
|
||||
};
|
||||
rx.await.unwrap();
|
||||
}
|
||||
|
||||
pub fn unload_game() {
|
||||
unsafe { ra_unload_game() };
|
||||
}
|
||||
|
||||
pub fn welcome() {
|
||||
unsafe { ra_welcome() };
|
||||
}
|
||||
|
||||
pub fn set_dmem(dmem: *const u8, dmem_size: usize) {
|
||||
unsafe { ra_set_dmem(dmem, dmem_size) };
|
||||
}
|
||||
|
||||
pub fn do_frame() {
|
||||
unsafe { ra_do_frame() };
|
||||
}
|
||||
|
||||
pub fn do_idle() {
|
||||
unsafe { ra_do_idle() };
|
||||
}
|
||||
|
||||
pub fn init_client(hardcore: bool, challenge: bool, leaderboard: bool) {
|
||||
unsafe { ra_init_client(hardcore, challenge, leaderboard) };
|
||||
}
|
||||
|
||||
pub fn shutdown_client() {
|
||||
unsafe { ra_shutdown_client() };
|
||||
}
|
||||
|
||||
pub fn get_hardcore() -> bool {
|
||||
unsafe { ra_get_hardcore() }
|
||||
}
|
||||
|
||||
pub fn login_user(username: String, password: String, tx: tokio::sync::oneshot::Sender<bool>) {
|
||||
unsafe {
|
||||
let tx_ptr = Box::into_raw(Box::new(tx)) as *mut std::ffi::c_void;
|
||||
let c_username = std::ffi::CString::new(username).unwrap();
|
||||
let c_password = std::ffi::CString::new(password).unwrap();
|
||||
ra_login_user(c_username.as_ptr(), c_password.as_ptr(), tx_ptr)
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
pub fn logout_user() {
|
||||
unsafe { ra_logout_user() };
|
||||
}
|
||||
|
||||
pub fn login_token_user(username: String, token: String, tx: tokio::sync::oneshot::Sender<bool>) {
|
||||
unsafe {
|
||||
let tx_ptr = Box::into_raw(Box::new(tx)) as *mut std::ffi::c_void;
|
||||
let c_username = std::ffi::CString::new(username).unwrap();
|
||||
let c_token = std::ffi::CString::new(token).unwrap();
|
||||
ra_login_token_user(c_username.as_ptr(), c_token.as_ptr(), tx_ptr)
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
pub fn get_username() -> Option<String> {
|
||||
let c_username = unsafe { ra_get_username() };
|
||||
if c_username.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
unsafe { std::ffi::CStr::from_ptr(c_username) }
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
pub fn get_token() -> Option<String> {
|
||||
let c_token = unsafe { ra_get_token() };
|
||||
if c_token.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
unsafe { std::ffi::CStr::from_ptr(c_token) }
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state_size() -> usize {
|
||||
unsafe { ra_state_size() }
|
||||
}
|
||||
|
||||
pub fn save_state(state: *mut u8, state_size: usize) {
|
||||
unsafe { ra_save_state(state, state_size) };
|
||||
}
|
||||
|
||||
pub fn load_state(state: *const u8, state_size: usize) {
|
||||
unsafe { ra_load_state(state, state_size) };
|
||||
}
|
||||
+60
-24
@@ -1,4 +1,4 @@
|
||||
use crate::{device, ui};
|
||||
use crate::{cheats, device, retroachievements, ui};
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
use device::__m128i;
|
||||
use serde::de::{Deserialize, Deserializer, SeqAccess, Visitor};
|
||||
@@ -20,7 +20,7 @@ impl<'de, const N: usize> Visitor<'de> for M128iArrayVisitor<N> {
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
let mut arr: [__m128i; N] = [device::zero_m128i(); N];
|
||||
for (index, item) in arr.iter_mut().enumerate().take(N) {
|
||||
for (index, item) in arr.iter_mut().enumerate() {
|
||||
match seq.next_element::<u128>()? {
|
||||
Some(value) => *item = unsafe { std::mem::transmute::<u128, __m128i>(value) },
|
||||
None => return Err(serde::de::Error::invalid_length(index, &self)),
|
||||
@@ -73,6 +73,9 @@ pub fn create_savestate(device: &device::Device) {
|
||||
let mut rdp_state: Vec<u8> = vec![0; ui::video::state_size()];
|
||||
ui::video::save_state(rdp_state.as_mut_ptr());
|
||||
|
||||
let mut ra_state: Vec<u8> = vec![0; retroachievements::state_size()];
|
||||
retroachievements::save_state(ra_state.as_mut_ptr(), ra_state.len());
|
||||
|
||||
let data: &[(&[u8], &str)] = &[
|
||||
(&postcard::to_stdvec(device).unwrap(), "device"),
|
||||
(
|
||||
@@ -80,25 +83,36 @@ pub fn create_savestate(device: &device::Device) {
|
||||
"saves",
|
||||
),
|
||||
(&rdp_state, "rdp_state"),
|
||||
(&ra_state, "ra_state"),
|
||||
];
|
||||
let save_path = device.ui.storage.paths.savestate_file_path.clone();
|
||||
let compressed_file = ui::storage::compress_file(data);
|
||||
std::fs::write(
|
||||
device.ui.storage.paths.savestate_file_path.clone(),
|
||||
compressed_file,
|
||||
)
|
||||
.unwrap();
|
||||
println!(
|
||||
"Savestate created at {}",
|
||||
device.ui.storage.paths.savestate_file_path.display()
|
||||
tokio::spawn(async move {
|
||||
tokio::fs::write(save_path, compressed_file).await.unwrap();
|
||||
});
|
||||
ui::video::onscreen_message(
|
||||
&format!(
|
||||
"Savestate created in slot {}",
|
||||
device.ui.storage.save_state_slot
|
||||
),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn load_savestate(device: &mut device::Device) {
|
||||
if retroachievements::get_hardcore() {
|
||||
ui::video::onscreen_message(
|
||||
"Cannot load savestate in RA hardcore mode",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
return;
|
||||
}
|
||||
let savestate = std::fs::read(&device.ui.storage.paths.savestate_file_path);
|
||||
if let Ok(savestate) = &savestate {
|
||||
let device_bytes = ui::storage::decompress_file(savestate, "device");
|
||||
let save_bytes = ui::storage::decompress_file(savestate, "saves");
|
||||
let rdp_state = ui::storage::decompress_file(savestate, "rdp_state");
|
||||
let ra_state = ui::storage::decompress_file(savestate, "ra_state");
|
||||
if let Ok(state) = postcard::from_bytes::<device::Device>(&device_bytes) {
|
||||
device.ui.storage.saves = postcard::from_bytes(&save_bytes).unwrap();
|
||||
|
||||
@@ -160,28 +174,22 @@ pub fn load_savestate(device: &mut device::Device) {
|
||||
|
||||
device::pif::connect_pif_channels(device);
|
||||
for i in 0..4 {
|
||||
if device.pif.channels[i].pak_handler.is_some() {
|
||||
if device.pif.channels[i].pak_handler.unwrap().pak_type
|
||||
== device::controller::PakType::RumblePak
|
||||
{
|
||||
if let Some(handler) = device.pif.channels[i].pak_handler {
|
||||
if handler.pak_type == device::controller::PakType::RumblePak {
|
||||
let rumblepak_handler = device::controller::PakHandler {
|
||||
read: device::controller::rumble::read,
|
||||
write: device::controller::rumble::write,
|
||||
pak_type: device::controller::PakType::RumblePak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(rumblepak_handler);
|
||||
} else if device.pif.channels[i].pak_handler.unwrap().pak_type
|
||||
== device::controller::PakType::MemPak
|
||||
{
|
||||
} else if handler.pak_type == device::controller::PakType::MemPak {
|
||||
let mempak_handler = device::controller::PakHandler {
|
||||
read: device::controller::mempak::read,
|
||||
write: device::controller::mempak::write,
|
||||
pak_type: device::controller::PakType::MemPak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(mempak_handler);
|
||||
} else if device.pif.channels[i].pak_handler.unwrap().pak_type
|
||||
== device::controller::PakType::TransferPak
|
||||
{
|
||||
} else if handler.pak_type == device::controller::PakType::TransferPak {
|
||||
let tpak_handler = device::controller::PakHandler {
|
||||
read: device::controller::transferpak::read,
|
||||
write: device::controller::transferpak::write,
|
||||
@@ -195,13 +203,41 @@ pub fn load_savestate(device: &mut device::Device) {
|
||||
ui::audio::close_game_audio(&mut device.ui);
|
||||
ui::audio::init_game_audio(&mut device.ui, device.ai.freq);
|
||||
ui::video::load_state(device, rdp_state.as_ptr());
|
||||
println!(
|
||||
"Savestate loaded from {}",
|
||||
device.ui.storage.paths.savestate_file_path.display()
|
||||
|
||||
if !ra_state.is_empty() {
|
||||
retroachievements::load_state(ra_state.as_ptr(), ra_state.len());
|
||||
} else {
|
||||
retroachievements::load_state(std::ptr::null(), 0);
|
||||
}
|
||||
|
||||
if device.cheats.enabled {
|
||||
cheats::execute_cheats(device, device.cheats.cheats.clone());
|
||||
}
|
||||
|
||||
ui::video::onscreen_message(
|
||||
&format!(
|
||||
"Savestate loaded from slot {}",
|
||||
device.ui.storage.save_state_slot
|
||||
),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
} else {
|
||||
println!("Failed to load savestate");
|
||||
ui::video::onscreen_message(
|
||||
&format!(
|
||||
"Failed to load savestate from slot {}",
|
||||
device.ui.storage.save_state_slot
|
||||
),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ui::video::onscreen_message(
|
||||
&format!(
|
||||
"Could not find savestate in slot {}",
|
||||
device.ui.storage.save_state_slot
|
||||
),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
pub mod audio;
|
||||
#[cfg(feature = "gui")]
|
||||
pub mod cheats;
|
||||
pub mod config;
|
||||
#[cfg(feature = "gui")]
|
||||
pub mod gui;
|
||||
pub mod input;
|
||||
#[cfg(feature = "gui")]
|
||||
pub mod netplay;
|
||||
#[cfg(feature = "gui")]
|
||||
pub mod retroachievements;
|
||||
pub mod storage;
|
||||
pub mod usb;
|
||||
pub mod video;
|
||||
#[cfg(feature = "gui")]
|
||||
pub mod vru;
|
||||
|
||||
pub struct Dirs {
|
||||
pub config_dir: std::path::PathBuf,
|
||||
pub data_dir: std::path::PathBuf,
|
||||
pub cache_dir: std::path::PathBuf,
|
||||
}
|
||||
|
||||
pub struct Audio {
|
||||
pub audio_device_spec: sdl3_sys::audio::SDL_AudioSpec,
|
||||
pub audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
|
||||
pub event_audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
|
||||
pub audio_device: u32,
|
||||
pub event_audio: audio::EventAudio,
|
||||
pub gain: f32,
|
||||
}
|
||||
|
||||
@@ -32,11 +36,29 @@ pub struct Storage {
|
||||
pub save_type: Vec<storage::SaveTypes>,
|
||||
pub paths: storage::Paths,
|
||||
pub saves: storage::Saves,
|
||||
pub save_state_slot: u32,
|
||||
}
|
||||
|
||||
pub struct Video {
|
||||
pub window: *mut sdl3_sys::video::SDL_Window,
|
||||
pub fullscreen: bool,
|
||||
pub fps_tx: tokio::sync::mpsc::Sender<bool>,
|
||||
pub fps_rx: Option<tokio::sync::mpsc::Receiver<bool>>,
|
||||
pub vis_tx: tokio::sync::mpsc::Sender<bool>,
|
||||
pub vis_rx: Option<tokio::sync::mpsc::Receiver<bool>>,
|
||||
}
|
||||
|
||||
pub struct Usb {
|
||||
pub usb_tx: Option<tokio::sync::broadcast::Sender<usb::UsbData>>,
|
||||
pub cart_rx: Option<tokio::sync::broadcast::Receiver<usb::UsbData>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GameSettings {
|
||||
pub overclock: bool,
|
||||
pub disable_expansion_pak: bool,
|
||||
pub cheats: std::collections::HashMap<String, Option<String>>,
|
||||
pub load_savestate_slot: Option<u32>,
|
||||
}
|
||||
|
||||
pub struct Ui {
|
||||
@@ -49,12 +71,14 @@ pub struct Ui {
|
||||
pub input: Input,
|
||||
pub storage: Storage,
|
||||
pub video: Video,
|
||||
pub usb: Usb,
|
||||
}
|
||||
|
||||
impl Drop for Ui {
|
||||
fn drop(&mut self) {
|
||||
if self.with_sdl {
|
||||
unsafe {
|
||||
sdl3_ttf_sys::ttf::TTF_Quit();
|
||||
sdl3_sys::init::SDL_Quit();
|
||||
}
|
||||
}
|
||||
@@ -63,12 +87,23 @@ impl Drop for Ui {
|
||||
|
||||
pub fn sdl_init(flag: sdl3_sys::init::SDL_InitFlags) {
|
||||
unsafe {
|
||||
let init = sdl3_sys::init::SDL_WasInit(0);
|
||||
if init & flag == 0 && !sdl3_sys::init::SDL_InitSubSystem(flag) {
|
||||
if sdl3_sys::init::SDL_WasInit(flag) == 0 && !sdl3_sys::init::SDL_InitSubSystem(flag) {
|
||||
let err = std::ffi::CStr::from_ptr(sdl3_sys::error::SDL_GetError())
|
||||
.to_str()
|
||||
.unwrap();
|
||||
panic!("Could not initialize SDL subsystem: {flag}, {err}");
|
||||
panic!(
|
||||
"Could not initialize SDL subsystem: {}, {}",
|
||||
u32::from(flag),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ttf_init() {
|
||||
unsafe {
|
||||
if !sdl3_ttf_sys::ttf::TTF_Init() {
|
||||
panic!("Could not initialize TTF");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,26 +112,27 @@ pub fn get_dirs() -> Dirs {
|
||||
let exe_path = std::env::current_exe().unwrap();
|
||||
let portable_dir = exe_path.parent();
|
||||
let portable = portable_dir.unwrap().join("portable.txt").exists();
|
||||
let config_dir;
|
||||
let data_dir;
|
||||
if portable {
|
||||
config_dir = portable_dir.unwrap().join("portable_data").join("config");
|
||||
data_dir = portable_dir.unwrap().join("portable_data").join("data");
|
||||
Dirs {
|
||||
config_dir: portable_dir.unwrap().join("portable_data").join("config"),
|
||||
data_dir: portable_dir.unwrap().join("portable_data").join("data"),
|
||||
cache_dir: portable_dir.unwrap().join("portable_data").join("cache"),
|
||||
}
|
||||
} else {
|
||||
config_dir = dirs::config_dir().unwrap().join("gopher64");
|
||||
data_dir = dirs::data_dir().unwrap().join("gopher64");
|
||||
};
|
||||
|
||||
Dirs {
|
||||
config_dir,
|
||||
data_dir,
|
||||
Dirs {
|
||||
config_dir: dirs::config_dir().unwrap().join("gopher64"),
|
||||
data_dir: dirs::data_dir().unwrap().join("gopher64"),
|
||||
cache_dir: dirs::cache_dir().unwrap().join("gopher64"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ui {
|
||||
fn construct_ui(joysticks: Vec<u32>, with_sdl: bool) -> Ui {
|
||||
fn construct_ui(joysticks: Vec<sdl3_sys::everything::SDL_JoystickID>, with_sdl: bool) -> Ui {
|
||||
let dirs = get_dirs();
|
||||
|
||||
let (fps_tx, fps_rx) = tokio::sync::mpsc::channel(1000);
|
||||
let (vis_tx, vis_rx) = tokio::sync::mpsc::channel(1000);
|
||||
Ui {
|
||||
input: Input {
|
||||
controllers: [
|
||||
@@ -125,6 +161,7 @@ impl Ui {
|
||||
joysticks,
|
||||
},
|
||||
storage: Storage {
|
||||
save_state_slot: 0,
|
||||
save_type: vec![],
|
||||
paths: storage::Paths {
|
||||
eep_file_path: std::path::PathBuf::new(),
|
||||
@@ -139,27 +176,27 @@ impl Ui {
|
||||
write_to_disk: true,
|
||||
eeprom: storage::Save {
|
||||
data: Vec::new(),
|
||||
written: false,
|
||||
write_pending: false,
|
||||
},
|
||||
sram: storage::Save {
|
||||
data: Vec::new(),
|
||||
written: false,
|
||||
write_pending: false,
|
||||
},
|
||||
flash: storage::Save {
|
||||
data: Vec::new(),
|
||||
written: false,
|
||||
write_pending: false,
|
||||
},
|
||||
mempak: storage::Save {
|
||||
data: Vec::new(),
|
||||
written: false,
|
||||
write_pending: false,
|
||||
},
|
||||
sdcard: storage::Save {
|
||||
data: Vec::new(),
|
||||
written: false,
|
||||
write_pending: false,
|
||||
},
|
||||
romsave: storage::RomSave {
|
||||
data: std::collections::HashMap::new(),
|
||||
written: false,
|
||||
write_pending: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -167,30 +204,20 @@ impl Ui {
|
||||
game_id: String::new(),
|
||||
game_hash: String::new(),
|
||||
audio: Audio {
|
||||
event_audio: audio::EventAudio {
|
||||
mempak: include_bytes!("../data/mempak.wav").to_vec(),
|
||||
rumblepak: include_bytes!("../data/rumblepak.wav").to_vec(),
|
||||
transferpak: include_bytes!("../data/transferpak.wav").to_vec(),
|
||||
netplay_desync: include_bytes!("../data/netplay_desync.wav").to_vec(),
|
||||
netplay_lost_connection: include_bytes!("../data/netplay_lost_connection.wav")
|
||||
.to_vec(),
|
||||
netplay_disconnected: [
|
||||
include_bytes!("../data/netplay_p1_disconnected.wav").to_vec(),
|
||||
include_bytes!("../data/netplay_p2_disconnected.wav").to_vec(),
|
||||
include_bytes!("../data/netplay_p3_disconnected.wav").to_vec(),
|
||||
include_bytes!("../data/netplay_p4_disconnected.wav").to_vec(),
|
||||
],
|
||||
cheats_enabled: include_bytes!("../data/cheats_enabled.wav").to_vec(),
|
||||
},
|
||||
audio_device_spec: Default::default(),
|
||||
audio_stream: std::ptr::null_mut(),
|
||||
event_audio_stream: std::ptr::null_mut(),
|
||||
audio_device: 0,
|
||||
gain: 1.0,
|
||||
},
|
||||
video: Video {
|
||||
window: std::ptr::null_mut(),
|
||||
fullscreen: false,
|
||||
fps_tx,
|
||||
fps_rx: Some(fps_rx),
|
||||
vis_tx,
|
||||
vis_rx: Some(vis_rx),
|
||||
},
|
||||
usb: Usb {
|
||||
usb_tx: None,
|
||||
cart_rx: None,
|
||||
},
|
||||
dirs,
|
||||
with_sdl,
|
||||
|
||||
+55
-141
@@ -1,63 +1,8 @@
|
||||
use crate::device;
|
||||
use crate::netplay;
|
||||
use crate::ui;
|
||||
|
||||
pub struct EventAudio {
|
||||
pub mempak: Vec<u8>,
|
||||
pub rumblepak: Vec<u8>,
|
||||
pub transferpak: Vec<u8>,
|
||||
pub netplay_desync: Vec<u8>,
|
||||
pub netplay_lost_connection: Vec<u8>,
|
||||
pub netplay_disconnected: [Vec<u8>; 4],
|
||||
pub cheats_enabled: Vec<u8>,
|
||||
}
|
||||
|
||||
pub fn init(ui: &mut ui::Ui, frequency: u64) {
|
||||
ui::sdl_init(sdl3_sys::init::SDL_INIT_AUDIO);
|
||||
|
||||
let device_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
|
||||
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
|
||||
freq: 48000,
|
||||
channels: 2,
|
||||
};
|
||||
ui.audio.audio_device = unsafe {
|
||||
sdl3_sys::audio::SDL_OpenAudioDevice(
|
||||
sdl3_sys::audio::SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
|
||||
&device_audio_spec,
|
||||
)
|
||||
};
|
||||
if ui.audio.audio_device == 0 {
|
||||
panic!("Could not open audio device");
|
||||
}
|
||||
|
||||
if !unsafe {
|
||||
sdl3_sys::audio::SDL_GetAudioDeviceFormat(
|
||||
ui.audio.audio_device,
|
||||
&mut ui.audio.audio_device_spec,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
} {
|
||||
panic!("Could not get audio device format");
|
||||
}
|
||||
|
||||
let wav_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
|
||||
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
|
||||
freq: 24000,
|
||||
channels: 1,
|
||||
};
|
||||
|
||||
ui.audio.event_audio_stream = unsafe {
|
||||
sdl3_sys::audio::SDL_CreateAudioStream(&wav_audio_spec, &ui.audio.audio_device_spec)
|
||||
};
|
||||
if !unsafe {
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.event_audio_stream, ui.audio.gain)
|
||||
&& sdl3_sys::audio::SDL_BindAudioStream(
|
||||
ui.audio.audio_device,
|
||||
ui.audio.event_audio_stream,
|
||||
)
|
||||
} {
|
||||
panic!("Could not bind audio stream");
|
||||
}
|
||||
init_game_audio(ui, frequency);
|
||||
}
|
||||
|
||||
@@ -69,29 +14,26 @@ pub fn init_game_audio(ui: &mut ui::Ui, frequency: u64) {
|
||||
};
|
||||
|
||||
ui.audio.audio_stream = unsafe {
|
||||
sdl3_sys::audio::SDL_CreateAudioStream(&game_audio_spec, &ui.audio.audio_device_spec)
|
||||
sdl3_sys::audio::SDL_OpenAudioDeviceStream(
|
||||
sdl3_sys::audio::SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
|
||||
&game_audio_spec,
|
||||
None,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ui.audio.audio_stream.is_null() {
|
||||
return;
|
||||
panic!("Could not create audio stream");
|
||||
}
|
||||
if !unsafe {
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain)
|
||||
&& sdl3_sys::audio::SDL_BindAudioStream(ui.audio.audio_device, ui.audio.audio_stream)
|
||||
&& sdl3_sys::audio::SDL_ResumeAudioStreamDevice(ui.audio.audio_stream)
|
||||
} {
|
||||
panic!("Could not bind audio stream");
|
||||
panic!("Could not resume audio stream");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(ui: &mut ui::Ui) {
|
||||
close_game_audio(ui);
|
||||
unsafe {
|
||||
if !ui.audio.event_audio_stream.is_null() {
|
||||
sdl3_sys::audio::SDL_DestroyAudioStream(ui.audio.event_audio_stream);
|
||||
ui.audio.event_audio_stream = std::ptr::null_mut();
|
||||
}
|
||||
sdl3_sys::audio::SDL_CloseAudioDevice(ui.audio.audio_device);
|
||||
ui.audio.audio_device = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_game_audio(ui: &mut ui::Ui) {
|
||||
@@ -110,8 +52,11 @@ pub fn lower_audio_volume(ui: &mut ui::Ui) {
|
||||
ui.audio.gain = 0.0;
|
||||
}
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain);
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.event_audio_stream, ui.audio.gain);
|
||||
}
|
||||
ui::video::onscreen_message(
|
||||
&format!("Audio volume: {:.0}%", ui.audio.gain * 100.0),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn raise_audio_volume(ui: &mut ui::Ui) {
|
||||
@@ -121,77 +66,31 @@ pub fn raise_audio_volume(ui: &mut ui::Ui) {
|
||||
ui.audio.gain = 2.0;
|
||||
}
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain);
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.event_audio_stream, ui.audio.gain);
|
||||
}
|
||||
ui::video::onscreen_message(
|
||||
&format!("Audio volume: {:.0}%", ui.audio.gain * 100.0),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn play_cheat_event(ui: &ui::Ui) {
|
||||
if ui.audio.event_audio_stream.is_null() {
|
||||
return;
|
||||
}
|
||||
let sound = &ui.audio.event_audio.cheats_enabled;
|
||||
if !unsafe {
|
||||
sdl3_sys::audio::SDL_PutAudioStreamData(
|
||||
ui.audio.event_audio_stream,
|
||||
sound.as_ptr() as *const std::ffi::c_void,
|
||||
sound.len() as i32,
|
||||
)
|
||||
} {
|
||||
panic!("Could not play audio");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play_netplay_audio(ui: &ui::Ui, error: u32) {
|
||||
if ui.audio.event_audio_stream.is_null() {
|
||||
return;
|
||||
}
|
||||
let sound = match error {
|
||||
netplay::NETPLAY_ERROR_DESYNC => &ui.audio.event_audio.netplay_desync,
|
||||
netplay::NETPLAY_ERROR_LOST_CONNECTION => &ui.audio.event_audio.netplay_lost_connection,
|
||||
netplay::NETPLAY_ERROR_PLAYER_1_DISCONNECTED => {
|
||||
&ui.audio.event_audio.netplay_disconnected[0]
|
||||
}
|
||||
netplay::NETPLAY_ERROR_PLAYER_2_DISCONNECTED => {
|
||||
&ui.audio.event_audio.netplay_disconnected[1]
|
||||
}
|
||||
netplay::NETPLAY_ERROR_PLAYER_3_DISCONNECTED => {
|
||||
&ui.audio.event_audio.netplay_disconnected[2]
|
||||
}
|
||||
netplay::NETPLAY_ERROR_PLAYER_4_DISCONNECTED => {
|
||||
&ui.audio.event_audio.netplay_disconnected[3]
|
||||
}
|
||||
_ => panic!("Invalid netplay error"),
|
||||
};
|
||||
if !unsafe {
|
||||
sdl3_sys::audio::SDL_PutAudioStreamData(
|
||||
ui.audio.event_audio_stream,
|
||||
sound.as_ptr() as *const std::ffi::c_void,
|
||||
sound.len() as i32,
|
||||
)
|
||||
} {
|
||||
panic!("Could not play audio");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play_pak_switch(ui: &ui::Ui, pak: device::controller::PakType) {
|
||||
if ui.audio.event_audio_stream.is_null() {
|
||||
fn adjust_audio_frequency(device: &device::Device, frequency: f32) {
|
||||
if !device.vi.enable_speed_limiter {
|
||||
return;
|
||||
}
|
||||
|
||||
let sound = match pak {
|
||||
device::controller::PakType::RumblePak => &ui.audio.event_audio.rumblepak,
|
||||
device::controller::PakType::MemPak => &ui.audio.event_audio.mempak,
|
||||
device::controller::PakType::TransferPak => &ui.audio.event_audio.transferpak,
|
||||
_ => panic!("Invalid pak type"),
|
||||
};
|
||||
if !unsafe {
|
||||
sdl3_sys::audio::SDL_PutAudioStreamData(
|
||||
ui.audio.event_audio_stream,
|
||||
sound.as_ptr() as *const std::ffi::c_void,
|
||||
sound.len() as i32,
|
||||
)
|
||||
} {
|
||||
panic!("Could not play audio");
|
||||
unsafe {
|
||||
let current_ratio =
|
||||
sdl3_sys::everything::SDL_GetAudioStreamFrequencyRatio(device.ui.audio.audio_stream);
|
||||
sdl3_sys::everything::SDL_SetAudioStreamFrequencyRatio(
|
||||
device.ui.audio.audio_stream,
|
||||
(current_ratio + frequency).clamp(0.995, 1.005),
|
||||
);
|
||||
/*
|
||||
println!(
|
||||
"Adjusted audio frequency ratio to {}",
|
||||
sdl3_sys::everything::SDL_GetAudioStreamFrequencyRatio(device.ui.audio.audio_stream)
|
||||
);
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,11 +114,11 @@ pub fn play_audio(device: &device::Device, dram_addr: usize, length: u64) {
|
||||
|
||||
let audio_queued =
|
||||
unsafe { sdl3_sys::audio::SDL_GetAudioStreamQueued(device.ui.audio.audio_stream) } as f64;
|
||||
let acceptable_latency = (device.ai.freq as f64 * 0.2) * 4.0;
|
||||
let min_latency = (device.ai.freq as f64 * 0.02) * 4.0;
|
||||
let samples_per_frame = device.ai.freq as f64 * device.vi.frame_time * 4.0;
|
||||
let max_latency = samples_per_frame * 8.0;
|
||||
|
||||
if audio_queued < min_latency {
|
||||
let silence_buffer: Vec<u8> = vec![0; min_latency as usize & !3];
|
||||
if audio_queued < samples_per_frame {
|
||||
let silence_buffer: Vec<u8> = vec![0; samples_per_frame as usize & !3];
|
||||
if !unsafe {
|
||||
sdl3_sys::audio::SDL_PutAudioStreamData(
|
||||
device.ui.audio.audio_stream,
|
||||
@@ -229,17 +128,32 @@ pub fn play_audio(device: &device::Device, dram_addr: usize, length: u64) {
|
||||
} {
|
||||
panic!("Could not play audio");
|
||||
}
|
||||
/*
|
||||
println!(
|
||||
"Audio underrun: queued {} samples, expected at least {} samples",
|
||||
audio_queued, samples_per_frame
|
||||
);
|
||||
*/
|
||||
adjust_audio_frequency(device, -0.0005);
|
||||
}
|
||||
|
||||
if audio_queued < acceptable_latency
|
||||
&& !unsafe {
|
||||
if audio_queued < max_latency {
|
||||
if !unsafe {
|
||||
sdl3_sys::audio::SDL_PutAudioStreamData(
|
||||
device.ui.audio.audio_stream,
|
||||
primary_buffer.as_ptr() as *const std::ffi::c_void,
|
||||
primary_buffer.len() as i32 * 2,
|
||||
)
|
||||
} {
|
||||
panic!("Could not play audio");
|
||||
}
|
||||
{
|
||||
panic!("Could not play audio");
|
||||
} else {
|
||||
/*
|
||||
println!(
|
||||
"Audio overrun: queued {} samples, expected at most {} samples",
|
||||
audio_queued, max_latency
|
||||
);
|
||||
*/
|
||||
adjust_audio_frequency(device, 0.0005);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-32
@@ -1,18 +1,10 @@
|
||||
use crate::cheats;
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
use crate::ui::gui::AppWindow;
|
||||
use crate::ui::gui::ErrorDialog;
|
||||
use slint::ComponentHandle;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone)]
|
||||
pub struct CheatData {
|
||||
note: String,
|
||||
pub data: Vec<String>,
|
||||
pub options: Option<std::collections::BTreeMap<String, String>>,
|
||||
}
|
||||
|
||||
pub type Cheats = std::collections::BTreeMap<String, std::collections::BTreeMap<String, CheatData>>;
|
||||
|
||||
pub fn cheats_window(app: &AppWindow) {
|
||||
let weak = app.as_weak();
|
||||
app.on_cheats_select_rom_clicked(move |rom_dir| {
|
||||
@@ -38,7 +30,7 @@ pub fn cheats_window(app: &AppWindow) {
|
||||
handle.set_cheat_game_name(game_name.into());
|
||||
})
|
||||
.unwrap();
|
||||
let cheats: Cheats =
|
||||
let cheats: cheats::Cheat =
|
||||
serde_json::from_slice(include_bytes!("../../data/cheats.json")).unwrap();
|
||||
if let Some(cheat) = cheats.get(&game_crc) {
|
||||
let cheat = cheat.clone();
|
||||
@@ -77,16 +69,7 @@ pub fn cheats_window(app: &AppWindow) {
|
||||
options,
|
||||
));
|
||||
}
|
||||
#[allow(clippy::type_complexity)]
|
||||
let cheats_model: std::rc::Rc<
|
||||
slint::VecModel<(
|
||||
slint::SharedString,
|
||||
bool,
|
||||
slint::SharedString,
|
||||
slint::ModelRc<(bool, slint::SharedString)>,
|
||||
)>,
|
||||
> = std::rc::Rc::new(cheats_vec);
|
||||
handle.set_cheats(slint::ModelRc::from(cheats_model));
|
||||
handle.set_cheats(slint::ModelRc::from(std::rc::Rc::new(cheats_vec)));
|
||||
})
|
||||
.unwrap();
|
||||
} else {
|
||||
@@ -137,19 +120,11 @@ pub fn cheats_window(app: &AppWindow) {
|
||||
|
||||
fn clear_cheats(weak: &slint::Weak<AppWindow>, clear_name: bool) {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
let cheats_vec = slint::VecModel::default();
|
||||
#[allow(clippy::type_complexity)]
|
||||
let cheats_model: std::rc::Rc<
|
||||
slint::VecModel<(
|
||||
slint::SharedString,
|
||||
bool,
|
||||
slint::SharedString,
|
||||
slint::ModelRc<(bool, slint::SharedString)>,
|
||||
)>,
|
||||
> = std::rc::Rc::new(cheats_vec);
|
||||
handle.set_cheats(slint::ModelRc::from(cheats_model));
|
||||
handle.set_cheats(slint::ModelRc::from(std::rc::Rc::new(
|
||||
slint::VecModel::default(),
|
||||
)));
|
||||
if clear_name {
|
||||
handle.set_cheat_game_name("".into());
|
||||
handle.set_cheat_game_name(String::new().into());
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -29,6 +29,7 @@ pub struct InputProfile {
|
||||
pub joystick_hat: [InputJoystickHat; ui::input::PROFILE_SIZE],
|
||||
pub joystick_axis: [InputControllerAxis; ui::input::PROFILE_SIZE],
|
||||
pub dinput: bool,
|
||||
pub deadzone: i32,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
@@ -38,6 +39,8 @@ pub struct Input {
|
||||
pub controller_assignment: [Option<String>; 4],
|
||||
pub controller_enabled: [bool; 4],
|
||||
pub transfer_pak: [bool; 4],
|
||||
pub gb_rom_path: [String; 4],
|
||||
pub gb_ram_path: [String; 4],
|
||||
pub emulate_vru: bool,
|
||||
}
|
||||
|
||||
@@ -47,6 +50,7 @@ pub struct Video {
|
||||
pub integer_scaling: bool,
|
||||
pub fullscreen: bool,
|
||||
pub widescreen: bool,
|
||||
pub vsync: bool,
|
||||
pub crt: bool,
|
||||
}
|
||||
|
||||
@@ -54,6 +58,7 @@ pub struct Video {
|
||||
pub struct Emulation {
|
||||
pub overclock: bool,
|
||||
pub disable_expansion_pak: bool,
|
||||
pub usb: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
@@ -68,6 +73,7 @@ pub struct Config {
|
||||
pub video: Video,
|
||||
pub emulation: Emulation,
|
||||
pub rom_dir: std::path::PathBuf,
|
||||
pub recent_roms: Vec<String>,
|
||||
}
|
||||
|
||||
impl Drop for Cheats {
|
||||
@@ -138,6 +144,8 @@ impl Config {
|
||||
input_profiles,
|
||||
controller_enabled: [true, false, false, false],
|
||||
transfer_pak: [false, false, false, false],
|
||||
gb_rom_path: [String::new(), String::new(), String::new(), String::new()],
|
||||
gb_ram_path: [String::new(), String::new(), String::new(), String::new()],
|
||||
emulate_vru: false,
|
||||
},
|
||||
video: Video {
|
||||
@@ -145,13 +153,16 @@ impl Config {
|
||||
integer_scaling: false,
|
||||
fullscreen: false,
|
||||
widescreen: false,
|
||||
vsync: true,
|
||||
crt: false,
|
||||
},
|
||||
emulation: Emulation {
|
||||
overclock: false,
|
||||
disable_expansion_pak: false,
|
||||
usb: false,
|
||||
},
|
||||
rom_dir: std::path::PathBuf::new(),
|
||||
recent_roms: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+359
-237
@@ -1,11 +1,13 @@
|
||||
use crate::device;
|
||||
use crate::netplay;
|
||||
use crate::retroachievements;
|
||||
use crate::ui;
|
||||
use slint::Model;
|
||||
use slint::winit_030::WinitWindowAccessor;
|
||||
|
||||
slint::include_modules!();
|
||||
|
||||
pub const N64_EXTENSIONS: [&str; 5] = ["n64", "v64", "z64", "7z", "zip"];
|
||||
pub const N64_EXTENSIONS: [&str; 12] = [
|
||||
"n64", "v64", "z64", "7z", "zip", "bin", "N64", "V64", "Z64", "7Z", "ZIP", "BIN",
|
||||
];
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct GithubData {
|
||||
@@ -17,22 +19,12 @@ pub struct NetplayDevice {
|
||||
pub player_number: u8,
|
||||
}
|
||||
|
||||
pub struct GbPaths {
|
||||
pub rom: [Option<std::path::PathBuf>; 4],
|
||||
pub ram: [Option<std::path::PathBuf>; 4],
|
||||
}
|
||||
|
||||
pub struct VruChannel {
|
||||
pub vru_window_notifier: Option<tokio::sync::mpsc::Sender<Option<Vec<String>>>>,
|
||||
pub vru_word_receiver: Option<tokio::sync::mpsc::Receiver<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GameSettings {
|
||||
pub fullscreen: bool,
|
||||
pub overclock: bool,
|
||||
pub disable_expansion_pak: bool,
|
||||
pub cheats: std::collections::HashMap<String, Option<String>>,
|
||||
pub struct RASettings {
|
||||
pub enabled: bool,
|
||||
pub hardcore: bool,
|
||||
pub challenge: bool,
|
||||
pub leaderboard: bool,
|
||||
}
|
||||
|
||||
fn check_latest_version(weak: slint::Weak<AppWindow>) {
|
||||
@@ -62,12 +54,92 @@ fn check_latest_version(weak: slint::Weak<AppWindow>) {
|
||||
});
|
||||
}
|
||||
|
||||
fn local_game_window(app: &AppWindow, controller_paths: &[Option<String>]) {
|
||||
let dirs = ui::get_dirs();
|
||||
fn run_with_path(
|
||||
weak: slint::Weak<AppWindow>,
|
||||
path: std::path::PathBuf,
|
||||
controller_paths: &[Option<String>],
|
||||
) {
|
||||
let controller_paths = controller_paths.to_owned();
|
||||
let weak2 = weak.clone();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
if handle.get_game_running() {
|
||||
return;
|
||||
}
|
||||
save_settings(&handle, &controller_paths);
|
||||
|
||||
run_rom(
|
||||
path,
|
||||
ui::GameSettings {
|
||||
overclock: handle.get_overclock_n64_cpu(),
|
||||
disable_expansion_pak: handle.get_disable_expansion_pak(),
|
||||
cheats: std::collections::HashMap::new(), // will be filled in later
|
||||
load_savestate_slot: None,
|
||||
},
|
||||
None,
|
||||
RASettings {
|
||||
enabled: handle.get_ra_enabled(),
|
||||
hardcore: handle.get_ra_hardcore(),
|
||||
challenge: handle.get_ra_challenge(),
|
||||
leaderboard: handle.get_ra_leaderboard(),
|
||||
},
|
||||
weak2,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn file_dropped(app: &AppWindow, controller_paths: &[Option<String>]) {
|
||||
let weak = app.as_weak();
|
||||
let controller_paths = controller_paths.to_owned();
|
||||
app.window()
|
||||
.on_winit_window_event(move |_winit_window, event| {
|
||||
if let slint::winit_030::winit::event::WindowEvent::DroppedFile(path) = event {
|
||||
run_with_path(weak.clone(), path.to_path_buf(), &controller_paths);
|
||||
}
|
||||
slint::winit_030::EventResult::Propagate
|
||||
});
|
||||
}
|
||||
|
||||
fn local_game_window(
|
||||
app: &AppWindow,
|
||||
config: &ui::config::Config,
|
||||
controller_paths: &[Option<String>],
|
||||
) {
|
||||
let dirs = ui::get_dirs();
|
||||
|
||||
app.set_recent_roms(slint::ModelRc::from(std::rc::Rc::new(
|
||||
slint::VecModel::from(
|
||||
config
|
||||
.recent_roms
|
||||
.iter()
|
||||
.filter(|x| {
|
||||
if let Ok(exists) = std::fs::exists(x)
|
||||
&& exists
|
||||
{
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.map(|x| {
|
||||
(
|
||||
x.into(),
|
||||
std::path::Path::new(x)
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.into(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(slint::SharedString, slint::SharedString)>>(),
|
||||
),
|
||||
)));
|
||||
|
||||
let weak = app.as_weak();
|
||||
let owned_controller_paths = controller_paths.to_owned();
|
||||
app.on_open_rom_button_clicked(move || {
|
||||
let controller_paths = controller_paths.clone();
|
||||
let controller_paths = owned_controller_paths.clone();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
save_settings(&handle, &controller_paths);
|
||||
open_rom(&handle)
|
||||
@@ -75,10 +147,25 @@ fn local_game_window(app: &AppWindow, controller_paths: &[Option<String>]) {
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let weak = app.as_weak();
|
||||
let owned_controller_paths = controller_paths.to_owned();
|
||||
app.on_recent_rom_button_clicked(move |rom| {
|
||||
let controller_paths = owned_controller_paths.clone();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
run_with_path(
|
||||
handle.as_weak(),
|
||||
std::path::PathBuf::from(rom.to_string()),
|
||||
&controller_paths,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let saves_path = dirs.data_dir.join("saves");
|
||||
app.on_saves_folder_button_clicked(move || {
|
||||
open::that_detached(saves_path.clone()).unwrap();
|
||||
});
|
||||
file_dropped(app, controller_paths);
|
||||
}
|
||||
|
||||
fn input_profiles(config: &ui::config::Config) -> Vec<String> {
|
||||
@@ -93,13 +180,16 @@ fn settings_window(app: &AppWindow, config: &ui::config::Config) {
|
||||
app.set_integer_scaling(config.video.integer_scaling);
|
||||
app.set_fullscreen(config.video.fullscreen);
|
||||
app.set_widescreen(config.video.widescreen);
|
||||
app.set_vsync(config.video.vsync);
|
||||
app.set_apply_crt_shader(config.video.crt);
|
||||
app.set_overclock_n64_cpu(config.emulation.overclock);
|
||||
app.set_disable_expansion_pak(config.emulation.disable_expansion_pak);
|
||||
app.set_emulate_usb(config.emulation.usb);
|
||||
let combobox_value = match config.video.upscale {
|
||||
1 => 0,
|
||||
2 => 1,
|
||||
4 => 2,
|
||||
8 => 3,
|
||||
_ => 0,
|
||||
};
|
||||
app.set_resolution(combobox_value);
|
||||
@@ -127,17 +217,17 @@ fn update_input_profiles(weak: &slint::Weak<AppWindow>, config: &ui::config::Con
|
||||
profile_bindings.push(position.unwrap() as i32);
|
||||
}
|
||||
|
||||
let input_profiles = slint::VecModel::default();
|
||||
for profile in profiles {
|
||||
input_profiles.push(profile.into());
|
||||
}
|
||||
let input_profiles_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
|
||||
std::rc::Rc::new(input_profiles);
|
||||
handle.set_input_profiles(slint::ModelRc::from(input_profiles_model));
|
||||
handle.set_input_profiles(slint::ModelRc::from(std::rc::Rc::new(
|
||||
slint::VecModel::from(
|
||||
profiles
|
||||
.iter()
|
||||
.map(|x| x.into())
|
||||
.collect::<Vec<slint::SharedString>>(),
|
||||
),
|
||||
)));
|
||||
|
||||
let input_profile_binding_model: std::rc::Rc<slint::VecModel<i32>> =
|
||||
std::rc::Rc::new(profile_bindings);
|
||||
handle.set_selected_profile_binding(slint::ModelRc::from(input_profile_binding_model));
|
||||
handle
|
||||
.set_selected_profile_binding(slint::ModelRc::from(std::rc::Rc::new(profile_bindings)));
|
||||
|
||||
// this is a workaround to make the input profile combobox update
|
||||
handle.set_blank_profiles(true);
|
||||
@@ -155,51 +245,70 @@ fn update_input_profiles(weak: &slint::Weak<AppWindow>, config: &ui::config::Con
|
||||
fn controller_window(
|
||||
app: &AppWindow,
|
||||
config: &ui::config::Config,
|
||||
controller_names: &Vec<String>,
|
||||
controller_names: &[String],
|
||||
controller_paths: &[Option<String>],
|
||||
) {
|
||||
let controller_enabled_model: std::rc::Rc<slint::VecModel<bool>> = std::rc::Rc::new(
|
||||
slint::VecModel::from(config.input.controller_enabled.to_vec()),
|
||||
);
|
||||
app.set_emulate_vru(config.input.emulate_vru);
|
||||
|
||||
app.set_controller_enabled(slint::ModelRc::from(controller_enabled_model));
|
||||
app.set_controller_enabled(slint::ModelRc::from(std::rc::Rc::new(
|
||||
slint::VecModel::from(config.input.controller_enabled.to_vec()),
|
||||
)));
|
||||
|
||||
let transferpak_enabled_model: std::rc::Rc<slint::VecModel<bool>> =
|
||||
std::rc::Rc::new(slint::VecModel::from(config.input.transfer_pak.to_vec()));
|
||||
app.set_transferpak(slint::ModelRc::from(transferpak_enabled_model));
|
||||
app.set_transferpak(slint::ModelRc::from(std::rc::Rc::new(
|
||||
slint::VecModel::from(config.input.transfer_pak.to_vec()),
|
||||
)));
|
||||
|
||||
app.set_gb_rom_paths(slint::ModelRc::from(std::rc::Rc::new(
|
||||
slint::VecModel::from(
|
||||
config
|
||||
.input
|
||||
.gb_rom_path
|
||||
.iter()
|
||||
.map(|x| x.into())
|
||||
.collect::<Vec<slint::SharedString>>(),
|
||||
),
|
||||
)));
|
||||
|
||||
app.set_gb_ram_paths(slint::ModelRc::from(std::rc::Rc::new(
|
||||
slint::VecModel::from(
|
||||
config
|
||||
.input
|
||||
.gb_ram_path
|
||||
.iter()
|
||||
.map(|x| x.into())
|
||||
.collect::<Vec<slint::SharedString>>(),
|
||||
),
|
||||
)));
|
||||
|
||||
update_input_profiles(&app.as_weak(), config);
|
||||
|
||||
let controllers = slint::VecModel::default();
|
||||
for controller in controller_names {
|
||||
controllers.push(controller.into());
|
||||
}
|
||||
let controller_names_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
|
||||
std::rc::Rc::new(controllers);
|
||||
app.set_controller_names(slint::ModelRc::from(controller_names_model));
|
||||
app.set_controller_names(slint::ModelRc::from(std::rc::Rc::new(
|
||||
slint::VecModel::from(
|
||||
controller_names
|
||||
.iter()
|
||||
.map(|x| x.into())
|
||||
.collect::<Vec<slint::SharedString>>(),
|
||||
),
|
||||
)));
|
||||
|
||||
let controller_changed = slint::VecModel::default();
|
||||
let selected_controllers = slint::VecModel::default();
|
||||
for selected in config.input.controller_assignment.iter() {
|
||||
let mut found = false;
|
||||
for (i, path) in controller_paths.iter().enumerate() {
|
||||
if selected == path {
|
||||
selected_controllers.push(i as i32);
|
||||
found = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
selected_controllers.push(0);
|
||||
}
|
||||
let selected_index = controller_paths
|
||||
.iter()
|
||||
.position(|path| selected == path)
|
||||
.unwrap_or(0) as i32;
|
||||
selected_controllers.push(selected_index);
|
||||
controller_changed.push(false);
|
||||
}
|
||||
let selected_controllers_model: std::rc::Rc<slint::VecModel<i32>> =
|
||||
std::rc::Rc::new(selected_controllers);
|
||||
app.set_selected_controller(slint::ModelRc::from(selected_controllers_model));
|
||||
app.set_selected_controller(slint::ModelRc::from(std::rc::Rc::new(selected_controllers)));
|
||||
|
||||
app.set_controller_changed(slint::ModelRc::from(std::rc::Rc::new(controller_changed)));
|
||||
|
||||
let weak_app = app.as_weak();
|
||||
app.on_input_profile_button_clicked(move || {
|
||||
let dialog = InputProfileDialog::new().unwrap();
|
||||
dialog.set_deadzone(ui::input::DEADZONE_DEFAULT);
|
||||
let weak_dialog = dialog.as_weak();
|
||||
let weak_app = weak_app.clone();
|
||||
dialog.on_profile_creation_button_clicked(move || {
|
||||
@@ -207,19 +316,87 @@ fn controller_window(
|
||||
weak_dialog
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
handle.hide().unwrap();
|
||||
let profile_name = handle.get_profile_name().into();
|
||||
let profile_name = handle.get_profile_name();
|
||||
let dinput = handle.get_dinput();
|
||||
let deadzone = handle.get_deadzone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut game_ui = ui::Ui::new();
|
||||
ui::input::configure_input_profile(&mut game_ui, profile_name, dinput);
|
||||
update_input_profiles(&weak_app, &game_ui.config);
|
||||
let cli_path = std::env::current_exe()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(format!("{}-cli", env!("CARGO_PKG_NAME")));
|
||||
let cmd_path = if cfg!(target_os = "macos") && cli_path.exists() {
|
||||
cli_path
|
||||
} else {
|
||||
std::env::current_exe().unwrap()
|
||||
};
|
||||
let mut command = tokio::process::Command::new(cmd_path);
|
||||
command.args([
|
||||
"--configure-input-profile",
|
||||
&profile_name,
|
||||
"--deadzone",
|
||||
&deadzone.to_string(),
|
||||
]);
|
||||
if dinput {
|
||||
command.arg("--use-dinput");
|
||||
}
|
||||
if !command.status().await.unwrap().success() {
|
||||
eprintln!("Failed to configure input profile");
|
||||
}
|
||||
let config = ui::config::Config::new();
|
||||
update_input_profiles(&weak_app, &config);
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
dialog.show().unwrap();
|
||||
});
|
||||
let weak_app2 = app.as_weak();
|
||||
app.on_transferpak_toggled(move |player, enabled| {
|
||||
if enabled {
|
||||
let select_gb_rom = rfd::AsyncFileDialog::new()
|
||||
.set_title(format!("GB ROM P{}", player + 1))
|
||||
.add_filter("GB ROM files", &["gb", "gbc", "GB", "GBC"])
|
||||
.pick_file();
|
||||
let select_gb_ram = rfd::AsyncFileDialog::new()
|
||||
.set_title(format!("GB RAM P{}", player + 1))
|
||||
.add_filter("GB RAM files", &["sav", "ram", "srm", "SAV", "RAM", "SRM"])
|
||||
.pick_file();
|
||||
let weak_app3 = weak_app2.clone();
|
||||
tokio::spawn(async move {
|
||||
if let (Some(gb_rom), Some(gb_ram)) = (select_gb_rom.await, select_gb_ram.await) {
|
||||
weak_app3
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
let rom_paths = handle.get_gb_rom_paths();
|
||||
let ram_paths = handle.get_gb_ram_paths();
|
||||
rom_paths.set_row_data(
|
||||
player as usize,
|
||||
gb_rom.path().to_str().unwrap().into(),
|
||||
);
|
||||
ram_paths.set_row_data(
|
||||
player as usize,
|
||||
gb_ram.path().to_str().unwrap().into(),
|
||||
);
|
||||
handle.set_gb_rom_paths(rom_paths);
|
||||
handle.set_gb_ram_paths(ram_paths);
|
||||
})
|
||||
.unwrap();
|
||||
} else {
|
||||
weak_app3
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
let rom_paths = handle.get_gb_rom_paths();
|
||||
let ram_paths = handle.get_gb_ram_paths();
|
||||
rom_paths.set_row_data(player as usize, String::new().into());
|
||||
ram_paths.set_row_data(player as usize, String::new().into());
|
||||
handle.set_gb_rom_paths(rom_paths);
|
||||
handle.set_gb_ram_paths(ram_paths);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
|
||||
@@ -227,10 +404,12 @@ pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
|
||||
config.video.integer_scaling = app.get_integer_scaling();
|
||||
config.video.fullscreen = app.get_fullscreen();
|
||||
config.video.widescreen = app.get_widescreen();
|
||||
config.video.vsync = app.get_vsync();
|
||||
config.video.crt = app.get_apply_crt_shader();
|
||||
config.emulation.overclock = app.get_overclock_n64_cpu();
|
||||
config.emulation.disable_expansion_pak = app.get_disable_expansion_pak();
|
||||
let upscale_values = [1, 2, 4];
|
||||
config.emulation.usb = app.get_emulate_usb();
|
||||
let upscale_values = [1, 2, 4, 8];
|
||||
config.video.upscale = upscale_values[app.get_resolution() as usize];
|
||||
|
||||
config.input.emulate_vru = app.get_emulate_vru();
|
||||
@@ -239,6 +418,8 @@ pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
|
||||
}
|
||||
for (i, transferpak_enabled) in app.get_transferpak().iter().enumerate() {
|
||||
config.input.transfer_pak[i] = transferpak_enabled;
|
||||
config.input.gb_rom_path[i] = app.get_gb_rom_paths().row_data(i).unwrap().to_string();
|
||||
config.input.gb_ram_path[i] = app.get_gb_ram_paths().row_data(i).unwrap().to_string();
|
||||
}
|
||||
for (i, input_profile_binding) in app.get_selected_profile_binding().iter().enumerate() {
|
||||
config.input.input_profile_binding[i] = app
|
||||
@@ -249,8 +430,10 @@ pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
|
||||
}
|
||||
|
||||
for (i, selected_controller) in app.get_selected_controller().iter().enumerate() {
|
||||
config.input.controller_assignment[i] =
|
||||
controller_paths[selected_controller as usize].clone();
|
||||
if app.get_controller_changed().row_data(i).unwrap_or(false) {
|
||||
config.input.controller_assignment[i] =
|
||||
controller_paths[selected_controller as usize].clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,13 +456,16 @@ fn about_window(app: &AppWindow) {
|
||||
app.on_newversion_button_clicked(move || {
|
||||
open::that_detached("https://github.com/gopher64/gopher64/releases/latest").unwrap();
|
||||
});
|
||||
app.set_version(format!("Version: {}", env!("CARGO_PKG_VERSION")).into());
|
||||
check_latest_version(app.as_weak());
|
||||
app.set_version(format!("Version: {}", env!("GIT_DESCRIBE")).into());
|
||||
if std::env::var("FLATPAK_ID").is_err() {
|
||||
check_latest_version(app.as_weak());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn app_window() {
|
||||
let app = AppWindow::new().unwrap();
|
||||
about_window(&app);
|
||||
ui::retroachievements::ra_window(&app);
|
||||
let mut controller_paths;
|
||||
{
|
||||
let game_ui = ui::Ui::new();
|
||||
@@ -289,124 +475,113 @@ pub fn app_window() {
|
||||
controller_paths.insert(0, None);
|
||||
settings_window(&app, &game_ui.config);
|
||||
controller_window(&app, &game_ui.config, &controller_names, &controller_paths);
|
||||
local_game_window(&app, &game_ui.config, &controller_paths);
|
||||
}
|
||||
local_game_window(&app, &controller_paths);
|
||||
ui::netplay::netplay_window(&app, &controller_paths);
|
||||
ui::cheats::cheats_window(&app);
|
||||
app.run().unwrap();
|
||||
save_settings(&app, &controller_paths);
|
||||
}
|
||||
|
||||
fn setup_vru_word_watcher(
|
||||
weak_vru: slint::Weak<AppWindow>,
|
||||
vru_word_notifier: tokio::sync::mpsc::Sender<String>,
|
||||
mut vru_window_receiver: tokio::sync::mpsc::Receiver<Option<Vec<String>>>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let notifier = vru_word_notifier.clone();
|
||||
let notifier_closed = vru_word_notifier.clone();
|
||||
let result = vru_window_receiver.recv().await;
|
||||
if let Some(Some(words)) = result {
|
||||
weak_vru
|
||||
.upgrade_in_event_loop(move |_handle| {
|
||||
let vru_dialog = VruDialog::new().unwrap();
|
||||
let vru_dialog_weak = vru_dialog.as_weak();
|
||||
|
||||
vru_dialog.on_vru_button_clicked(move |chosen_word| {
|
||||
notifier.try_send(chosen_word.to_string()).unwrap();
|
||||
vru_dialog_weak.unwrap().window().hide().unwrap();
|
||||
});
|
||||
|
||||
vru_dialog.window().on_close_requested(move || {
|
||||
notifier_closed.try_send("".to_string()).unwrap();
|
||||
slint::CloseRequestResponse::HideWindow
|
||||
});
|
||||
|
||||
let words_vec = slint::VecModel::default();
|
||||
for word in words {
|
||||
words_vec.push(word.into());
|
||||
}
|
||||
let words_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
|
||||
std::rc::Rc::new(words_vec);
|
||||
vru_dialog.set_words(slint::ModelRc::from(words_model));
|
||||
|
||||
vru_dialog.show().unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn run_rom(
|
||||
gb_paths: GbPaths,
|
||||
file_path: std::path::PathBuf,
|
||||
mut game_settings: GameSettings,
|
||||
vru_channel: VruChannel,
|
||||
game_settings: ui::GameSettings,
|
||||
netplay: Option<NetplayDevice>,
|
||||
ra_settings: RASettings,
|
||||
weak: slint::Weak<AppWindow>,
|
||||
) {
|
||||
std::thread::Builder::new()
|
||||
.name("n64".to_string())
|
||||
.stack_size(env!("N64_STACK_SIZE").parse().unwrap())
|
||||
.spawn(move || {
|
||||
weak.upgrade_in_event_loop(move |handle| handle.set_game_running(true))
|
||||
.unwrap();
|
||||
|
||||
let mut device = device::Device::new();
|
||||
device.ui.config.rom_dir = file_path.parent().unwrap().to_path_buf();
|
||||
|
||||
for i in 0..4 {
|
||||
if gb_paths.rom[i].is_some() && gb_paths.ram[i].is_some() {
|
||||
device.transferpaks[i].cart.rom =
|
||||
std::fs::read(gb_paths.rom[i].as_ref().unwrap()).unwrap();
|
||||
|
||||
device.transferpaks[i].cart.ram =
|
||||
std::fs::read(gb_paths.ram[i].as_ref().unwrap()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
device.vru_window.window_notifier = vru_channel.vru_window_notifier;
|
||||
device.vru_window.word_receiver = vru_channel.vru_word_receiver;
|
||||
|
||||
if let Some(rom_contents) = device::get_rom_contents(file_path.as_path()) {
|
||||
if let Some(netplay_device) = netplay {
|
||||
device.netplay = Some(netplay::init(
|
||||
netplay_device.peer_addr,
|
||||
netplay_device.player_number,
|
||||
));
|
||||
} else {
|
||||
game_settings.cheats = {
|
||||
let game_crc = ui::storage::get_game_crc(&rom_contents);
|
||||
let cheats = ui::config::Cheats::new();
|
||||
cheats.cheats.get(&game_crc).cloned().unwrap_or_default()
|
||||
};
|
||||
}
|
||||
device::run_game(&mut device, rom_contents, game_settings);
|
||||
if device.netplay.is_some() {
|
||||
netplay::close(&mut device);
|
||||
}
|
||||
} else {
|
||||
println!("Could not read rom file");
|
||||
}
|
||||
|
||||
if let Some(vru_window_notifier) = device.vru_window.window_notifier {
|
||||
vru_window_notifier.try_send(None).unwrap();
|
||||
}
|
||||
|
||||
let rom_dir = device.ui.config.rom_dir.clone();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
if let Some(rom_dir_str) = rom_dir.to_str() {
|
||||
handle.set_rom_dir(rom_dir_str.into());
|
||||
}
|
||||
handle.set_game_running(false);
|
||||
})
|
||||
tokio::spawn(async move {
|
||||
weak.upgrade_in_event_loop(move |handle| handle.set_game_running(true))
|
||||
.unwrap();
|
||||
|
||||
let cli_path = std::env::current_exe()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(format!("{}-cli", env!("CARGO_PKG_NAME")));
|
||||
let cmd_path = if cfg!(target_os = "macos") && cli_path.exists() {
|
||||
cli_path
|
||||
} else {
|
||||
std::env::current_exe().unwrap()
|
||||
};
|
||||
let mut command = tokio::process::Command::new(cmd_path);
|
||||
command.args([
|
||||
"--overclock",
|
||||
&game_settings.overclock.to_string(),
|
||||
"--disable-expansion-pak",
|
||||
&game_settings.disable_expansion_pak.to_string(),
|
||||
]);
|
||||
let cheats_path = ui::get_dirs().cache_dir.join("cheats.json");
|
||||
if let Some(netplay_device) = netplay {
|
||||
let f = std::fs::File::create(cheats_path.to_str().unwrap()).unwrap();
|
||||
serde_json::to_writer_pretty(f, &game_settings.cheats).unwrap();
|
||||
|
||||
command.args([
|
||||
"--netplay-peer-addr",
|
||||
&netplay_device.peer_addr.to_string(),
|
||||
"--netplay-player-number",
|
||||
&netplay_device.player_number.to_string(),
|
||||
"--cheats",
|
||||
cheats_path.to_str().unwrap(),
|
||||
]);
|
||||
}
|
||||
if ra_settings.enabled {
|
||||
command.args([
|
||||
"--ra-username",
|
||||
&retroachievements::get_username().unwrap_or("unknown".into()),
|
||||
"--ra-token",
|
||||
&retroachievements::get_token().unwrap_or("unknown".into()),
|
||||
]);
|
||||
if ra_settings.hardcore {
|
||||
command.args(["--ra-hardcore"]);
|
||||
}
|
||||
if ra_settings.challenge {
|
||||
command.args(["--ra-challenge"]);
|
||||
}
|
||||
if ra_settings.leaderboard {
|
||||
command.args(["--ra-leaderboard"]);
|
||||
}
|
||||
}
|
||||
|
||||
let success = command
|
||||
.arg(file_path.to_str().unwrap())
|
||||
.status()
|
||||
.await
|
||||
.unwrap()
|
||||
.success();
|
||||
|
||||
if !success {
|
||||
eprintln!("Failed to run game");
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_file(cheats_path.to_str().unwrap());
|
||||
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
if let Some(rom_dir) = file_path.parent().unwrap().to_str() {
|
||||
handle.set_rom_dir(rom_dir.into());
|
||||
}
|
||||
if success {
|
||||
let recent_roms = slint::VecModel::default();
|
||||
recent_roms.push((
|
||||
file_path.to_str().unwrap().into(),
|
||||
file_path.file_name().unwrap().to_str().unwrap().into(),
|
||||
));
|
||||
|
||||
for rom in handle.get_recent_roms().iter() {
|
||||
if rom.0 != file_path.to_str().unwrap()
|
||||
&& recent_roms.row_count() < 5
|
||||
&& let Ok(exists) = std::fs::exists(&rom.0)
|
||||
&& exists
|
||||
{
|
||||
recent_roms.push(rom);
|
||||
}
|
||||
}
|
||||
handle.set_recent_roms(slint::ModelRc::from(std::rc::Rc::new(recent_roms)));
|
||||
}
|
||||
handle.set_game_running(false);
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
fn open_rom(app: &AppWindow) {
|
||||
@@ -422,85 +597,32 @@ fn open_rom(app: &AppWindow) {
|
||||
.set_title("Select ROM")
|
||||
.add_filter("ROM files", &N64_EXTENSIONS)
|
||||
.pick_file();
|
||||
let mut select_gb_rom = [None, None, None, None];
|
||||
let mut select_gb_ram = [None, None, None, None];
|
||||
|
||||
for (i, transfer_pak_enabled) in app.get_transferpak().iter().enumerate() {
|
||||
if transfer_pak_enabled {
|
||||
select_gb_rom[i] = Some(
|
||||
rfd::AsyncFileDialog::new()
|
||||
.set_title(format!("GB ROM P{}", i + 1))
|
||||
.add_filter("GB ROM files", &["gb", "gbc"])
|
||||
.pick_file(),
|
||||
);
|
||||
select_gb_ram[i] = Some(
|
||||
rfd::AsyncFileDialog::new()
|
||||
.set_title(format!("GB RAM P{}", i + 1))
|
||||
.add_filter("GB RAM files", &["sav", "ram"])
|
||||
.pick_file(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
let (vru_window_notifier, vru_window_receiver): (
|
||||
tokio::sync::mpsc::Sender<Option<Vec<String>>>,
|
||||
tokio::sync::mpsc::Receiver<Option<Vec<String>>>,
|
||||
) = tokio::sync::mpsc::channel(5);
|
||||
|
||||
let (vru_word_notifier, vru_word_receiver): (
|
||||
tokio::sync::mpsc::Sender<String>,
|
||||
tokio::sync::mpsc::Receiver<String>,
|
||||
) = tokio::sync::mpsc::channel(5);
|
||||
|
||||
let fullscreen = app.get_fullscreen();
|
||||
let overclock = app.get_overclock_n64_cpu();
|
||||
let emulate_vru = app.get_emulate_vru();
|
||||
let disable_expansion_pak = app.get_disable_expansion_pak();
|
||||
let ra_enabled = app.get_ra_enabled();
|
||||
let ra_hardcore = app.get_ra_hardcore();
|
||||
let ra_challenge = app.get_ra_challenge();
|
||||
let ra_leaderboard = app.get_ra_leaderboard();
|
||||
|
||||
if emulate_vru {
|
||||
setup_vru_word_watcher(app.as_weak(), vru_word_notifier, vru_window_receiver);
|
||||
}
|
||||
let weak = app.as_weak();
|
||||
tokio::spawn(async move {
|
||||
if let Some(file) = select_rom.await {
|
||||
let mut gb_rom_path = [None, None, None, None];
|
||||
let mut gb_ram_path = [None, None, None, None];
|
||||
|
||||
for i in 0..4 {
|
||||
if let (Some(gb_rom), Some(gb_ram)) =
|
||||
(select_gb_rom[i].as_mut(), select_gb_ram[i].as_mut())
|
||||
&& let (Some(gb_rom), Some(gb_ram)) = (gb_rom.await, gb_ram.await)
|
||||
{
|
||||
gb_rom_path[i] = Some(gb_rom.path().to_path_buf());
|
||||
gb_ram_path[i] = Some(gb_ram.path().to_path_buf());
|
||||
}
|
||||
}
|
||||
|
||||
run_rom(
|
||||
GbPaths {
|
||||
rom: gb_rom_path,
|
||||
ram: gb_ram_path,
|
||||
},
|
||||
file.path().to_path_buf(),
|
||||
GameSettings {
|
||||
fullscreen,
|
||||
ui::GameSettings {
|
||||
overclock,
|
||||
disable_expansion_pak,
|
||||
cheats: std::collections::HashMap::new(), // will be filled in later
|
||||
},
|
||||
if emulate_vru {
|
||||
VruChannel {
|
||||
vru_window_notifier: Some(vru_window_notifier),
|
||||
vru_word_receiver: Some(vru_word_receiver),
|
||||
}
|
||||
} else {
|
||||
VruChannel {
|
||||
vru_window_notifier: None,
|
||||
vru_word_receiver: None,
|
||||
}
|
||||
load_savestate_slot: None,
|
||||
},
|
||||
None,
|
||||
RASettings {
|
||||
enabled: ra_enabled,
|
||||
hardcore: ra_hardcore,
|
||||
challenge: ra_challenge,
|
||||
leaderboard: ra_leaderboard,
|
||||
},
|
||||
weak,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { StandardButton } from "std-widgets.slint";
|
||||
import { LocalGame, ControllerConfig, Netplay, Cheats, Settings, About } from "pages.slint";
|
||||
import { LocalGame, ControllerConfig, Netplay, Cheats, RetroAchievements, Settings, About } from "pages.slint";
|
||||
import { SideBar } from "side_bar.slint";
|
||||
import { SettingsData } from "settings_page.slint";
|
||||
import { RAData } from "retroachievements_page.slint";
|
||||
import { AboutData } from "about_page.slint";
|
||||
import { ControllerData } from "controller_page.slint";
|
||||
import { State } from "localgame_page.slint";
|
||||
@@ -21,12 +22,14 @@ export component AppWindow inherits Window {
|
||||
callback wiki_button_clicked;
|
||||
callback discord_button_clicked;
|
||||
callback newversion_button_clicked;
|
||||
callback recent_rom_button_clicked(string);
|
||||
callback patreon_button_clicked;
|
||||
callback github_sponsors_button_clicked;
|
||||
callback source_code_button_clicked;
|
||||
callback open_rom_button_clicked;
|
||||
callback saves_folder_button_clicked;
|
||||
callback input_profile_button_clicked;
|
||||
callback transferpak_toggled(int,bool);
|
||||
callback create_session_button_clicked;
|
||||
callback join_session_button_clicked;
|
||||
callback netplay_discord_button_clicked;
|
||||
@@ -34,24 +37,44 @@ export component AppWindow inherits Window {
|
||||
callback cheats_select_rom_clicked(string);
|
||||
callback cheats_clear_clicked;
|
||||
callback cheat_toggled(string, string, string, bool);
|
||||
callback ra_button_clicked(string);
|
||||
callback ra_games_clicked;
|
||||
callback ra_show_profile_clicked;
|
||||
callback ra_toggled(bool,bool,bool,bool);
|
||||
in property version <=> AboutData.version;
|
||||
in property has_update <=> AboutData.has_update;
|
||||
in-out property ra_username <=> RAData.ra_username;
|
||||
in-out property ra_password <=> RAData.ra_password;
|
||||
in-out property ra_logging_in <=> RAData.ra_logging_in;
|
||||
in-out property ra_show_profile <=> RAData.ra_show_profile;
|
||||
in-out property ra_current_user_message <=> RAData.current_user_message;
|
||||
in-out property ra_enabled <=> RAData.enabled;
|
||||
in-out property ra_hardcore <=> RAData.hardcore;
|
||||
in-out property ra_challenge <=> RAData.challenge;
|
||||
in-out property ra_leaderboard <=> RAData.leaderboard;
|
||||
in-out property ra_softcore_only <=> RAData.ra_softcore_only;
|
||||
in-out property integer_scaling <=> SettingsData.integer_scaling;
|
||||
in-out property fullscreen <=> SettingsData.fullscreen;
|
||||
in-out property widescreen <=> SettingsData.widescreen;
|
||||
in-out property vsync <=> SettingsData.vsync;
|
||||
in-out property apply_crt_shader <=> SettingsData.apply_crt_shader;
|
||||
in-out property overclock_n64_cpu <=> SettingsData.overclock_n64_cpu;
|
||||
in-out property disable_expansion_pak <=> SettingsData.disable_expansion_pak;
|
||||
in-out property resolution <=> SettingsData.resolution;
|
||||
in-out property emulate_usb <=> SettingsData.emulate_usb;
|
||||
in-out property rom_dir <=> SettingsData.rom_dir;
|
||||
in-out property emulate_vru <=> ControllerData.emulate_vru;
|
||||
in-out property controller_enabled <=> ControllerData.controller_enabled;
|
||||
in-out property controller_changed <=> ControllerData.controller_changed;
|
||||
in-out property transferpak <=> ControllerData.transferpak;
|
||||
in-out property gb_rom_paths <=> ControllerData.gb_rom_paths;
|
||||
in-out property gb_ram_paths <=> ControllerData.gb_ram_paths;
|
||||
in-out property selected_profile_binding <=> ControllerData.selected_profile_binding;
|
||||
in-out property input_profiles <=> ControllerData.input_profiles;
|
||||
in-out property controller_names <=> ControllerData.controller_names;
|
||||
in-out property selected_controller <=> ControllerData.selected_controller;
|
||||
in-out property game_running <=> State.game_running;
|
||||
in-out property recent_roms <=> State.recent_roms;
|
||||
in-out property blank_profiles <=> ControllerData.blank_profiles;
|
||||
in-out property cheat_game_name <=> CheatsData.cheat_game_name;
|
||||
in-out property cheat_game_crc <=> CheatsData.cheat_game_crc;
|
||||
@@ -69,6 +92,7 @@ export component AppWindow inherits Window {
|
||||
@tr("Menu" => "Controller Config"),
|
||||
@tr("Menu" => "Netplay"),
|
||||
@tr("Menu" => "Cheats"),
|
||||
@tr("Menu" => "RetroAchievements"),
|
||||
@tr("Menu" => "Settings"),
|
||||
@tr("Menu" => "About")
|
||||
];
|
||||
@@ -84,11 +108,17 @@ export component AppWindow inherits Window {
|
||||
newversion_button_clicked => {
|
||||
newversion_button_clicked();
|
||||
}
|
||||
recent_rom_button_clicked(rom) => {
|
||||
recent_rom_button_clicked(rom);
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 1): ControllerConfig {
|
||||
input_profile_button_clicked => {
|
||||
input_profile_button_clicked();
|
||||
}
|
||||
transferpak_toggled(player,enabled) => {
|
||||
transferpak_toggled(player,enabled);
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 2): Netplay {
|
||||
create_session_button_clicked => {
|
||||
@@ -115,8 +145,22 @@ export component AppWindow inherits Window {
|
||||
cheat_toggled(game_crc,cheat_name,option,enabled);
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 4): Settings { }
|
||||
if(side-bar.current-item == 5): About {
|
||||
if(side-bar.current-item == 4): RetroAchievements {
|
||||
ra_button_clicked(password) => {
|
||||
ra_button_clicked(password);
|
||||
}
|
||||
ra_toggled(enabled,hardcore,challenge,leaderboard) => {
|
||||
ra_toggled(enabled,hardcore,challenge,leaderboard);
|
||||
}
|
||||
ra_games_clicked => {
|
||||
ra_games_clicked();
|
||||
}
|
||||
ra_show_profile_clicked => {
|
||||
ra_show_profile_clicked();
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 5): Settings { }
|
||||
if(side-bar.current-item == 6): About {
|
||||
wiki_button_clicked => {
|
||||
wiki_button_clicked();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { ListView, HorizontalBox, VerticalBox, Button, GroupBox, CheckBox } from "std-widgets.slint";
|
||||
import { ListView, HorizontalBox, VerticalBox, Button, GroupBox, Switch, CheckBox } from "std-widgets.slint";
|
||||
import { Page } from "page.slint";
|
||||
import { SettingsData } from "settings_page.slint";
|
||||
|
||||
@@ -62,15 +62,14 @@ export component Cheats inherits Page {
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
if data.options.length == 0: CheckBox {
|
||||
text: @tr("Enabled");
|
||||
if data.options.length == 0: Switch {
|
||||
checked: data.enabled;
|
||||
toggled => {
|
||||
data.enabled = self.checked;
|
||||
cheat_toggled(CheatsData.cheat_game_crc,data.cheat_name,"",self.checked);
|
||||
}
|
||||
}
|
||||
for item[index] in data.options: CheckBox {
|
||||
for item in data.options: CheckBox {
|
||||
text: item.option;
|
||||
enabled: !data.enabled || (data.enabled && item.enabled);
|
||||
checked: item.enabled;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { CheckBox, ComboBox, Button, HorizontalBox, VerticalBox, GroupBox } from "std-widgets.slint";
|
||||
import { Switch, ComboBox, Button, HorizontalBox, VerticalBox, GroupBox } from "std-widgets.slint";
|
||||
import { Page } from "page.slint";
|
||||
import { State } from "localgame_page.slint";
|
||||
|
||||
@@ -9,15 +9,19 @@ export global ControllerData {
|
||||
in-out property <bool> emulate_vru;
|
||||
in-out property <[bool]> controller_enabled;
|
||||
in-out property <[bool]> transferpak;
|
||||
in-out property <[string]> gb_rom_paths;
|
||||
in-out property <[string]> gb_ram_paths;
|
||||
in-out property <[int]> selected_profile_binding: [-1, -1, -1, -1];
|
||||
in-out property <[string]> input_profiles;
|
||||
in-out property <[string]> controller_names;
|
||||
in-out property <[int]> selected_controller;
|
||||
in-out property <[bool]> controller_changed;
|
||||
in-out property <bool> blank_profiles;
|
||||
}
|
||||
|
||||
export component ControllerConfig inherits Page {
|
||||
callback input_profile_button_clicked;
|
||||
callback transferpak_toggled(int,bool);
|
||||
title: @tr("Controller Configuration");
|
||||
|
||||
VerticalBox {
|
||||
@@ -51,7 +55,7 @@ export component ControllerConfig inherits Page {
|
||||
title: @tr("Enabled");
|
||||
VerticalLayout {
|
||||
alignment: space-between;
|
||||
for player in [0, 1, 2, 3]: CheckBox {
|
||||
for player in [0, 1, 2, 3]: Switch {
|
||||
checked: ControllerData.controller_enabled[player];
|
||||
toggled => {
|
||||
ControllerData.controller_enabled[player] = self.checked;
|
||||
@@ -64,7 +68,7 @@ export component ControllerConfig inherits Page {
|
||||
title: @tr("Emulate VRU");
|
||||
VerticalLayout {
|
||||
alignment: space-between;
|
||||
for valid in [false, false, false, true]: CheckBox {
|
||||
for valid in [false, false, false, true]: Switch {
|
||||
enabled: valid;
|
||||
checked: valid && ControllerData.emulate_vru;
|
||||
toggled => {
|
||||
@@ -78,10 +82,11 @@ export component ControllerConfig inherits Page {
|
||||
title: @tr("Transfer Pak");
|
||||
VerticalLayout {
|
||||
alignment: space-between;
|
||||
for player in [0, 1, 2, 3]: CheckBox {
|
||||
for player in [0, 1, 2, 3]: Switch {
|
||||
checked: ControllerData.transferpak[player];
|
||||
toggled => {
|
||||
ControllerData.transferpak[player] = self.checked;
|
||||
transferpak_toggled(player,self.checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,10 +94,12 @@ export component ControllerConfig inherits Page {
|
||||
|
||||
if !ControllerData.blank_profiles: GroupBox {
|
||||
title: @tr("Profile");
|
||||
horizontal-stretch: 0.25;
|
||||
VerticalLayout {
|
||||
alignment: space-between;
|
||||
for player in [0, 1, 2, 3]: ComboBox {
|
||||
model: ControllerData.input_profiles;
|
||||
min-width: self.preferred-width > 150px ? self.preferred-width : 150px;
|
||||
current-index: ControllerData.selected_profile_binding[player];
|
||||
selected => {
|
||||
ControllerData.selected_profile_binding[player] = self.current-index;
|
||||
@@ -103,14 +110,16 @@ export component ControllerConfig inherits Page {
|
||||
|
||||
GroupBox {
|
||||
title: @tr("Controller");
|
||||
horizontal-stretch: 0.75;
|
||||
VerticalLayout {
|
||||
alignment: space-between;
|
||||
for player in [0, 1, 2, 3]: ComboBox {
|
||||
model: ControllerData.controller_names;
|
||||
width: self.preferred-width;
|
||||
min-width: self.preferred-width > 300px ? self.preferred-width : 300px;
|
||||
current-index: ControllerData.selected_controller[player];
|
||||
selected => {
|
||||
ControllerData.selected_controller[player] = self.current-index;
|
||||
ControllerData.controller_changed[player] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { StandardButton, HorizontalBox, VerticalBox, Button, LineEdit, CheckBox } from "std-widgets.slint";
|
||||
import { StandardButton, HorizontalBox, VerticalBox, Button, LineEdit, Switch, Slider } from "std-widgets.slint";
|
||||
|
||||
export global InputProfileData {
|
||||
in-out property <bool> dinput;
|
||||
in-out property <string> profile_name;
|
||||
in-out property <int> deadzone;
|
||||
}
|
||||
|
||||
export component InputProfileDialog inherits Window {
|
||||
@@ -10,6 +11,7 @@ export component InputProfileDialog inherits Window {
|
||||
callback profile_creation_button_clicked;
|
||||
in-out property dinput <=> InputProfileData.dinput;
|
||||
in-out property profile_name <=> InputProfileData.profile_name;
|
||||
in-out property deadzone <=> InputProfileData.deadzone;
|
||||
VerticalBox {
|
||||
HorizontalBox {
|
||||
LineEdit {
|
||||
@@ -23,7 +25,29 @@ export component InputProfileDialog inherits Window {
|
||||
|
||||
HorizontalBox {
|
||||
alignment: center;
|
||||
CheckBox {
|
||||
Text {
|
||||
text: @tr("Deadzone Percentage");
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
Slider {
|
||||
min-width: 160px;
|
||||
maximum: 99;
|
||||
value: InputProfileData.deadzone;
|
||||
changed(value) => {
|
||||
InputProfileData.deadzone = value;
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: InputProfileData.deadzone + "%";
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalBox {
|
||||
alignment: center;
|
||||
Switch {
|
||||
text: @tr("Use DirectInput");
|
||||
checked: InputProfileData.dinput;
|
||||
toggled => {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Button, HorizontalBox, VerticalBox } from "std-widgets.slint";
|
||||
import { Button, HorizontalBox, VerticalBox, GroupBox } from "std-widgets.slint";
|
||||
import { Page } from "page.slint";
|
||||
import { AboutData, NewVersionButton } from "about_page.slint";
|
||||
|
||||
export global State {
|
||||
in-out property <bool> game_running;
|
||||
in-out property <[{a:string, b:string}]> recent_roms;
|
||||
}
|
||||
|
||||
export component LocalGame inherits Page {
|
||||
callback open_rom_button_clicked;
|
||||
callback saves_folder_button_clicked;
|
||||
callback newversion_button_clicked;
|
||||
callback recent_rom_button_clicked(string);
|
||||
title: @tr("Local Game");
|
||||
|
||||
VerticalBox {
|
||||
@@ -44,5 +46,22 @@ export component LocalGame inherits Page {
|
||||
}
|
||||
}
|
||||
Rectangle { }
|
||||
|
||||
if State.recent_roms.length>0:HorizontalBox {
|
||||
alignment: center;
|
||||
GroupBox {
|
||||
title: @tr("Recent ROMs");
|
||||
VerticalLayout {
|
||||
alignment: space-between;
|
||||
for rom in State.recent_roms: Button {
|
||||
text: rom.b;
|
||||
enabled: !State.game_running;
|
||||
clicked => {
|
||||
recent_rom_button_clicked(rom.a);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export component NetplayCreate inherits Window {
|
||||
}
|
||||
|
||||
LineEdit {
|
||||
placeholder_text: @tr("Enter session name");
|
||||
placeholder-text: @tr("Enter session name");
|
||||
edited(text) => {
|
||||
NetplayData.session_name = text;
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export component NetplayCreate inherits Window {
|
||||
}
|
||||
|
||||
LineEdit {
|
||||
placeholder_text: @tr("Enter session password");
|
||||
placeholder-text: @tr("Enter session password");
|
||||
edited(text) => {
|
||||
NetplayData.password = text;
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export component NetplayCreate inherits Window {
|
||||
}
|
||||
|
||||
LineEdit {
|
||||
placeholder_text: @tr("Enter player name");
|
||||
placeholder-text: @tr("Enter player name");
|
||||
edited(text) => {
|
||||
NetplayData.player_name = text;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export component NetplayJoin inherits Window {
|
||||
in-out property sessions <=> NetplayData.sessions;
|
||||
in-out property current_session <=> NetplayData.current_session;
|
||||
in-out property room_urls <=> NetplayData.room_urls;
|
||||
in-out property room_ports <=> NetplayData.room_ports;
|
||||
in-out property game_name <=> NetplayData.game_name;
|
||||
in-out property game_hash <=> NetplayData.game_hash;
|
||||
in-out property game_cheats <=> NetplayData.game_cheats;
|
||||
@@ -19,18 +20,18 @@ export component NetplayJoin inherits Window {
|
||||
in-out property rom_dir <=> NetplayData.rom_dir;
|
||||
callback refresh_session();
|
||||
callback select_rom(string);
|
||||
callback join_session(string, string, string, string, string);
|
||||
callback join_session(string, string, string, string, int);
|
||||
preferred-width: 700px;
|
||||
|
||||
VerticalBox {
|
||||
HorizontalBox {
|
||||
VerticalBox {
|
||||
Text {
|
||||
text: @tr("Player Name:");
|
||||
text: @tr("Player Name (required):");
|
||||
}
|
||||
|
||||
LineEdit {
|
||||
placeholder_text: @tr("Enter player name");
|
||||
placeholder-text: @tr("Enter player name");
|
||||
edited(text) => {
|
||||
NetplayData.player_name = text;
|
||||
}
|
||||
@@ -93,6 +94,7 @@ export component NetplayJoin inherits Window {
|
||||
NetplayData.current_session = -1;
|
||||
NetplayData.sessions = [];
|
||||
NetplayData.room_urls = [];
|
||||
NetplayData.room_ports = [];
|
||||
refresh_session();
|
||||
}
|
||||
}
|
||||
@@ -129,7 +131,7 @@ export component NetplayJoin inherits Window {
|
||||
NetplayData.game_hash,
|
||||
NetplayData.password,
|
||||
NetplayData.room_urls[NetplayData.current_session],
|
||||
NetplayData.sessions[NetplayData.current_session][1].text);
|
||||
NetplayData.room_ports[NetplayData.current_session]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export global NetplayData {
|
||||
in-out property <string> session_name;
|
||||
in-out property <[[StandardListViewItem]]> sessions;
|
||||
in-out property <[string]> room_urls;
|
||||
in-out property <[int]> room_ports;
|
||||
in-out property <int> current_session: -1;
|
||||
in-out property <string> rom_dir;
|
||||
in-out property <string> custom_server_url;
|
||||
|
||||
@@ -73,7 +73,7 @@ export component NetplayWait inherits Window {
|
||||
|
||||
HorizontalBox {
|
||||
chat_message := LineEdit {
|
||||
placeholder_text: @tr("Enter chat message");
|
||||
placeholder-text: @tr("Enter chat message");
|
||||
key-released(event) => {
|
||||
if (event.text == Key.Return && chat_message.text != "") {
|
||||
send_chat_message(chat_message.text);
|
||||
|
||||
@@ -2,5 +2,6 @@ export { LocalGame } from "localgame_page.slint";
|
||||
export { ControllerConfig } from "controller_page.slint";
|
||||
export { Netplay } from "netplay_page.slint";
|
||||
export { Cheats } from "cheat_page.slint";
|
||||
export { RetroAchievements } from "retroachievements_page.slint";
|
||||
export { Settings } from "settings_page.slint";
|
||||
export { About } from "about_page.slint";
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { LineEdit, Switch, Button, HorizontalBox, VerticalBox } from "std-widgets.slint";
|
||||
import { Page } from "page.slint";
|
||||
|
||||
export global RAData {
|
||||
in-out property <bool> enabled;
|
||||
in-out property <bool> hardcore;
|
||||
in-out property <bool> challenge;
|
||||
in-out property <bool> leaderboard;
|
||||
in-out property <bool> ra_logging_in;
|
||||
in-out property <bool> ra_show_profile;
|
||||
in-out property <bool> ra_softcore_only;
|
||||
in-out property <string> ra_username;
|
||||
in-out property <string> ra_password;
|
||||
in-out property <string> current_user_message;
|
||||
}
|
||||
|
||||
export component RetroAchievements inherits Page {
|
||||
callback ra_button_clicked(string);
|
||||
callback ra_toggled(bool,bool,bool,bool);
|
||||
callback ra_games_clicked;
|
||||
callback ra_show_profile_clicked;
|
||||
title: @tr("RetroAchievements");
|
||||
|
||||
VerticalBox {
|
||||
alignment: start;
|
||||
HorizontalBox {
|
||||
alignment: start;
|
||||
Switch {
|
||||
text: @tr("Enabled");
|
||||
checked: RAData.enabled;
|
||||
toggled => {
|
||||
RAData.enabled = self.checked;
|
||||
if (!RAData.enabled) {
|
||||
RAData.ra_username = "";
|
||||
username_widget.text = "";
|
||||
password_widget.text = "";
|
||||
RAData.current_user_message = "Not currently logged in";
|
||||
RAData.ra_show_profile = false;
|
||||
}
|
||||
ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge,RAData.leaderboard);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !RAData.ra_softcore_only:HorizontalBox {
|
||||
alignment: start;
|
||||
Switch {
|
||||
text: @tr("Hardcore mode");
|
||||
checked: RAData.hardcore;
|
||||
enabled: RAData.enabled;
|
||||
toggled => {
|
||||
RAData.hardcore = self.checked;
|
||||
ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge,RAData.leaderboard);
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalBox {
|
||||
alignment: start;
|
||||
Switch {
|
||||
text: @tr("Challenge Indicators");
|
||||
checked: RAData.challenge;
|
||||
enabled: RAData.enabled;
|
||||
toggled => {
|
||||
RAData.challenge = self.checked;
|
||||
ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge,RAData.leaderboard);
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalBox {
|
||||
alignment: start;
|
||||
Switch {
|
||||
text: @tr("Leaderboard Trackers");
|
||||
checked: RAData.leaderboard;
|
||||
enabled: RAData.enabled;
|
||||
toggled => {
|
||||
RAData.leaderboard = self.checked;
|
||||
ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge,RAData.leaderboard);
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalBox {
|
||||
alignment: center;
|
||||
Text {
|
||||
text: RAData.current_user_message;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
if RAData.ra_show_profile:Button {
|
||||
enabled: RAData.enabled;
|
||||
text: @tr("View Profile");
|
||||
clicked => {
|
||||
ra_show_profile_clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalBox {
|
||||
alignment: center;
|
||||
username_widget := LineEdit {
|
||||
enabled: RAData.enabled;
|
||||
placeholder-text: @tr("Username");
|
||||
text: RAData.ra_username;
|
||||
edited(text) => {
|
||||
RAData.ra_username = text;
|
||||
}
|
||||
|
||||
}
|
||||
password_widget := LineEdit {
|
||||
enabled: RAData.enabled;
|
||||
input-type: password;
|
||||
placeholder-text: @tr("Password");
|
||||
edited(text) => {
|
||||
RAData.ra_password = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalBox {
|
||||
alignment: center;
|
||||
Button {
|
||||
text: @tr("Login");
|
||||
enabled: RAData.enabled && !RAData.ra_logging_in && RAData.ra_username != "" && RAData.ra_password != "";
|
||||
clicked => {
|
||||
RAData.ra_logging_in = true;
|
||||
ra_button_clicked(RAData.ra_password);
|
||||
RAData.ra_password = "";
|
||||
password_widget.text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
height: 20px;
|
||||
}
|
||||
HorizontalBox {
|
||||
alignment: center;
|
||||
Button {
|
||||
text: @tr("View Supported Games");
|
||||
clicked => {
|
||||
ra_games_clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { ComboBox, HorizontalBox, VerticalBox, CheckBox } from "std-widgets.slint";
|
||||
import { ComboBox, HorizontalBox, VerticalBox, Switch } from "std-widgets.slint";
|
||||
import { Page } from "page.slint";
|
||||
|
||||
export global SettingsData {
|
||||
in-out property <bool> integer_scaling;
|
||||
in-out property <bool> fullscreen;
|
||||
in-out property <bool> widescreen;
|
||||
in-out property <bool> vsync;
|
||||
in-out property <bool> apply_crt_shader;
|
||||
in-out property <bool> overclock_n64_cpu;
|
||||
in-out property <bool> disable_expansion_pak;
|
||||
in-out property <int> resolution;
|
||||
in-out property <bool> emulate_usb;
|
||||
in-out property <string> rom_dir;
|
||||
}
|
||||
|
||||
@@ -28,7 +30,7 @@ export component Settings inherits Page {
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
model: ["1x", "2x", "4x"];
|
||||
model: ["1x", "2x", "4x", "8x"];
|
||||
current-index: SettingsData.resolution;
|
||||
selected => {
|
||||
SettingsData.resolution = self.current-index;
|
||||
@@ -38,7 +40,7 @@ export component Settings inherits Page {
|
||||
|
||||
HorizontalBox {
|
||||
alignment: start;
|
||||
CheckBox {
|
||||
Switch {
|
||||
text: @tr("Integer Scaling");
|
||||
checked: SettingsData.integer_scaling;
|
||||
toggled => {
|
||||
@@ -49,8 +51,8 @@ export component Settings inherits Page {
|
||||
|
||||
HorizontalBox {
|
||||
alignment: start;
|
||||
CheckBox {
|
||||
text: @tr("Fullscreen (Esc closes game)");
|
||||
Switch {
|
||||
text: @tr("Fullscreen");
|
||||
checked: SettingsData.fullscreen;
|
||||
toggled => {
|
||||
SettingsData.fullscreen = self.checked;
|
||||
@@ -60,8 +62,8 @@ export component Settings inherits Page {
|
||||
|
||||
HorizontalBox {
|
||||
alignment: start;
|
||||
CheckBox {
|
||||
text: @tr("Widescreen (strech)");
|
||||
Switch {
|
||||
text: @tr("Widescreen (stretch)");
|
||||
checked: SettingsData.widescreen;
|
||||
toggled => {
|
||||
SettingsData.widescreen = self.checked;
|
||||
@@ -71,7 +73,18 @@ export component Settings inherits Page {
|
||||
|
||||
HorizontalBox {
|
||||
alignment: start;
|
||||
CheckBox {
|
||||
Switch {
|
||||
text: @tr("VSync");
|
||||
checked: SettingsData.vsync;
|
||||
toggled => {
|
||||
SettingsData.vsync = self.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalBox {
|
||||
alignment: start;
|
||||
Switch {
|
||||
text: @tr("Apply CRT shader");
|
||||
checked: SettingsData.apply_crt_shader;
|
||||
toggled => {
|
||||
@@ -86,7 +99,7 @@ export component Settings inherits Page {
|
||||
|
||||
HorizontalBox {
|
||||
alignment: start;
|
||||
CheckBox {
|
||||
Switch {
|
||||
text: @tr("Overclock N64 CPU (may cause bugs)");
|
||||
checked: SettingsData.overclock_n64_cpu;
|
||||
toggled => {
|
||||
@@ -97,7 +110,7 @@ export component Settings inherits Page {
|
||||
|
||||
HorizontalBox {
|
||||
alignment: start;
|
||||
CheckBox {
|
||||
Switch {
|
||||
text: @tr("Disable Expansion Pak");
|
||||
checked: SettingsData.disable_expansion_pak;
|
||||
toggled => {
|
||||
@@ -105,5 +118,16 @@ export component Settings inherits Page {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalBox {
|
||||
alignment: start;
|
||||
Switch {
|
||||
text: @tr("Emulate UNFLoader");
|
||||
checked: SettingsData.emulate_usb;
|
||||
toggled => {
|
||||
SettingsData.emulate_usb = self.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+114
-41
@@ -28,6 +28,8 @@ const Y_AXIS_SHIFT: usize = 24;
|
||||
|
||||
const MAX_AXIS_VALUE: f64 = 85.0;
|
||||
|
||||
pub const DEADZONE_DEFAULT: i32 = 5;
|
||||
|
||||
pub struct Controllers {
|
||||
pub rumble: bool,
|
||||
pub game_controller: *mut sdl3_sys::gamepad::SDL_Gamepad,
|
||||
@@ -40,7 +42,7 @@ pub struct InputData {
|
||||
}
|
||||
|
||||
fn bound_axis(x: &mut f64, y: &mut f64) {
|
||||
let radius = 95.0; // this is roughly the maxium diagonal distance of the controller
|
||||
let radius = f64::sqrt(70.0 * 70.0 + 70.0 * 70.0); // this is roughly the maximum diagonal distance of the controller
|
||||
|
||||
// Calculate the distance from the origin (0, 0)
|
||||
let distance = f64::sqrt((*x) * (*x) + (*y) * (*y));
|
||||
@@ -53,6 +55,28 @@ fn bound_axis(x: &mut f64, y: &mut f64) {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_deadzone(x: &mut f64, y: &mut f64, deadzone: i32) {
|
||||
let axis_deadzone = MAX_AXIS_VALUE * (deadzone as f64 / 100.0);
|
||||
|
||||
// Calculate the distance from the origin (0, 0)
|
||||
let distance = f64::sqrt((*x) * (*x) + (*y) * (*y));
|
||||
|
||||
if distance <= axis_deadzone {
|
||||
*x = 0.0;
|
||||
*y = 0.0;
|
||||
return;
|
||||
}
|
||||
|
||||
let new_distance =
|
||||
(distance - axis_deadzone) * MAX_AXIS_VALUE / (MAX_AXIS_VALUE - axis_deadzone);
|
||||
*x = *x / distance * new_distance;
|
||||
*y = *y / distance * new_distance;
|
||||
}
|
||||
|
||||
fn normalize_axis_position(axis_position: i16) -> f64 {
|
||||
axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64
|
||||
}
|
||||
|
||||
fn set_axis_from_joystick(
|
||||
profile: &ui::config::InputProfile,
|
||||
joystick: *mut sdl3_sys::joystick::SDL_Joystick,
|
||||
@@ -64,7 +88,7 @@ fn set_axis_from_joystick(
|
||||
sdl3_sys::joystick::SDL_GetJoystickAxis(joystick, profile.joystick_axis[AXIS_LEFT].id)
|
||||
};
|
||||
if axis_position as isize * profile.joystick_axis[AXIS_LEFT].axis as isize > 0 {
|
||||
x = axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64;
|
||||
x = normalize_axis_position(axis_position);
|
||||
}
|
||||
}
|
||||
if profile.joystick_axis[AXIS_RIGHT].enabled {
|
||||
@@ -72,7 +96,7 @@ fn set_axis_from_joystick(
|
||||
sdl3_sys::joystick::SDL_GetJoystickAxis(joystick, profile.joystick_axis[AXIS_RIGHT].id)
|
||||
};
|
||||
if axis_position as isize * profile.joystick_axis[AXIS_RIGHT].axis as isize > 0 {
|
||||
x = axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64;
|
||||
x = normalize_axis_position(axis_position);
|
||||
}
|
||||
}
|
||||
if profile.joystick_axis[AXIS_DOWN].enabled {
|
||||
@@ -80,7 +104,7 @@ fn set_axis_from_joystick(
|
||||
sdl3_sys::joystick::SDL_GetJoystickAxis(joystick, profile.joystick_axis[AXIS_DOWN].id)
|
||||
};
|
||||
if axis_position as isize * profile.joystick_axis[AXIS_DOWN].axis as isize > 0 {
|
||||
y = (axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64).neg();
|
||||
y = normalize_axis_position(axis_position).neg();
|
||||
}
|
||||
}
|
||||
if profile.joystick_axis[AXIS_UP].enabled {
|
||||
@@ -88,7 +112,7 @@ fn set_axis_from_joystick(
|
||||
sdl3_sys::joystick::SDL_GetJoystickAxis(joystick, profile.joystick_axis[AXIS_UP].id)
|
||||
};
|
||||
if axis_position as isize * profile.joystick_axis[AXIS_UP].axis as isize > 0 {
|
||||
y = (axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64).neg();
|
||||
y = normalize_axis_position(axis_position).neg();
|
||||
}
|
||||
}
|
||||
(x, y)
|
||||
@@ -123,6 +147,17 @@ fn get_button_from_i32(button: i32) -> sdl3_sys::gamepad::SDL_GamepadButton {
|
||||
12 => sdl3_sys::gamepad::SDL_GamepadButton::DPAD_DOWN,
|
||||
13 => sdl3_sys::gamepad::SDL_GamepadButton::DPAD_LEFT,
|
||||
14 => sdl3_sys::gamepad::SDL_GamepadButton::DPAD_RIGHT,
|
||||
15 => sdl3_sys::gamepad::SDL_GamepadButton::MISC1,
|
||||
16 => sdl3_sys::gamepad::SDL_GamepadButton::RIGHT_PADDLE1,
|
||||
17 => sdl3_sys::gamepad::SDL_GamepadButton::LEFT_PADDLE1,
|
||||
18 => sdl3_sys::gamepad::SDL_GamepadButton::RIGHT_PADDLE2,
|
||||
19 => sdl3_sys::gamepad::SDL_GamepadButton::LEFT_PADDLE2,
|
||||
20 => sdl3_sys::gamepad::SDL_GamepadButton::TOUCHPAD,
|
||||
21 => sdl3_sys::gamepad::SDL_GamepadButton::MISC2,
|
||||
22 => sdl3_sys::gamepad::SDL_GamepadButton::MISC3,
|
||||
23 => sdl3_sys::gamepad::SDL_GamepadButton::MISC4,
|
||||
24 => sdl3_sys::gamepad::SDL_GamepadButton::MISC5,
|
||||
25 => sdl3_sys::gamepad::SDL_GamepadButton::MISC6,
|
||||
_ => panic!("Invalid button"),
|
||||
}
|
||||
}
|
||||
@@ -141,7 +176,7 @@ fn set_axis_from_controller(
|
||||
)
|
||||
};
|
||||
if axis_position as isize * profile.controller_axis[AXIS_LEFT].axis as isize > 0 {
|
||||
x = axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64;
|
||||
x = normalize_axis_position(axis_position);
|
||||
}
|
||||
}
|
||||
if profile.controller_axis[AXIS_RIGHT].enabled {
|
||||
@@ -152,7 +187,7 @@ fn set_axis_from_controller(
|
||||
)
|
||||
};
|
||||
if axis_position as isize * profile.controller_axis[AXIS_RIGHT].axis as isize > 0 {
|
||||
x = axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64;
|
||||
x = normalize_axis_position(axis_position);
|
||||
}
|
||||
}
|
||||
if profile.controller_axis[AXIS_DOWN].enabled {
|
||||
@@ -163,7 +198,7 @@ fn set_axis_from_controller(
|
||||
)
|
||||
};
|
||||
if axis_position as isize * profile.controller_axis[AXIS_DOWN].axis as isize > 0 {
|
||||
y = (axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64).neg();
|
||||
y = normalize_axis_position(axis_position).neg();
|
||||
}
|
||||
}
|
||||
if profile.controller_axis[AXIS_UP].enabled {
|
||||
@@ -174,7 +209,7 @@ fn set_axis_from_controller(
|
||||
)
|
||||
};
|
||||
if axis_position as isize * profile.controller_axis[AXIS_UP].axis as isize > 0 {
|
||||
y = (axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64).neg();
|
||||
y = normalize_axis_position(axis_position).neg();
|
||||
}
|
||||
}
|
||||
(x, y)
|
||||
@@ -225,8 +260,9 @@ fn set_buttons_from_joystick(
|
||||
|
||||
let profile_joystick_hat = profile.joystick_hat[i];
|
||||
if profile_joystick_hat.enabled
|
||||
&& unsafe { sdl3_sys::joystick::SDL_GetJoystickHat(joystick, profile_joystick_hat.id) }
|
||||
== profile_joystick_hat.direction
|
||||
&& (unsafe { sdl3_sys::joystick::SDL_GetJoystickHat(joystick, profile_joystick_hat.id) }
|
||||
& profile_joystick_hat.direction)
|
||||
!= 0
|
||||
{
|
||||
*keys |= 1 << i;
|
||||
}
|
||||
@@ -288,7 +324,7 @@ pub fn set_rumble(ui: &ui::Ui, channel: usize, rumble: u8) {
|
||||
controller,
|
||||
(rumble & 1) as u16 * u16::MAX,
|
||||
(rumble & 1) as u16 * u16::MAX,
|
||||
(rumble & 1) as u32 * 60000,
|
||||
(rumble & 1) as u32 * sdl3_sys::haptic::SDL_HAPTIC_INFINITY,
|
||||
)
|
||||
};
|
||||
} else if !joystick.is_null() {
|
||||
@@ -297,7 +333,7 @@ pub fn set_rumble(ui: &ui::Ui, channel: usize, rumble: u8) {
|
||||
joystick,
|
||||
(rumble & 1) as u16 * u16::MAX,
|
||||
(rumble & 1) as u16 * u16::MAX,
|
||||
(rumble & 1) as u32 * 60000,
|
||||
(rumble & 1) as u32 * sdl3_sys::haptic::SDL_HAPTIC_INFINITY,
|
||||
)
|
||||
};
|
||||
}
|
||||
@@ -326,8 +362,9 @@ fn change_paks(
|
||||
pressed =
|
||||
unsafe { sdl3_sys::joystick::SDL_GetJoystickButton(joystick, joystick_button.id) };
|
||||
} else if joystick_hat.enabled && !joystick.is_null() {
|
||||
pressed = unsafe { sdl3_sys::joystick::SDL_GetJoystickHat(joystick, joystick_hat.id) }
|
||||
== joystick_hat.direction;
|
||||
pressed = (unsafe { sdl3_sys::joystick::SDL_GetJoystickHat(joystick, joystick_hat.id) }
|
||||
& joystick_hat.direction)
|
||||
!= 0;
|
||||
} else if key.enabled {
|
||||
pressed = unsafe { *keyboard_state.offset(key.id as isize) };
|
||||
}
|
||||
@@ -347,6 +384,7 @@ pub fn get_controller_names(game_ui: &ui::Ui) -> Vec<String> {
|
||||
controllers
|
||||
}
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
pub fn get_controller_paths(game_ui: &ui::Ui) -> Vec<Option<String>> {
|
||||
let mut controller_paths: Vec<Option<String>> = vec![];
|
||||
|
||||
@@ -363,8 +401,6 @@ pub fn get_controller_paths(game_ui: &ui::Ui) -> Vec<Option<String>> {
|
||||
}
|
||||
|
||||
pub fn get(ui: &ui::Ui, channel: usize) -> InputData {
|
||||
unsafe { sdl3_sys::events::SDL_PumpEvents() };
|
||||
|
||||
let profile_name = ui.config.input.input_profile_binding[channel].clone();
|
||||
let profile = ui.config.input.input_profiles.get(&profile_name).unwrap();
|
||||
let mut keys = 0;
|
||||
@@ -407,9 +443,11 @@ pub fn get(ui: &ui::Ui, channel: usize) -> InputData {
|
||||
}
|
||||
|
||||
if !controller.is_null() {
|
||||
(x, y) = set_axis_from_controller(profile, controller)
|
||||
(x, y) = set_axis_from_controller(profile, controller);
|
||||
apply_deadzone(&mut x, &mut y, profile.deadzone);
|
||||
} else if !joystick.is_null() {
|
||||
(x, y) = set_axis_from_joystick(profile, joystick)
|
||||
(x, y) = set_axis_from_joystick(profile, joystick);
|
||||
apply_deadzone(&mut x, &mut y, profile.deadzone);
|
||||
}
|
||||
bound_axis(&mut x, &mut y);
|
||||
|
||||
@@ -452,9 +490,13 @@ pub fn clear_bindings(ui: &mut ui::Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
fn close_controllers(
|
||||
fn close_input_profile_window(
|
||||
open_joysticks: Vec<*mut sdl3_sys::joystick::SDL_Joystick>,
|
||||
open_controllers: Vec<*mut sdl3_sys::gamepad::SDL_Gamepad>,
|
||||
renderer: *mut sdl3_sys::render::SDL_Renderer,
|
||||
text_engine: *mut sdl3_ttf_sys::ttf::TTF_TextEngine,
|
||||
font: *mut sdl3_ttf_sys::ttf::TTF_Font,
|
||||
window: *mut sdl3_sys::video::SDL_Window,
|
||||
) {
|
||||
for joystick in open_joysticks {
|
||||
unsafe { sdl3_sys::joystick::SDL_CloseJoystick(joystick) }
|
||||
@@ -462,10 +504,18 @@ fn close_controllers(
|
||||
for controller in open_controllers {
|
||||
unsafe { sdl3_sys::gamepad::SDL_CloseGamepad(controller) }
|
||||
}
|
||||
|
||||
unsafe {
|
||||
sdl3_ttf_sys::ttf::TTF_CloseFont(font);
|
||||
sdl3_ttf_sys::ttf::TTF_DestroyRendererTextEngine(text_engine);
|
||||
sdl3_sys::render::SDL_DestroyRenderer(renderer);
|
||||
sdl3_sys::video::SDL_DestroyWindow(window);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
|
||||
pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool, deadzone: i32) {
|
||||
ui::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
|
||||
ui::ttf_init();
|
||||
|
||||
if profile == "default" {
|
||||
println!("Profile name cannot be default");
|
||||
@@ -498,9 +548,9 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
|
||||
if !unsafe {
|
||||
sdl3_sys::render::SDL_CreateWindowAndRenderer(
|
||||
title.as_ptr(),
|
||||
640,
|
||||
852,
|
||||
480,
|
||||
0,
|
||||
sdl3_sys::video::SDL_WindowFlags(0),
|
||||
&mut window,
|
||||
&mut renderer,
|
||||
)
|
||||
@@ -567,26 +617,43 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
|
||||
axis: 0,
|
||||
};
|
||||
|
||||
let font =
|
||||
ab_glyph::FontRef::try_from_slice(include_bytes!("../../data/Roboto-Regular.ttf")).unwrap();
|
||||
let text_engine = unsafe { sdl3_ttf_sys::ttf::TTF_CreateRendererTextEngine(renderer) };
|
||||
let font = unsafe {
|
||||
let font_bytes = include_bytes!("../../data/RobotoMono-Regular.ttf");
|
||||
sdl3_ttf_sys::ttf::TTF_OpenFontIO(
|
||||
sdl3_sys::everything::SDL_IOFromConstMem(
|
||||
font_bytes.as_ptr() as *const std::ffi::c_void,
|
||||
font_bytes.len(),
|
||||
),
|
||||
true,
|
||||
35.0,
|
||||
)
|
||||
};
|
||||
|
||||
for (key, value) in key_labels.iter() {
|
||||
let mut event: sdl3_sys::events::SDL_Event = Default::default();
|
||||
while unsafe { sdl3_sys::events::SDL_PollEvent(&mut event) } {} // clear events
|
||||
|
||||
ui::video::draw_text(
|
||||
format!("Select binding for: {key}").as_str(),
|
||||
renderer,
|
||||
&font,
|
||||
);
|
||||
|
||||
let mut key_set = false;
|
||||
while !key_set {
|
||||
ui::video::draw_text(
|
||||
format!("Select binding for: {key}").as_str(),
|
||||
renderer,
|
||||
text_engine,
|
||||
font,
|
||||
);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
while unsafe { sdl3_sys::events::SDL_PollEvent(&mut event) } {
|
||||
let event_type = unsafe { event.r#type };
|
||||
if event_type == u32::from(sdl3_sys::events::SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
|
||||
close_controllers(open_joysticks, open_controllers);
|
||||
close_input_profile_window(
|
||||
open_joysticks,
|
||||
open_controllers,
|
||||
renderer,
|
||||
text_engine,
|
||||
font,
|
||||
window,
|
||||
);
|
||||
return;
|
||||
} else if event_type == u32::from(sdl3_sys::events::SDL_EVENT_KEY_DOWN) {
|
||||
if unsafe {
|
||||
@@ -666,14 +733,18 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
ui::video::draw_text("Binding set, please wait...", renderer, text_engine, font);
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
}
|
||||
|
||||
close_controllers(open_joysticks, open_controllers);
|
||||
|
||||
unsafe {
|
||||
sdl3_sys::render::SDL_DestroyRenderer(renderer);
|
||||
sdl3_sys::video::SDL_DestroyWindow(window);
|
||||
}
|
||||
close_input_profile_window(
|
||||
open_joysticks,
|
||||
open_controllers,
|
||||
renderer,
|
||||
text_engine,
|
||||
font,
|
||||
window,
|
||||
);
|
||||
|
||||
let new_profile = ui::config::InputProfile {
|
||||
keys: new_keys,
|
||||
@@ -683,6 +754,7 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
|
||||
joystick_hat: new_joystick_hat,
|
||||
joystick_axis: new_joystick_axis,
|
||||
dinput,
|
||||
deadzone,
|
||||
};
|
||||
ui.config.input.input_profiles.insert(profile, new_profile);
|
||||
}
|
||||
@@ -883,6 +955,7 @@ pub fn get_default_profile() -> ui::config::InputProfile {
|
||||
axis: 0,
|
||||
}; PROFILE_SIZE],
|
||||
dinput: false,
|
||||
deadzone: DEADZONE_DEFAULT,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -896,7 +969,7 @@ pub fn init(ui: &mut ui::Ui) {
|
||||
for i in 0..4 {
|
||||
let controller_assignment = &ui.config.input.controller_assignment[i];
|
||||
if controller_assignment.is_some() && ui.config.input.controller_enabled[i] {
|
||||
let mut joystick_id = 0;
|
||||
let mut joystick_id = sdl3_sys::everything::SDL_JoystickID(0);
|
||||
let assigned_path = controller_assignment.as_ref().unwrap();
|
||||
|
||||
for joystick in ui.input.joysticks.iter() {
|
||||
@@ -923,7 +996,7 @@ pub fn init(ui: &mut ui::Ui) {
|
||||
if !profile.dinput {
|
||||
let gamepad = unsafe { sdl3_sys::gamepad::SDL_OpenGamepad(joystick_id) };
|
||||
if gamepad.is_null() {
|
||||
println!("could not connect gamepad: {joystick_id}")
|
||||
println!("could not connect gamepad: {}", u32::from(joystick_id))
|
||||
} else {
|
||||
ui.input.controllers[i].game_controller = gamepad;
|
||||
let properties =
|
||||
@@ -942,7 +1015,7 @@ pub fn init(ui: &mut ui::Ui) {
|
||||
} else {
|
||||
let joystick = unsafe { sdl3_sys::joystick::SDL_OpenJoystick(joystick_id) };
|
||||
if joystick.is_null() {
|
||||
println!("could not connect joystick: {joystick_id}")
|
||||
println!("could not connect joystick: {}", u32::from(joystick_id))
|
||||
} else {
|
||||
ui.input.controllers[i].joystick = joystick;
|
||||
let properties =
|
||||
|
||||
+179
-187
@@ -1,8 +1,8 @@
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
use crate::ui::gui::{
|
||||
AppWindow, CustomNetplayServer, DispatcherDialog, ErrorDialog, GameSettings, GbPaths,
|
||||
NetplayCreate, NetplayDevice, NetplayJoin, NetplayWait, VruChannel, run_rom, save_settings,
|
||||
AppWindow, CustomNetplayServer, DispatcherDialog, ErrorDialog, NetplayCreate, NetplayDevice,
|
||||
NetplayJoin, NetplayWait, RASettings, run_rom, save_settings,
|
||||
};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -111,7 +111,7 @@ impl NetplayPages for NetplayJoin {
|
||||
fn populate_server_names<T: ComponentHandle + NetplayPages + 'static>(weak: slint::Weak<T>) {
|
||||
let task = reqwest::Client::new()
|
||||
.get("https://dispatch.gopher64.com/getRegions")
|
||||
.header("netplay-id", EMU_NAME)
|
||||
.header("netplay-id", env!("NETPLAY_ID"))
|
||||
.send();
|
||||
tokio::spawn(async move {
|
||||
let mut local_servers: Vec<(String, String)> = vec![];
|
||||
@@ -121,10 +121,9 @@ fn populate_server_names<T: ComponentHandle + NetplayPages + 'static>(weak: slin
|
||||
.unwrap();
|
||||
broadcast_sock.set_broadcast(true).unwrap();
|
||||
let data: [u8; 1] = [1];
|
||||
broadcast_sock
|
||||
let _ = broadcast_sock
|
||||
.send_to(&data, (std::net::Ipv4Addr::BROADCAST, 45000))
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
let mut buffer = [0; 1024];
|
||||
if let Ok(Ok(result)) = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(200),
|
||||
@@ -140,31 +139,30 @@ fn populate_server_names<T: ComponentHandle + NetplayPages + 'static>(weak: slin
|
||||
}
|
||||
|
||||
let response = task.await;
|
||||
if let Ok(response) = response
|
||||
let public_servers = if let Ok(response) = response
|
||||
&& let Ok(servers) = response.json::<Vec<String>>().await
|
||||
{
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
let server_names: slint::VecModel<slint::SharedString> = slint::VecModel::default();
|
||||
let server_urls: slint::VecModel<slint::SharedString> = slint::VecModel::default();
|
||||
for local_server in local_servers {
|
||||
server_names.push(local_server.0.into());
|
||||
server_urls.push(local_server.1.into());
|
||||
}
|
||||
for server in servers {
|
||||
server_names.push(server.clone().into());
|
||||
server_urls.push(format!("dispatcher:{server}").into());
|
||||
}
|
||||
server_names.push("Custom".into());
|
||||
handle.refresh_sessions();
|
||||
let server_names_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
|
||||
std::rc::Rc::new(server_names);
|
||||
let server_urls_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
|
||||
std::rc::Rc::new(server_urls);
|
||||
handle.set_server_names(slint::ModelRc::from(server_names_model));
|
||||
handle.set_server_urls(slint::ModelRc::from(server_urls_model));
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
servers
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
let server_names: slint::VecModel<slint::SharedString> = slint::VecModel::default();
|
||||
let server_urls: slint::VecModel<slint::SharedString> = slint::VecModel::default();
|
||||
for local_server in local_servers {
|
||||
server_names.push(local_server.0.into());
|
||||
server_urls.push(local_server.1.into());
|
||||
}
|
||||
for server in public_servers {
|
||||
server_names.push(server.clone().into());
|
||||
server_urls.push(format!("dispatcher:{server}").into());
|
||||
}
|
||||
server_names.push("Custom".into());
|
||||
handle.refresh_sessions();
|
||||
handle.set_server_names(slint::ModelRc::from(std::rc::Rc::new(server_names)));
|
||||
handle.set_server_urls(slint::ModelRc::from(std::rc::Rc::new(server_urls)));
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -187,15 +185,23 @@ fn select_rom<T: ComponentHandle + NetplayPages + 'static>(
|
||||
if let Some(file) = select_rom.await {
|
||||
if let Some(rom_contents) = device::get_rom_contents(file.path()) {
|
||||
let hash = device::cart::rom::calculate_hash(&rom_contents);
|
||||
let game_name = ui::storage::get_game_name(&rom_contents);
|
||||
let mut game_name = ui::storage::get_game_name(&rom_contents);
|
||||
let game_crc = ui::storage::get_game_crc(&rom_contents);
|
||||
let cheats = ui::config::Cheats::new();
|
||||
let mut parsed_cheats = "".to_string();
|
||||
let mut parsed_cheats = String::new();
|
||||
if let Some(game_cheats) = cheats.cheats.get(&game_crc)
|
||||
&& !game_cheats.is_empty()
|
||||
{
|
||||
parsed_cheats = serde_json::to_string(game_cheats).unwrap();
|
||||
}
|
||||
if game_name.is_empty() {
|
||||
game_name = file
|
||||
.path()
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
}
|
||||
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_game_name(game_name.into());
|
||||
@@ -214,10 +220,10 @@ fn select_rom<T: ComponentHandle + NetplayPages + 'static>(
|
||||
message_dialog.set_text("Could not read ROM".into());
|
||||
message_dialog.show().unwrap();
|
||||
|
||||
handle.set_game_name("".into());
|
||||
handle.set_game_hash("".into());
|
||||
handle.set_game_cheats("".into());
|
||||
handle.set_rom_path("".into());
|
||||
handle.set_game_name(String::new().into());
|
||||
handle.set_game_hash(String::new().into());
|
||||
handle.set_game_cheats(String::new().into());
|
||||
handle.set_rom_path(String::new().into());
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
@@ -241,7 +247,8 @@ fn show_custom_url_dialog(weak: slint::Weak<NetplayCreate>, server_url: slint::S
|
||||
|
||||
pub fn setup_create_window(
|
||||
create_window: &NetplayCreate,
|
||||
game_settings: GameSettings,
|
||||
game_settings: ui::GameSettings,
|
||||
ra_settings: RASettings,
|
||||
rom_dir: slint::SharedString,
|
||||
weak_app: slint::Weak<AppWindow>,
|
||||
) {
|
||||
@@ -288,7 +295,7 @@ pub fn setup_create_window(
|
||||
let task = reqwest::Client::new()
|
||||
.get("https://dispatch.gopher64.com/createServer")
|
||||
.query(&[("region", server_url.strip_prefix("dispatcher:").unwrap())])
|
||||
.header("netplay-id", EMU_NAME)
|
||||
.header("netplay-id", env!("NETPLAY_ID"))
|
||||
.send();
|
||||
let netplay_read_sender = netplay_read_sender.clone();
|
||||
let netplay_write_receiver = netplay_write_receiver.resubscribe();
|
||||
@@ -297,6 +304,7 @@ pub fn setup_create_window(
|
||||
let game_settings = game_settings.clone();
|
||||
let weak = weak.clone();
|
||||
let weak_app = weak_app.clone();
|
||||
let ra_settings = ra_settings.clone();
|
||||
tokio::spawn(async move {
|
||||
let response = task.await;
|
||||
weak_dialog
|
||||
@@ -328,6 +336,7 @@ pub fn setup_create_window(
|
||||
game_cheats.to_string(),
|
||||
password.to_string(),
|
||||
game_settings,
|
||||
ra_settings,
|
||||
weak_app,
|
||||
weak.clone(),
|
||||
);
|
||||
@@ -357,6 +366,7 @@ pub fn setup_create_window(
|
||||
game_cheats.to_string(),
|
||||
password.to_string(),
|
||||
game_settings.clone(),
|
||||
ra_settings.clone(),
|
||||
weak_app.clone(),
|
||||
weak.clone(),
|
||||
);
|
||||
@@ -440,7 +450,7 @@ fn show_netplay_error(message: String) {
|
||||
fn update_sessions(weak: slint::Weak<NetplayJoin>) {
|
||||
let task = reqwest::Client::new()
|
||||
.get("https://dispatch.gopher64.com/getServers")
|
||||
.header("netplay-id", EMU_NAME)
|
||||
.header("netplay-id", env!("NETPLAY_ID"))
|
||||
.send();
|
||||
tokio::spawn(async move {
|
||||
let mut dispatcher_servers = std::collections::HashMap::new();
|
||||
@@ -483,6 +493,7 @@ fn update_sessions(weak: slint::Weak<NetplayJoin>) {
|
||||
tokio::spawn(async move {
|
||||
let mut sessions = vec![];
|
||||
let mut room_urls = vec![];
|
||||
let mut room_ports = vec![];
|
||||
for (server_name, server_url) in servers.iter() {
|
||||
if let Ok(Ok((socket, _response))) = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
@@ -505,7 +516,13 @@ fn update_sessions(weak: slint::Weak<NetplayJoin>) {
|
||||
rooms: None,
|
||||
message: None,
|
||||
auth_time: Some(now_utc),
|
||||
auth: Some(format!("{:x}", hasher.finalize())),
|
||||
auth: Some(
|
||||
hasher
|
||||
.finalize()
|
||||
.iter()
|
||||
.map(|b| format!("{:02x}", b))
|
||||
.collect(),
|
||||
),
|
||||
room: None,
|
||||
};
|
||||
write
|
||||
@@ -515,50 +532,45 @@ fn update_sessions(weak: slint::Weak<NetplayJoin>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Some(Ok(response)) = read.next().await {
|
||||
if let Ok(message) =
|
||||
if let Some(Ok(response)) = read.next().await
|
||||
&& let Ok(message) =
|
||||
serde_json::from_slice::<NetplayMessage>(&response.into_data())
|
||||
{
|
||||
if message.message_type == "reply_get_rooms"
|
||||
&& message.accept.unwrap() == 0
|
||||
&& let Some(rooms) = message.rooms
|
||||
{
|
||||
for room in rooms {
|
||||
let mut session = vec![];
|
||||
room_urls.push(server_url.into());
|
||||
&& message.message_type == "reply_get_rooms"
|
||||
&& message.accept.unwrap() == 0
|
||||
&& let Some(rooms) = message.rooms
|
||||
{
|
||||
for room in rooms {
|
||||
let mut session = vec![];
|
||||
room_urls.push(server_url.into());
|
||||
room_ports.push(room.port.unwrap());
|
||||
|
||||
session.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(server_name),
|
||||
));
|
||||
session.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(room.room_name.unwrap()),
|
||||
));
|
||||
session.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(room.game_name.unwrap()),
|
||||
));
|
||||
session.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(if room.protected.unwrap() {
|
||||
"True"
|
||||
} else {
|
||||
"False"
|
||||
}),
|
||||
));
|
||||
session.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(
|
||||
if room
|
||||
.features
|
||||
.unwrap_or_default()
|
||||
.contains_key("cheats")
|
||||
{
|
||||
"True"
|
||||
} else {
|
||||
"False"
|
||||
},
|
||||
),
|
||||
));
|
||||
sessions.push(session);
|
||||
}
|
||||
}
|
||||
session.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(server_name),
|
||||
));
|
||||
session.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(room.room_name.unwrap()),
|
||||
));
|
||||
session.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(room.game_name.unwrap()),
|
||||
));
|
||||
session.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(if room.protected.unwrap() {
|
||||
"True"
|
||||
} else {
|
||||
"False"
|
||||
}),
|
||||
));
|
||||
session.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(
|
||||
if room.features.unwrap_or_default().contains_key("cheats")
|
||||
{
|
||||
"True"
|
||||
} else {
|
||||
"False"
|
||||
},
|
||||
),
|
||||
));
|
||||
sessions.push(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -567,21 +579,19 @@ fn update_sessions(weak: slint::Weak<NetplayJoin>) {
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
let sessions_vec = slint::VecModel::default();
|
||||
for session in sessions.iter() {
|
||||
let session_vec = slint::VecModel::from(session.to_vec());
|
||||
let session_model: std::rc::Rc<
|
||||
slint::VecModel<slint::StandardListViewItem>,
|
||||
> = std::rc::Rc::new(session_vec);
|
||||
sessions_vec.push(slint::ModelRc::from(session_model));
|
||||
sessions_vec.push(slint::ModelRc::from(std::rc::Rc::new(
|
||||
slint::VecModel::from(session.to_vec()),
|
||||
)));
|
||||
}
|
||||
let rooms_model: std::rc::Rc<
|
||||
slint::VecModel<slint::ModelRc<slint::StandardListViewItem>>,
|
||||
> = std::rc::Rc::new(sessions_vec);
|
||||
handle.set_sessions(slint::ModelRc::from(rooms_model));
|
||||
handle.set_sessions(slint::ModelRc::from(std::rc::Rc::new(sessions_vec)));
|
||||
|
||||
let room_urls_vec = slint::VecModel::from(room_urls.to_vec());
|
||||
let room_urls_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
|
||||
std::rc::Rc::new(room_urls_vec);
|
||||
handle.set_room_urls(slint::ModelRc::from(room_urls_model));
|
||||
handle.set_room_urls(slint::ModelRc::from(std::rc::Rc::new(
|
||||
slint::VecModel::from(room_urls.to_vec()),
|
||||
)));
|
||||
|
||||
handle.set_room_ports(slint::ModelRc::from(std::rc::Rc::new(
|
||||
slint::VecModel::from(room_ports.to_vec()),
|
||||
)));
|
||||
|
||||
handle.set_current_session(-1);
|
||||
handle.set_pending_refresh(false);
|
||||
@@ -603,7 +613,8 @@ fn create_session(
|
||||
game_hash: String,
|
||||
game_cheats: String,
|
||||
password: String,
|
||||
game_settings: GameSettings,
|
||||
game_settings: ui::GameSettings,
|
||||
ra_settings: RASettings,
|
||||
weak_app: slint::Weak<AppWindow>,
|
||||
weak: slint::Weak<NetplayCreate>,
|
||||
) {
|
||||
@@ -625,7 +636,7 @@ fn create_session(
|
||||
let create_room = NetplayMessage {
|
||||
message_type: "request_create_room".to_string(),
|
||||
player_name: Some(player_name),
|
||||
client_sha: Some(env!("GIT_HASH").to_string()),
|
||||
client_sha: Some(env!("GIT_DESCRIBE").to_string()),
|
||||
netplay_version: Some(NETPLAY_VERSION),
|
||||
emulator: Some(EMU_NAME.to_string()),
|
||||
accept: None,
|
||||
@@ -633,7 +644,13 @@ fn create_session(
|
||||
rooms: None,
|
||||
auth_time: Some(now_utc),
|
||||
player_names: None,
|
||||
auth: Some(format!("{:x}", hasher.finalize())),
|
||||
auth: Some(
|
||||
hasher
|
||||
.finalize()
|
||||
.iter()
|
||||
.map(|b| format!("{:02x}", b))
|
||||
.collect(),
|
||||
),
|
||||
room: Some(NetplayRoom {
|
||||
room_name: Some(session_name),
|
||||
password: Some(password),
|
||||
@@ -686,13 +703,13 @@ fn create_session(
|
||||
handle.get_rom_path(),
|
||||
message.player_name.as_ref().unwrap().into(),
|
||||
session.port.unwrap(),
|
||||
true,
|
||||
GameSettings {
|
||||
fullscreen: game_settings.fullscreen,
|
||||
ui::GameSettings {
|
||||
overclock: overclock.parse().unwrap(),
|
||||
disable_expansion_pak: disable_expansion_pak.parse().unwrap(),
|
||||
cheats: serde_json::from_str(cheats).unwrap(),
|
||||
load_savestate_slot: None,
|
||||
},
|
||||
ra_settings,
|
||||
handle.get_peer_addr(),
|
||||
weak_app,
|
||||
);
|
||||
@@ -730,81 +747,40 @@ fn join_session(
|
||||
player_name: String,
|
||||
game_hash: String,
|
||||
password: String,
|
||||
room_name: String,
|
||||
fullscreen: bool,
|
||||
room_port: i32,
|
||||
ra_settings: RASettings,
|
||||
weak_app: slint::Weak<AppWindow>,
|
||||
weak: slint::Weak<NetplayJoin>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let now_utc = chrono::Utc::now().timestamp_millis().to_string();
|
||||
let hasher = Sha256::new().chain_update(&now_utc).chain_update(EMU_NAME);
|
||||
let request_rooms = NetplayMessage {
|
||||
message_type: "request_get_rooms".to_string(),
|
||||
player_name: None,
|
||||
client_sha: None,
|
||||
netplay_version: Some(NETPLAY_VERSION),
|
||||
player_names: None,
|
||||
emulator: Some(EMU_NAME.to_string()),
|
||||
accept: None,
|
||||
rooms: None,
|
||||
message: None,
|
||||
auth_time: Some(now_utc),
|
||||
auth: Some(format!("{:x}", hasher.finalize())),
|
||||
room: None,
|
||||
};
|
||||
|
||||
netplay_write_sender.send(Some(request_rooms)).unwrap();
|
||||
|
||||
let mut port = 0;
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
netplay_read_receiver.recv(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(message)) => {
|
||||
if message.message_type == "reply_get_rooms"
|
||||
&& message.accept.unwrap() == 0
|
||||
&& let Some(rooms) = message.rooms
|
||||
{
|
||||
for room in rooms {
|
||||
if room.room_name.unwrap() == room_name {
|
||||
port = room.port.unwrap_or(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
panic!("netplay_read_receiver error: {err}");
|
||||
}
|
||||
Err(_) => {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_pending_session(false);
|
||||
show_netplay_error("Server did not respond".to_string());
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let join_room = NetplayMessage {
|
||||
message_type: "request_join_room".to_string(),
|
||||
player_name: Some(player_name),
|
||||
client_sha: Some(env!("GIT_HASH").to_string()),
|
||||
netplay_version: None,
|
||||
emulator: None,
|
||||
client_sha: Some(env!("GIT_DESCRIBE").to_string()),
|
||||
netplay_version: Some(NETPLAY_VERSION),
|
||||
emulator: Some(EMU_NAME.to_string()),
|
||||
accept: None,
|
||||
message: None,
|
||||
rooms: None,
|
||||
auth_time: None,
|
||||
player_names: None,
|
||||
auth: None,
|
||||
auth_time: Some(now_utc),
|
||||
auth: Some(
|
||||
hasher
|
||||
.finalize()
|
||||
.iter()
|
||||
.map(|b| format!("{:02x}", b))
|
||||
.collect(),
|
||||
),
|
||||
room: Some(NetplayRoom {
|
||||
room_name: None,
|
||||
password: Some(password),
|
||||
game_name: None,
|
||||
md5: Some(game_hash),
|
||||
protected: None,
|
||||
port: Some(port),
|
||||
port: Some(room_port),
|
||||
features: None,
|
||||
buffer_target: None,
|
||||
}),
|
||||
@@ -850,13 +826,13 @@ fn join_session(
|
||||
handle.get_rom_path(),
|
||||
message.player_name.as_ref().unwrap().into(),
|
||||
session.port.unwrap(),
|
||||
false,
|
||||
GameSettings {
|
||||
fullscreen,
|
||||
ui::GameSettings {
|
||||
overclock: overclock.parse().unwrap(),
|
||||
disable_expansion_pak: disable_expansion_pak.parse().unwrap(),
|
||||
cheats: serde_json::from_str(cheats).unwrap(),
|
||||
load_savestate_slot: None,
|
||||
},
|
||||
ra_settings,
|
||||
handle.get_peer_addr(),
|
||||
weak_app,
|
||||
);
|
||||
@@ -896,8 +872,8 @@ fn setup_wait_window(
|
||||
rom_path: slint::SharedString,
|
||||
player_name: slint::SharedString,
|
||||
port: i32,
|
||||
can_start: bool,
|
||||
game_settings: GameSettings,
|
||||
game_settings: ui::GameSettings,
|
||||
ra_settings: RASettings,
|
||||
peer_addr: slint::SharedString,
|
||||
weak_app: slint::Weak<AppWindow>,
|
||||
) {
|
||||
@@ -910,7 +886,6 @@ fn setup_wait_window(
|
||||
wait.set_game_name(game_name);
|
||||
wait.set_rom_path(rom_path);
|
||||
wait.set_port(port);
|
||||
wait.set_can_start(can_start);
|
||||
wait.set_ping("Unknown".into());
|
||||
|
||||
let sender = netplay_write_sender.clone();
|
||||
@@ -1067,17 +1042,23 @@ fn setup_wait_window(
|
||||
.unwrap();
|
||||
}
|
||||
"reply_players" => {
|
||||
let local_player = local_player.clone();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
if let Some(player_names) = response.player_names {
|
||||
let players_vec: slint::VecModel<slint::SharedString> =
|
||||
slint::VecModel::default();
|
||||
for player in player_names {
|
||||
players_vec.push(player.into());
|
||||
if local_player == player_names[0] {
|
||||
handle.set_can_start(true);
|
||||
} else {
|
||||
handle.set_can_start(false);
|
||||
}
|
||||
let players_model: std::rc::Rc<
|
||||
slint::VecModel<slint::SharedString>,
|
||||
> = std::rc::Rc::new(players_vec);
|
||||
handle.set_players(slint::ModelRc::from(players_model));
|
||||
|
||||
handle.set_players(slint::ModelRc::from(std::rc::Rc::new(
|
||||
slint::VecModel::from(
|
||||
player_names
|
||||
.iter()
|
||||
.map(|x| x.into())
|
||||
.collect::<Vec<slint::SharedString>>(),
|
||||
),
|
||||
)));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
@@ -1108,33 +1089,33 @@ fn setup_wait_window(
|
||||
}
|
||||
|
||||
run_rom(
|
||||
GbPaths {
|
||||
rom: [None, None, None, None],
|
||||
ram: [None, None, None, None],
|
||||
},
|
||||
handle.get_rom_path().as_str().into(),
|
||||
GameSettings {
|
||||
fullscreen: game_settings.fullscreen,
|
||||
ui::GameSettings {
|
||||
overclock: game_settings.overclock,
|
||||
disable_expansion_pak: game_settings.disable_expansion_pak,
|
||||
cheats: game_settings.cheats,
|
||||
},
|
||||
VruChannel {
|
||||
vru_window_notifier: None,
|
||||
vru_word_receiver: None,
|
||||
load_savestate_slot: None,
|
||||
},
|
||||
Some(NetplayDevice {
|
||||
peer_addr: socket_addr,
|
||||
player_number: player_number as u8,
|
||||
}),
|
||||
ra_settings,
|
||||
weak_app,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
return;
|
||||
} else {
|
||||
let local_player = local_player.clone();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_can_start(can_start);
|
||||
if let Some(p0) = handle.get_players().row_data(0) {
|
||||
if p0 == local_player {
|
||||
handle.set_can_start(true);
|
||||
} else {
|
||||
handle.set_can_start(false);
|
||||
}
|
||||
}
|
||||
if let Some(message) = response.message {
|
||||
show_netplay_error(message);
|
||||
}
|
||||
@@ -1161,8 +1142,8 @@ fn setup_wait_window(
|
||||
|
||||
pub fn setup_join_window(
|
||||
join_window: &NetplayJoin,
|
||||
fullscreen: bool,
|
||||
rom_dir: slint::SharedString,
|
||||
ra_settings: RASettings,
|
||||
weak_app: slint::Weak<AppWindow>,
|
||||
) {
|
||||
let (netplay_read_sender, netplay_read_receiver): (
|
||||
@@ -1194,7 +1175,7 @@ pub fn setup_join_window(
|
||||
});
|
||||
let weak = join_window.as_weak();
|
||||
join_window.on_join_session(
|
||||
move |player_name, game_hash, password, room_url, room_name| {
|
||||
move |player_name, game_hash, password, room_url, room_port| {
|
||||
let _ = netplay_write_sender.send(None); // close current websocket if any
|
||||
manage_websocket(
|
||||
room_url.to_string(),
|
||||
@@ -1209,8 +1190,8 @@ pub fn setup_join_window(
|
||||
player_name.to_string(),
|
||||
game_hash.to_string(),
|
||||
password.to_string(),
|
||||
room_name.to_string(),
|
||||
fullscreen,
|
||||
room_port,
|
||||
ra_settings.clone(),
|
||||
weak_app.clone(),
|
||||
weak.clone(),
|
||||
);
|
||||
@@ -1233,11 +1214,17 @@ pub fn netplay_window(app: &AppWindow, controller_paths: &[Option<String>]) {
|
||||
save_settings(&handle, &controller_paths);
|
||||
setup_create_window(
|
||||
&create_window,
|
||||
GameSettings {
|
||||
fullscreen: handle.get_fullscreen(),
|
||||
ui::GameSettings {
|
||||
overclock: handle.get_overclock_n64_cpu(),
|
||||
disable_expansion_pak: handle.get_disable_expansion_pak(),
|
||||
cheats: std::collections::HashMap::new(), // not used here
|
||||
load_savestate_slot: None,
|
||||
},
|
||||
RASettings {
|
||||
enabled: handle.get_ra_enabled(),
|
||||
hardcore: handle.get_ra_hardcore(),
|
||||
challenge: handle.get_ra_challenge(),
|
||||
leaderboard: handle.get_ra_leaderboard(),
|
||||
},
|
||||
handle.get_rom_dir(),
|
||||
weak_app,
|
||||
@@ -1258,8 +1245,13 @@ pub fn netplay_window(app: &AppWindow, controller_paths: &[Option<String>]) {
|
||||
save_settings(&handle, &controller_paths);
|
||||
setup_join_window(
|
||||
&join_window,
|
||||
handle.get_fullscreen(),
|
||||
handle.get_rom_dir(),
|
||||
RASettings {
|
||||
enabled: handle.get_ra_enabled(),
|
||||
hardcore: handle.get_ra_hardcore(),
|
||||
challenge: handle.get_ra_challenge(),
|
||||
leaderboard: handle.get_ra_leaderboard(),
|
||||
},
|
||||
weak_app,
|
||||
);
|
||||
})
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
use crate::retroachievements;
|
||||
use crate::ui;
|
||||
use slint::ComponentHandle;
|
||||
|
||||
pub fn ra_window(app: &ui::gui::AppWindow) {
|
||||
let mut token = String::new();
|
||||
if let Ok(ra_config) = std::fs::read(ui::get_dirs().config_dir.join("retroachievements.json"))
|
||||
&& let Ok(result) =
|
||||
serde_json::from_slice::<retroachievements::RAConfig>(ra_config.as_ref())
|
||||
{
|
||||
app.set_ra_username(result.username.into());
|
||||
app.set_ra_enabled(result.enabled);
|
||||
app.set_ra_hardcore(result.hardcore);
|
||||
app.set_ra_challenge(result.challenge);
|
||||
app.set_ra_leaderboard(result.leaderboard);
|
||||
token = result.token;
|
||||
} else {
|
||||
app.set_ra_hardcore(true);
|
||||
}
|
||||
|
||||
if !cfg!(ra_hardcore_enabled) {
|
||||
app.set_ra_softcore_only(true);
|
||||
}
|
||||
|
||||
if app.get_ra_enabled() && !app.get_ra_username().is_empty() {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
|
||||
retroachievements::login_token_user(app.get_ra_username().to_string(), token, tx);
|
||||
set_current_user_message(app, rx);
|
||||
} else {
|
||||
app.set_ra_current_user_message("Not currently logged in".into());
|
||||
}
|
||||
|
||||
let weak_app2 = app.as_weak();
|
||||
app.on_ra_button_clicked(move |password| {
|
||||
weak_app2
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
|
||||
retroachievements::login_user(
|
||||
handle.get_ra_username().to_string(),
|
||||
password.to_string(),
|
||||
tx,
|
||||
);
|
||||
|
||||
set_current_user_message(&handle, rx);
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
app.on_ra_toggled(move |enabled, hardcore, challenge, leaderboard| {
|
||||
let file_path = ui::get_dirs().config_dir.join("retroachievements.json");
|
||||
let raconfig = if let Ok(ra_config) = std::fs::read(&file_path)
|
||||
&& let Ok(result) =
|
||||
serde_json::from_slice::<retroachievements::RAConfig>(ra_config.as_ref())
|
||||
{
|
||||
if !enabled {
|
||||
retroachievements::logout_user();
|
||||
retroachievements::RAConfig {
|
||||
username: String::new(),
|
||||
token: String::new(),
|
||||
enabled,
|
||||
hardcore,
|
||||
challenge,
|
||||
leaderboard,
|
||||
}
|
||||
} else {
|
||||
retroachievements::RAConfig {
|
||||
username: result.username,
|
||||
token: result.token,
|
||||
enabled,
|
||||
hardcore,
|
||||
challenge,
|
||||
leaderboard,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
retroachievements::RAConfig {
|
||||
username: String::new(),
|
||||
token: String::new(),
|
||||
enabled,
|
||||
hardcore,
|
||||
challenge,
|
||||
leaderboard,
|
||||
}
|
||||
};
|
||||
let f = std::fs::File::create(&file_path).unwrap();
|
||||
serde_json::to_writer_pretty(f, &raconfig).unwrap();
|
||||
});
|
||||
|
||||
app.on_ra_games_clicked(move || {
|
||||
open::that_detached("https://retroachievements.org/system/2-nintendo-64/games").unwrap();
|
||||
});
|
||||
|
||||
app.on_ra_show_profile_clicked(move || {
|
||||
open::that_detached(format!(
|
||||
"https://retroachievements.org/user/{}",
|
||||
retroachievements::get_username().unwrap_or_default()
|
||||
))
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
fn set_current_user_message(app: &ui::gui::AppWindow, rx: tokio::sync::oneshot::Receiver<bool>) {
|
||||
app.set_ra_current_user_message("Logging in...".into());
|
||||
let weak_app = app.as_weak();
|
||||
tokio::spawn(async move {
|
||||
rx.await.unwrap();
|
||||
weak_app
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
if let Some(username) = retroachievements::get_username() {
|
||||
handle.set_ra_current_user_message(format!("Logged in as {}", username).into());
|
||||
handle.set_ra_show_profile(true);
|
||||
} else {
|
||||
handle.set_ra_current_user_message("Login failed".into());
|
||||
handle.set_ra_show_profile(false);
|
||||
}
|
||||
handle.set_ra_logging_in(false);
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
+82
-47
@@ -28,13 +28,13 @@ pub struct Paths {
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Save {
|
||||
pub data: Vec<u8>,
|
||||
pub written: bool,
|
||||
pub write_pending: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct RomSave {
|
||||
pub data: std::collections::HashMap<u32, u8>,
|
||||
pub written: bool,
|
||||
pub write_pending: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
@@ -135,13 +135,13 @@ pub fn get_game_crc(rom: &[u8]) -> String {
|
||||
}
|
||||
|
||||
pub fn get_game_name(rom: &[u8]) -> String {
|
||||
let mut game_name = "".to_owned();
|
||||
let header_value = std::str::from_utf8(&rom[0x20..0x34]);
|
||||
if let Ok(header_value) = header_value {
|
||||
let re = regex::Regex::new(r"[^a-zA-Z0-9_ -]").unwrap();
|
||||
game_name = re.replace_all(header_value, "").trim().replace('\0', "");
|
||||
re.replace_all(header_value, "").trim().replace('\0', "")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
game_name
|
||||
}
|
||||
|
||||
pub fn init(ui: &mut ui::Ui, rom: &[u8]) {
|
||||
@@ -163,46 +163,46 @@ pub fn init(ui: &mut ui::Ui, rom: &[u8]) {
|
||||
ui.storage
|
||||
.paths
|
||||
.eep_file_path
|
||||
.push(prefix.to_owned() + "-" + &ui.game_hash + ".eep");
|
||||
.push(format!("{}-{}.eep", prefix, ui.game_hash));
|
||||
|
||||
ui.storage.paths.sra_file_path.clone_from(&saves_path);
|
||||
ui.storage
|
||||
.paths
|
||||
.sra_file_path
|
||||
.push(prefix.to_owned() + "-" + &ui.game_hash + ".sra");
|
||||
.push(format!("{}-{}.sra", prefix, ui.game_hash));
|
||||
|
||||
ui.storage.paths.fla_file_path.clone_from(&saves_path);
|
||||
ui.storage
|
||||
.paths
|
||||
.fla_file_path
|
||||
.push(prefix.to_owned() + "-" + &ui.game_hash + ".fla");
|
||||
.push(format!("{}-{}.fla", prefix, ui.game_hash));
|
||||
|
||||
ui.storage.paths.pak_file_path.clone_from(&saves_path);
|
||||
ui.storage
|
||||
.paths
|
||||
.pak_file_path
|
||||
.push(prefix.to_owned() + "-" + &ui.game_hash + ".mpk");
|
||||
.push(format!("{}-{}.mpk", prefix, ui.game_hash));
|
||||
|
||||
ui.storage.paths.sdcard_file_path.clone_from(&saves_path);
|
||||
ui.storage
|
||||
.paths
|
||||
.sdcard_file_path
|
||||
.push(prefix.to_owned() + "-" + &ui.game_hash + ".img");
|
||||
.push(format!("{}-{}.img", prefix, ui.game_hash));
|
||||
|
||||
ui.storage.paths.romsave_file_path.clone_from(&saves_path);
|
||||
ui.storage
|
||||
.paths
|
||||
.romsave_file_path
|
||||
.push(prefix.to_owned() + "-" + &ui.game_hash + ".romsave");
|
||||
.push(format!("{}-{}.romsave", prefix, ui.game_hash));
|
||||
|
||||
ui.storage
|
||||
.paths
|
||||
.savestate_file_path
|
||||
.clone_from(&states_path);
|
||||
ui.storage
|
||||
.paths
|
||||
.savestate_file_path
|
||||
.push(prefix.to_owned() + "-" + &ui.game_hash + ".state");
|
||||
ui.storage.paths.savestate_file_path.push(format!(
|
||||
"{}-{}.state{}",
|
||||
prefix, ui.game_hash, ui.storage.save_state_slot
|
||||
));
|
||||
}
|
||||
|
||||
pub fn load_saves(ui: &mut ui::Ui, netplay: &mut Option<netplay::Netplay>) {
|
||||
@@ -351,15 +351,10 @@ pub fn compress_file(data: &[(&[u8], &str)]) -> Vec<u8> {
|
||||
compressed_file
|
||||
}
|
||||
|
||||
fn write_rom_save(ui: &ui::Ui) {
|
||||
let data = postcard::to_stdvec(&ui.storage.saves.romsave.data).unwrap();
|
||||
std::fs::write(ui.storage.paths.romsave_file_path.clone(), data).unwrap();
|
||||
}
|
||||
|
||||
fn writeback_sdcard(device: &mut device::Device) {
|
||||
let length;
|
||||
let save_data: &[u8];
|
||||
if device.ui.storage.saves.eeprom.written {
|
||||
if device.ui.storage.saves.eeprom.write_pending {
|
||||
if device
|
||||
.ui
|
||||
.storage
|
||||
@@ -371,12 +366,15 @@ fn writeback_sdcard(device: &mut device::Device) {
|
||||
length = 4;
|
||||
}
|
||||
save_data = device.ui.storage.saves.eeprom.data.as_ref();
|
||||
} else if device.ui.storage.saves.sram.written {
|
||||
device.ui.storage.saves.eeprom.write_pending = false;
|
||||
} else if device.ui.storage.saves.sram.write_pending {
|
||||
length = device.ui.storage.saves.sram.data.len() / 512;
|
||||
save_data = device.ui.storage.saves.sram.data.as_ref();
|
||||
} else if device.ui.storage.saves.flash.written {
|
||||
device.ui.storage.saves.sram.write_pending = false;
|
||||
} else if device.ui.storage.saves.flash.write_pending {
|
||||
length = device.ui.storage.saves.flash.data.len() / 512;
|
||||
save_data = device.ui.storage.saves.flash.data.as_ref();
|
||||
device.ui.storage.saves.flash.write_pending = false;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -386,71 +384,108 @@ fn writeback_sdcard(device: &mut device::Device) {
|
||||
device.ui.storage.saves.sdcard.data[offset..offset + 512]
|
||||
.copy_from_slice(&save_data[i * 512..(i + 1) * 512]);
|
||||
}
|
||||
device.ui.storage.saves.sdcard.written = true;
|
||||
device.ui.storage.saves.sdcard.write_pending = true;
|
||||
}
|
||||
|
||||
pub fn write_saves(device: &mut device::Device) {
|
||||
if device.netplay.is_none() || device.netplay.as_ref().unwrap().player_number == 0 {
|
||||
if device.ui.storage.saves.write_to_disk {
|
||||
if device.ui.storage.saves.eeprom.written {
|
||||
write_save(&device.ui, SaveTypes::Eeprom16k)
|
||||
if device.ui.storage.saves.eeprom.write_pending {
|
||||
write_save(&mut device.ui, SaveTypes::Eeprom16k)
|
||||
}
|
||||
if device.ui.storage.saves.sram.written {
|
||||
write_save(&device.ui, SaveTypes::Sram)
|
||||
if device.ui.storage.saves.sram.write_pending {
|
||||
write_save(&mut device.ui, SaveTypes::Sram)
|
||||
}
|
||||
if device.ui.storage.saves.flash.written {
|
||||
write_save(&device.ui, SaveTypes::Flash)
|
||||
if device.ui.storage.saves.flash.write_pending {
|
||||
write_save(&mut device.ui, SaveTypes::Flash)
|
||||
}
|
||||
if device.ui.storage.saves.romsave.written {
|
||||
write_save(&device.ui, SaveTypes::Romsave)
|
||||
if device.ui.storage.saves.romsave.write_pending {
|
||||
write_save(&mut device.ui, SaveTypes::Romsave)
|
||||
}
|
||||
} else {
|
||||
writeback_sdcard(device)
|
||||
}
|
||||
if device.ui.storage.saves.mempak.written {
|
||||
write_save(&device.ui, SaveTypes::Mempak)
|
||||
if device.ui.storage.saves.mempak.write_pending {
|
||||
write_save(&mut device.ui, SaveTypes::Mempak)
|
||||
}
|
||||
if device.ui.storage.saves.sdcard.written {
|
||||
write_save(&device.ui, SaveTypes::Sdcard)
|
||||
if device.ui.storage.saves.sdcard.write_pending {
|
||||
write_save(&mut device.ui, SaveTypes::Sdcard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_save(ui: &ui::Ui, save_type: SaveTypes) {
|
||||
pub fn save_event(device: &mut device::Device) {
|
||||
write_saves(device);
|
||||
}
|
||||
|
||||
pub fn schedule_save(device: &mut device::Device, save_type: SaveTypes) {
|
||||
match save_type {
|
||||
SaveTypes::Eeprom4k | SaveTypes::Eeprom16k => {
|
||||
device.ui.storage.saves.eeprom.write_pending = true;
|
||||
}
|
||||
SaveTypes::Sram => {
|
||||
device.ui.storage.saves.sram.write_pending = true;
|
||||
}
|
||||
SaveTypes::Flash => {
|
||||
device.ui.storage.saves.flash.write_pending = true;
|
||||
}
|
||||
SaveTypes::Mempak => {
|
||||
device.ui.storage.saves.mempak.write_pending = true;
|
||||
}
|
||||
SaveTypes::Sdcard => {
|
||||
device.ui.storage.saves.sdcard.write_pending = true;
|
||||
}
|
||||
SaveTypes::Romsave => {
|
||||
device.ui.storage.saves.romsave.write_pending = true;
|
||||
}
|
||||
}
|
||||
device::events::create_event(
|
||||
device,
|
||||
device::events::EVENT_TYPE_SAVE,
|
||||
device.cpu.clock_rate, // 1 second
|
||||
);
|
||||
}
|
||||
|
||||
fn write_save(ui: &mut ui::Ui, save_type: SaveTypes) {
|
||||
let path: &std::path::Path;
|
||||
let data: &Vec<u8>;
|
||||
let rom_save_data: Vec<u8>;
|
||||
match save_type {
|
||||
SaveTypes::Eeprom4k | SaveTypes::Eeprom16k => {
|
||||
path = ui.storage.paths.eep_file_path.as_ref();
|
||||
data = ui.storage.saves.eeprom.data.as_ref();
|
||||
ui.storage.saves.eeprom.write_pending = false;
|
||||
}
|
||||
SaveTypes::Sram => {
|
||||
path = ui.storage.paths.sra_file_path.as_ref();
|
||||
data = ui.storage.saves.sram.data.as_ref();
|
||||
ui.storage.saves.sram.write_pending = false;
|
||||
}
|
||||
SaveTypes::Flash => {
|
||||
path = ui.storage.paths.fla_file_path.as_ref();
|
||||
data = ui.storage.saves.flash.data.as_ref();
|
||||
ui.storage.saves.flash.write_pending = false;
|
||||
}
|
||||
SaveTypes::Mempak => {
|
||||
path = ui.storage.paths.pak_file_path.as_ref();
|
||||
data = ui.storage.saves.mempak.data.as_ref();
|
||||
ui.storage.saves.mempak.write_pending = false;
|
||||
}
|
||||
SaveTypes::Sdcard => {
|
||||
path = ui.storage.paths.sdcard_file_path.as_ref();
|
||||
data = ui.storage.saves.sdcard.data.as_ref();
|
||||
ui.storage.saves.sdcard.write_pending = false;
|
||||
}
|
||||
SaveTypes::Romsave => {
|
||||
write_rom_save(ui);
|
||||
return;
|
||||
path = ui.storage.paths.romsave_file_path.as_ref();
|
||||
rom_save_data = postcard::to_stdvec(&ui.storage.saves.romsave.data).unwrap();
|
||||
data = rom_save_data.as_ref();
|
||||
ui.storage.saves.romsave.write_pending = false;
|
||||
}
|
||||
}
|
||||
let result = std::fs::write(path, data);
|
||||
if result.is_err() {
|
||||
panic!(
|
||||
"could not save {} {}",
|
||||
path.display(),
|
||||
result.err().unwrap()
|
||||
)
|
||||
}
|
||||
let save_data = data.clone();
|
||||
let save_path = path.to_path_buf();
|
||||
tokio::spawn(async move {
|
||||
tokio::fs::write(save_path, save_data).await.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
use crate::ui;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
const DATATYPE_TCPTEST: u32 = 0x07;
|
||||
const DATATYPE_ROMUPLOAD: u32 = 0x08;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UsbData {
|
||||
pub data: Vec<u8>,
|
||||
pub data_type: u32,
|
||||
pub data_size: u32,
|
||||
}
|
||||
|
||||
fn respond_to_handshake(usb_tx: &tokio::sync::broadcast::Sender<UsbData>, data: Vec<u8>) {
|
||||
if let Ok(data) = String::from_utf8(data)
|
||||
&& data == "N64"
|
||||
{
|
||||
ui::usb::send_to_usb(
|
||||
usb_tx,
|
||||
ui::usb::UsbData {
|
||||
data: [b'N', b'6', b'4'].to_vec(),
|
||||
data_type: DATATYPE_TCPTEST,
|
||||
data_size: 3,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(
|
||||
conn: tokio::net::TcpStream,
|
||||
mut shutdown_rx: tokio::sync::watch::Receiver<()>,
|
||||
mut usb_rx: tokio::sync::broadcast::Receiver<UsbData>,
|
||||
usb_tx: tokio::sync::broadcast::Sender<UsbData>,
|
||||
cart_tx: tokio::sync::broadcast::Sender<UsbData>,
|
||||
) {
|
||||
let (mut incoming, mut outgoing) = conn.into_split();
|
||||
|
||||
let mut shutdown_rx_clone = shutdown_rx.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
data = usb_rx.recv() => {
|
||||
match data {
|
||||
Ok(data) => {
|
||||
let mut output: Vec<u8> = vec![];
|
||||
output.extend_from_slice(&data.data_type.to_be_bytes());
|
||||
output.extend_from_slice(&data.data_size.to_be_bytes());
|
||||
output.extend_from_slice(&data.data);
|
||||
if outgoing.write_all(&output).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
|
||||
panic!("usb_rx lagged");
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = shutdown_rx_clone.changed() => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut incoming_buffer = vec![0u8; 4096];
|
||||
let mut data_type: Option<u32> = None;
|
||||
let mut data_size: Option<u32> = None;
|
||||
let mut usb_buffer: Vec<u8> = vec![];
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = incoming.read(&mut incoming_buffer) => {
|
||||
match result {
|
||||
Ok(0) => {
|
||||
break;
|
||||
}
|
||||
Ok(n) => {
|
||||
usb_buffer.extend_from_slice(&incoming_buffer[0..n]);
|
||||
if data_type.is_none() {
|
||||
if usb_buffer.len() < 4 {
|
||||
continue;
|
||||
} else {
|
||||
data_type = Some(u32::from_be_bytes(usb_buffer[0..4].try_into().unwrap()));
|
||||
usb_buffer.drain(0..4);
|
||||
}
|
||||
}
|
||||
if data_type.is_some() && data_size.is_none() {
|
||||
if usb_buffer.len() < 4 {
|
||||
continue;
|
||||
} else {
|
||||
data_size = Some(u32::from_be_bytes(usb_buffer[0..4].try_into().unwrap()));
|
||||
usb_buffer.drain(0..4);
|
||||
}
|
||||
}
|
||||
if let Some(d_type) = data_type && let Some(d_size) = data_size {
|
||||
let length = d_size as usize;
|
||||
if usb_buffer.len() >= length {
|
||||
let usb_data = UsbData {
|
||||
data: usb_buffer[0..length].to_vec(),
|
||||
data_type: d_type,
|
||||
data_size: d_size,
|
||||
};
|
||||
usb_buffer.drain(0..length);
|
||||
if usb_data.data_type == DATATYPE_TCPTEST {
|
||||
respond_to_handshake(&usb_tx,usb_data.data);
|
||||
} else if usb_data.data_type == DATATYPE_ROMUPLOAD {
|
||||
panic!("ROM upload not supported");
|
||||
} else {
|
||||
cart_tx.send(usb_data).unwrap();
|
||||
}
|
||||
data_type = None;
|
||||
data_size = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_e) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = shutdown_rx.changed() => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init() -> (Option<tokio::sync::watch::Sender<()>>, ui::Usb) {
|
||||
let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(());
|
||||
let (usb_tx, usb_rx): (
|
||||
tokio::sync::broadcast::Sender<UsbData>,
|
||||
tokio::sync::broadcast::Receiver<UsbData>,
|
||||
) = tokio::sync::broadcast::channel(1024);
|
||||
let (cart_tx, cart_rx): (
|
||||
tokio::sync::broadcast::Sender<UsbData>,
|
||||
tokio::sync::broadcast::Receiver<UsbData>,
|
||||
) = tokio::sync::broadcast::channel(1024);
|
||||
|
||||
let usb_tx_clone = usb_tx.clone();
|
||||
let cart_rx_clone = cart_rx.resubscribe();
|
||||
tokio::spawn(async move {
|
||||
let listener = tokio::net::TcpListener::bind("localhost:64000")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = listener.accept() => {
|
||||
if let Ok((c,_)) = res {
|
||||
handle_connection(c,shutdown_rx.clone(),usb_rx.resubscribe(),usb_tx.clone(),cart_tx.clone()).await;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ = shutdown_rx.changed() => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
(
|
||||
Some(shutdown_tx),
|
||||
ui::Usb {
|
||||
usb_tx: Some(usb_tx_clone),
|
||||
cart_rx: Some(cart_rx_clone),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn close(shutdown_tx: &tokio::sync::watch::Sender<()>) {
|
||||
let _ = shutdown_tx.send(());
|
||||
}
|
||||
|
||||
pub fn send_to_usb(usb_tx: &tokio::sync::broadcast::Sender<UsbData>, buffer: UsbData) {
|
||||
usb_tx.send(buffer).unwrap();
|
||||
}
|
||||
+155
-99
@@ -1,12 +1,43 @@
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_camel_case_types)]
|
||||
include!(concat!(env!("OUT_DIR"), "/parallel_bindings.rs"));
|
||||
use crate::{device, ui};
|
||||
use ab_glyph::{Font, ScaleFont};
|
||||
use crate::{device, retroachievements, ui};
|
||||
|
||||
const PAL_WIDESCREEN_WIDTH: i32 = 512;
|
||||
const PAL_STANDARD_WIDTH: i32 = 384;
|
||||
const PAL_HEIGHT: i32 = 288;
|
||||
const NTSC_WIDESCREEN_WIDTH: i32 = 426;
|
||||
const NTSC_STANDARD_WIDTH: i32 = 320;
|
||||
const NTSC_HEIGHT: i32 = 240;
|
||||
|
||||
fn build_gfx_info(device: &mut device::Device) -> GFX_INFO {
|
||||
GFX_INFO {
|
||||
RDRAM: device.rdram.mem.as_mut_ptr(),
|
||||
DMEM: device.rsp.mem.as_mut_ptr(),
|
||||
RDRAM_SIZE: device.rdram.size,
|
||||
DPC_CURRENT_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_CURRENT_REG as usize],
|
||||
DPC_START_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_START_REG as usize],
|
||||
DPC_END_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_END_REG as usize],
|
||||
DPC_STATUS_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_STATUS_REG as usize],
|
||||
PAL: device.cart.pal,
|
||||
widescreen: device.ui.config.video.widescreen,
|
||||
fullscreen: device.ui.video.fullscreen,
|
||||
vsync: if device.netplay.is_none() {
|
||||
device.ui.config.video.vsync
|
||||
} else {
|
||||
false
|
||||
},
|
||||
integer_scaling: device.ui.config.video.integer_scaling,
|
||||
upscale: device.ui.config.video.upscale,
|
||||
crt: device.ui.config.video.crt,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
ui::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
|
||||
ui::ttf_init();
|
||||
|
||||
let title = std::ffi::CString::new("gopher64").unwrap();
|
||||
let window_title = std::ffi::CString::new("gopher64").unwrap();
|
||||
|
||||
let mut flags = sdl3_sys::video::SDL_WINDOW_VULKAN
|
||||
| sdl3_sys::video::SDL_WINDOW_RESIZABLE
|
||||
@@ -18,23 +49,28 @@ pub fn init(device: &mut device::Device) {
|
||||
|
||||
let window_width;
|
||||
let window_height;
|
||||
let scale = if device.ui.config.video.upscale > 1 {
|
||||
device.ui.config.video.upscale as i32
|
||||
} else {
|
||||
2
|
||||
};
|
||||
if device.cart.pal {
|
||||
window_width = if device.ui.config.video.widescreen {
|
||||
1024
|
||||
PAL_WIDESCREEN_WIDTH * scale
|
||||
} else {
|
||||
768
|
||||
PAL_STANDARD_WIDTH * scale
|
||||
};
|
||||
window_height = 576;
|
||||
window_height = PAL_HEIGHT * scale;
|
||||
} else {
|
||||
window_width = if device.ui.config.video.widescreen {
|
||||
852
|
||||
NTSC_WIDESCREEN_WIDTH * scale
|
||||
} else {
|
||||
640
|
||||
NTSC_STANDARD_WIDTH * scale
|
||||
};
|
||||
window_height = 480;
|
||||
window_height = NTSC_HEIGHT * scale;
|
||||
}
|
||||
device.ui.video.window = unsafe {
|
||||
sdl3_sys::video::SDL_CreateWindow(title.as_ptr(), window_width, window_height, flags)
|
||||
sdl3_sys::video::SDL_CreateWindow(window_title.as_ptr(), window_width, window_height, flags)
|
||||
};
|
||||
if device.ui.video.window.is_null() {
|
||||
panic!("Could not create window: {}", unsafe {
|
||||
@@ -52,29 +88,54 @@ pub fn init(device: &mut device::Device) {
|
||||
}
|
||||
unsafe {
|
||||
sdl3_sys::everything::SDL_HideCursor();
|
||||
let hint = std::ffi::CString::new("1").unwrap();
|
||||
sdl3_sys::everything::SDL_SetHint(
|
||||
sdl3_sys::everything::SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS,
|
||||
std::ffi::CString::new("1").unwrap().as_ptr(),
|
||||
hint.as_ptr(),
|
||||
);
|
||||
}
|
||||
|
||||
let gfx_info = GFX_INFO {
|
||||
RDRAM: device.rdram.mem.as_mut_ptr(),
|
||||
DMEM: device.rsp.mem.as_mut_ptr(),
|
||||
RDRAM_SIZE: device.rdram.size,
|
||||
DPC_CURRENT_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_CURRENT_REG as usize],
|
||||
DPC_START_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_START_REG as usize],
|
||||
DPC_END_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_END_REG as usize],
|
||||
DPC_STATUS_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_STATUS_REG as usize],
|
||||
PAL: device.cart.pal,
|
||||
widescreen: device.ui.config.video.widescreen,
|
||||
fullscreen: device.ui.video.fullscreen,
|
||||
integer_scaling: device.ui.config.video.integer_scaling,
|
||||
upscale: device.ui.config.video.upscale,
|
||||
crt: device.ui.config.video.crt,
|
||||
};
|
||||
let gfx_info = build_gfx_info(device);
|
||||
|
||||
unsafe { rdp_init(device.ui.video.window as *mut std::ffi::c_void, gfx_info) }
|
||||
unsafe {
|
||||
let font_bytes = include_bytes!("../../data/RobotoMono-Regular.ttf");
|
||||
rdp_init(
|
||||
device.ui.video.window as *mut std::ffi::c_void,
|
||||
gfx_info,
|
||||
font_bytes.as_ptr() as *const std::ffi::c_void,
|
||||
font_bytes.len(),
|
||||
device.ui.storage.save_state_slot,
|
||||
)
|
||||
}
|
||||
|
||||
fps_counter(&mut device.ui);
|
||||
}
|
||||
|
||||
fn fps_counter(ui: &mut ui::Ui) {
|
||||
let mut fps_rx = ui.video.fps_rx.take().unwrap();
|
||||
let mut vis_rx = ui.video.vis_rx.take().unwrap();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let mut fps: u32 = 0;
|
||||
let mut vis: u32 = 0;
|
||||
loop {
|
||||
match fps_rx.try_recv() {
|
||||
Ok(_fps_update) => fps += 1,
|
||||
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
|
||||
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => return,
|
||||
}
|
||||
}
|
||||
loop {
|
||||
match vis_rx.try_recv() {
|
||||
Ok(_vis_update) => vis += 1,
|
||||
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
|
||||
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => return,
|
||||
}
|
||||
}
|
||||
unsafe { rdp_set_fps(fps, vis) };
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn close(ui: &ui::Ui) {
|
||||
@@ -85,10 +146,13 @@ pub fn close(ui: &ui::Ui) {
|
||||
}
|
||||
|
||||
pub fn update_screen() {
|
||||
// when the window is closed, running is set to false
|
||||
unsafe { rdp_update_screen() }
|
||||
}
|
||||
|
||||
pub fn render_frame() {
|
||||
unsafe { rdp_render_frame() }
|
||||
}
|
||||
|
||||
pub fn state_size() -> usize {
|
||||
unsafe { rdp_state_size() }
|
||||
}
|
||||
@@ -98,21 +162,7 @@ pub fn save_state(rdp_state: *mut u8) {
|
||||
}
|
||||
|
||||
pub fn load_state(device: &mut device::Device, rdp_state: *const u8) {
|
||||
let gfx_info = GFX_INFO {
|
||||
RDRAM: device.rdram.mem.as_mut_ptr(),
|
||||
DMEM: device.rsp.mem.as_mut_ptr(),
|
||||
RDRAM_SIZE: device.rdram.size,
|
||||
DPC_CURRENT_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_CURRENT_REG as usize],
|
||||
DPC_START_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_START_REG as usize],
|
||||
DPC_END_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_END_REG as usize],
|
||||
DPC_STATUS_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_STATUS_REG as usize],
|
||||
PAL: device.cart.pal,
|
||||
widescreen: device.ui.config.video.widescreen,
|
||||
fullscreen: device.ui.video.fullscreen,
|
||||
integer_scaling: device.ui.config.video.integer_scaling,
|
||||
upscale: device.ui.config.video.upscale,
|
||||
crt: device.ui.config.video.crt,
|
||||
};
|
||||
let gfx_info = build_gfx_info(device);
|
||||
unsafe {
|
||||
rdp_new_processor(gfx_info);
|
||||
rdp_load_state(rdp_state);
|
||||
@@ -122,9 +172,22 @@ pub fn load_state(device: &mut device::Device, rdp_state: *const u8) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_callback(device: &mut device::Device) -> bool {
|
||||
pub fn pause_loop(frame_time: f64) {
|
||||
let mut paused = true;
|
||||
let mut frame_advance = false;
|
||||
while paused && !frame_advance {
|
||||
std::thread::sleep(std::time::Duration::from_secs_f64(frame_time));
|
||||
unsafe { sdl3_sys::events::SDL_PumpEvents() };
|
||||
retroachievements::do_idle();
|
||||
let callback = unsafe { rdp_check_callback() };
|
||||
paused = callback.paused;
|
||||
frame_advance = callback.frame_advance;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_callback(device: &mut device::Device) -> (bool, bool) {
|
||||
let mut speed_limiter_toggled = false;
|
||||
let mut callback = unsafe { rdp_check_callback() };
|
||||
let callback = unsafe { rdp_check_callback() };
|
||||
device.cpu.running = callback.emu_running;
|
||||
if device.netplay.is_none() {
|
||||
if callback.save_state {
|
||||
@@ -132,23 +195,43 @@ pub fn check_callback(device: &mut device::Device) -> bool {
|
||||
} else if callback.load_state {
|
||||
device.load_state = true;
|
||||
}
|
||||
if callback.reset_game {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] |=
|
||||
device::cop0::COP0_CAUSE_IP4;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
|
||||
!device::cop0::COP0_CAUSE_EXCCODE_MASK;
|
||||
|
||||
device::events::create_event(
|
||||
device,
|
||||
device::events::EVENT_TYPE_NMI,
|
||||
device.cpu.clock_rate, // 1 second
|
||||
);
|
||||
}
|
||||
if device.vi.enable_speed_limiter != callback.enable_speedlimiter {
|
||||
speed_limiter_toggled = true;
|
||||
device.vi.enable_speed_limiter = callback.enable_speedlimiter;
|
||||
}
|
||||
while callback.paused {
|
||||
std::thread::sleep(std::time::Duration::from_secs_f64(1.0 / 60.0));
|
||||
unsafe { sdl3_sys::events::SDL_PumpEvents() };
|
||||
callback = unsafe { rdp_check_callback() };
|
||||
}
|
||||
}
|
||||
|
||||
if device.ui.storage.save_state_slot != callback.save_state_slot {
|
||||
onscreen_message(
|
||||
&format!("Switching savestate slot to {}", callback.save_state_slot),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
device.ui.storage.save_state_slot = callback.save_state_slot;
|
||||
device
|
||||
.ui
|
||||
.storage
|
||||
.paths
|
||||
.savestate_file_path
|
||||
.set_extension(format!("state{}", callback.save_state_slot));
|
||||
}
|
||||
if callback.lower_volume {
|
||||
ui::audio::lower_audio_volume(&mut device.ui);
|
||||
} else if callback.raise_volume {
|
||||
ui::audio::raise_audio_volume(&mut device.ui);
|
||||
}
|
||||
speed_limiter_toggled
|
||||
(speed_limiter_toggled, callback.paused)
|
||||
}
|
||||
|
||||
pub fn set_register(reg: u32, value: u32) {
|
||||
@@ -161,60 +244,33 @@ pub fn process_rdp_list() -> u64 {
|
||||
unsafe { rdp_process_commands() }
|
||||
}
|
||||
|
||||
pub fn check_framebuffers(address: u32, length: u32) {
|
||||
unsafe { rdp_check_framebuffers(address, length) }
|
||||
}
|
||||
|
||||
pub fn onscreen_message(message: &str, milliseconds: MESSAGE_LENGTH) {
|
||||
unsafe {
|
||||
let c_message = std::ffi::CString::new(message).unwrap();
|
||||
rdp_onscreen_message(c_message.as_ptr(), milliseconds)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn draw_text(
|
||||
text: &str,
|
||||
renderer: *mut sdl3_sys::render::SDL_Renderer,
|
||||
font: &ab_glyph::FontRef,
|
||||
text_engine: *mut sdl3_ttf_sys::ttf::TTF_TextEngine,
|
||||
font: *mut sdl3_ttf_sys::ttf::TTF_Font,
|
||||
) {
|
||||
// Clear the canvas
|
||||
unsafe {
|
||||
sdl3_sys::render::SDL_SetRenderDrawColor(
|
||||
renderer,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
sdl3_sys::pixels::SDL_ALPHA_OPAQUE,
|
||||
);
|
||||
sdl3_sys::render::SDL_RenderClear(renderer);
|
||||
};
|
||||
let (mut w, mut h) = (0, 0);
|
||||
sdl3_sys::render::SDL_GetRenderOutputSize(renderer, &mut w, &mut h);
|
||||
|
||||
let text_size = 40.0;
|
||||
let (mut w, mut h) = (0, 0);
|
||||
unsafe { sdl3_sys::render::SDL_GetRenderOutputSize(renderer, &mut w, &mut h) };
|
||||
let x_start = 20.0;
|
||||
let y_start = (h / 2) as f32;
|
||||
let c_text = std::ffi::CString::new(text).unwrap();
|
||||
let ttf_text = sdl3_ttf_sys::ttf::TTF_CreateText(text_engine, font, c_text.as_ptr(), 0);
|
||||
|
||||
let mut x_offset = 0.0;
|
||||
for c in text.chars() {
|
||||
let q_glyph_id = font.glyph_id(c);
|
||||
let q_glyph = q_glyph_id.with_scale(text_size);
|
||||
|
||||
if let Some(q) = font.outline_glyph(q_glyph) {
|
||||
q.draw(|x, y, c| {
|
||||
if c > 0.5 {
|
||||
unsafe {
|
||||
sdl3_sys::render::SDL_SetRenderDrawColor(
|
||||
renderer,
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
sdl3_sys::pixels::SDL_ALPHA_OPAQUE,
|
||||
);
|
||||
sdl3_sys::render::SDL_RenderPoint(
|
||||
renderer,
|
||||
x_start + x_offset + x as f32 - q.px_bounds().width()
|
||||
+ q.px_bounds().max.x,
|
||||
y_start + y as f32 - q.px_bounds().height() + q.px_bounds().max.y,
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
x_offset += font.as_scaled(text_size).h_advance(q_glyph_id);
|
||||
}
|
||||
|
||||
// Present the canvas
|
||||
if !unsafe { sdl3_sys::render::SDL_RenderPresent(renderer) } {
|
||||
panic!("Could not present renderer");
|
||||
sdl3_sys::everything::SDL_RenderClear(renderer);
|
||||
sdl3_ttf_sys::ttf::TTF_DrawRendererText(ttf_text, 20.0, h as f32 / 2.0);
|
||||
sdl3_sys::render::SDL_RenderPresent(renderer);
|
||||
sdl3_ttf_sys::ttf::TTF_DestroyText(ttf_text);
|
||||
}
|
||||
}
|
||||
|
||||
+42
-15
@@ -1,21 +1,48 @@
|
||||
pub fn prompt_for_match(
|
||||
words: &[String],
|
||||
window_notifier: &tokio::sync::mpsc::Sender<Option<Vec<String>>>,
|
||||
word_index_receiver: &mut tokio::sync::mpsc::Receiver<String>,
|
||||
) -> u16 {
|
||||
use crate::retroachievements;
|
||||
use crate::ui::gui;
|
||||
use slint::ComponentHandle;
|
||||
|
||||
pub fn prompt_for_match(words: &[String], frame_time: f64) -> u16 {
|
||||
let mut dedup_words = words.to_owned();
|
||||
dedup_words.sort();
|
||||
dedup_words.dedup();
|
||||
window_notifier.try_send(Some(dedup_words)).unwrap();
|
||||
let mut result = word_index_receiver.try_recv();
|
||||
while result.is_err() {
|
||||
result = word_index_receiver.try_recv();
|
||||
std::thread::sleep(std::time::Duration::from_secs_f64(1.0 / 60.0));
|
||||
unsafe { sdl3_sys::events::SDL_PumpEvents() }; // so the OS doesn't complain about the game window being frozen
|
||||
}
|
||||
for (i, v) in words.iter().enumerate() {
|
||||
if *v == *result.as_ref().unwrap() {
|
||||
return i as u16;
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
|
||||
|
||||
let vru_dialog = gui::VruDialog::new().unwrap();
|
||||
let vru_dialog_weak = vru_dialog.as_weak();
|
||||
|
||||
vru_dialog.on_vru_button_clicked(move |chosen_word| {
|
||||
tx.try_send(chosen_word.to_string()).unwrap();
|
||||
vru_dialog_weak.unwrap().window().hide().unwrap();
|
||||
});
|
||||
|
||||
vru_dialog.set_words(slint::ModelRc::from(std::rc::Rc::new(
|
||||
slint::VecModel::from(
|
||||
dedup_words
|
||||
.iter()
|
||||
.map(|x| x.into())
|
||||
.collect::<Vec<slint::SharedString>>(),
|
||||
),
|
||||
)));
|
||||
|
||||
let timer = slint::Timer::default();
|
||||
timer.start(
|
||||
slint::TimerMode::Repeated,
|
||||
std::time::Duration::from_secs_f64(frame_time),
|
||||
move || {
|
||||
unsafe { sdl3_sys::events::SDL_PumpEvents() }; // so the OS doesn't complain about the game window being frozen
|
||||
retroachievements::do_idle();
|
||||
},
|
||||
);
|
||||
|
||||
vru_dialog.run().unwrap();
|
||||
|
||||
if let Ok(result) = rx.try_recv() {
|
||||
for (i, v) in words.iter().enumerate() {
|
||||
if *v == result {
|
||||
return i as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
0x7FFF
|
||||
|
||||
Reference in New Issue
Block a user