mirror of
https://github.com/gopher64/gopher64.git
synced 2026-07-11 09:35:21 +02:00
Compare commits
196 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 |
+9
-2
@@ -1,16 +1,23 @@
|
||||
[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"
|
||||
|
||||
@@ -8,6 +8,7 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
# - package-ecosystem: "cargo"
|
||||
# directory: "/"
|
||||
# schedule:
|
||||
|
||||
+103
-32
@@ -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@v5
|
||||
- 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@v5
|
||||
- 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.3
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
with:
|
||||
api-token: "${{ secrets.SIGNPATH_API_TOKEN }}"
|
||||
organization-id: "${{ secrets.SIGNPATH_ORG_ID }}"
|
||||
@@ -118,7 +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: false
|
||||
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
|
||||
@@ -126,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@v5
|
||||
- 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@v5
|
||||
- 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@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: rustsec/audit-check@v2
|
||||
|
||||
+6
-2
@@ -1,7 +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
+2451
-1880
File diff suppressed because it is too large
Load Diff
+28
-16
@@ -1,51 +1,63 @@
|
||||
[package]
|
||||
name = "gopher64"
|
||||
version = "1.1.7"
|
||||
version = "1.1.18"
|
||||
edition = "2024"
|
||||
rust-version = "1.90"
|
||||
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 = "5.1"
|
||||
governor = "0.10"
|
||||
sevenz-rust2 = { version = "0.19", 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-wgpu"] }
|
||||
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"
|
||||
sdl3-sys = { version = "0.5", features = ["build-from-source-static"] }
|
||||
sdl3-ttf-sys = { version = "0.2", features = ["build-static-vendored", "no-sdlttf-harfbuzz", "no-sdlttf-plutosvg"] }
|
||||
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.28"
|
||||
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")
|
||||
@@ -54,7 +57,6 @@ fn main() {
|
||||
.file("parallel-rdp/parallel-rdp-standalone/util/timeline_trace_file.cpp")
|
||||
.file("parallel-rdp/parallel-rdp-standalone/util/environment.cpp")
|
||||
.file("parallel-rdp/parallel-rdp-standalone/util/thread_name.cpp")
|
||||
.file("parallel-rdp/parallel-rdp-standalone/util/slab_allocator.cpp")
|
||||
.file("parallel-rdp/interface.cpp")
|
||||
.file("parallel-rdp/wsi_platform.cpp")
|
||||
.include("parallel-rdp/parallel-rdp-standalone/parallel-rdp")
|
||||
@@ -63,19 +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())
|
||||
.join("include"),
|
||||
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").to_owned().unwrap())
|
||||
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 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")
|
||||
@@ -84,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");
|
||||
@@ -93,17 +132,69 @@ 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");
|
||||
}
|
||||
|
||||
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")
|
||||
@@ -119,6 +210,7 @@ fn main() {
|
||||
.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");
|
||||
@@ -185,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.
@@ -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>
|
||||
+818
-697
File diff suppressed because it is too large
Load Diff
+59
-43
@@ -4,53 +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;
|
||||
bool frame_advance;
|
||||
uint32_t save_state_slot;
|
||||
} 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, const void *font, size_t font_size);
|
||||
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);
|
||||
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);
|
||||
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: ba3293573d...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.90.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::video::onscreen_message(&device.ui, "Cheats enabled");
|
||||
ui::video::onscreen_message("Cheats enabled", ui::video::MESSAGE_LENGTH_MESSAGE_SHORT);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
Submodule src/compat/sse2neon updated: 3cf69760cc...b26afbb44d
+34
-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![
|
||||
@@ -474,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
-1
@@ -147,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) {
|
||||
|
||||
+11
-7
@@ -1,5 +1,6 @@
|
||||
use crate::device;
|
||||
use crate::netplay;
|
||||
use crate::ui;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub const CART_MASK: usize = 0xFFFFFFF;
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ 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;
|
||||
@@ -405,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(
|
||||
@@ -429,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
-12
@@ -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,21 +190,30 @@ pub fn pak_switch_event(device: &mut device::Device) {
|
||||
write: device::controller::mempak::write,
|
||||
pak_type: new_pak_type,
|
||||
});
|
||||
ui::video::onscreen_message(&device.ui, "MemPak selected");
|
||||
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(&device.ui, "RumblePak selected");
|
||||
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(&device.ui, "TransferPak selected");
|
||||
ui::video::onscreen_message(
|
||||
"TransferPak selected",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
}
|
||||
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 = [
|
||||
|
||||
+1
-1
@@ -228,8 +228,8 @@ pub fn init(device: &mut device::Device) {
|
||||
93750000
|
||||
} else {
|
||||
ui::video::onscreen_message(
|
||||
&device.ui,
|
||||
"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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
use rand_chacha::rand_core::RngCore;
|
||||
use rand::Rng;
|
||||
|
||||
const PI_DRAM_ADDR_REG: u32 = 0;
|
||||
const PI_CART_ADDR_REG: u32 = 1;
|
||||
@@ -169,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,5 @@
|
||||
use crate::device;
|
||||
use crate::retroachievements;
|
||||
use crate::ui;
|
||||
use std::alloc::{Layout, alloc_zeroed};
|
||||
|
||||
@@ -151,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
|
||||
|
||||
@@ -844,7 +844,7 @@ pub fn vrcpl(device: &mut device::Device, opcode: u32) {
|
||||
let vte = vte(device, vt(opcode), ve(opcode) as usize);
|
||||
let input = if device.rsp.cpu.divdp {
|
||||
((device.rsp.cpu.divin as i32) << 16)
|
||||
| get_vpr16(device.rsp.cpu.vpr[vt(opcode) as usize], ve(opcode) as u8) as u16 as i32
|
||||
| get_vpr16(device.rsp.cpu.vpr[vt(opcode) as usize], ve(opcode) as u8) as i32
|
||||
} else {
|
||||
get_vpr16(device.rsp.cpu.vpr[vt(opcode) as usize], ve(opcode) as u8) as i16 as i32
|
||||
};
|
||||
@@ -901,7 +901,7 @@ pub fn vrsql(device: &mut device::Device, opcode: u32) {
|
||||
let vte = vte(device, vt(opcode), ve(opcode) as usize);
|
||||
let input = if device.rsp.cpu.divdp {
|
||||
((device.rsp.cpu.divin as i32) << 16)
|
||||
| get_vpr16(device.rsp.cpu.vpr[vt(opcode) as usize], ve(opcode) as u8) as u16 as i32
|
||||
| get_vpr16(device.rsp.cpu.vpr[vt(opcode) as usize], ve(opcode) as u8) as i32
|
||||
} else {
|
||||
get_vpr16(device.rsp.cpu.vpr[vt(opcode) as usize], ve(opcode) as u8) as i16 as i32
|
||||
};
|
||||
|
||||
+2
-3
@@ -1,7 +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;
|
||||
@@ -39,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
|
||||
}
|
||||
|
||||
|
||||
+63
-55
@@ -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);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -139,14 +127,17 @@ pub fn vertical_interrupt_event(device: &mut device::Device) {
|
||||
}
|
||||
|
||||
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 device.netplay.is_some() {
|
||||
netplay::send_sync_check(device);
|
||||
if device.vi.enable_speed_limiter == device.netplay.as_ref().unwrap().fast_forward {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,16 +149,17 @@ pub fn vertical_interrupt_event(device: &mut device::Device) {
|
||||
device.vi.vi_counter += 1;
|
||||
|
||||
if device.netplay.is_none() && paused {
|
||||
ui::video::pause_loop();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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;
|
||||
}
|
||||
*/
|
||||
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;
|
||||
@@ -189,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+213
-98
@@ -6,180 +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)"
|
||||
)]
|
||||
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(
|
||||
short,
|
||||
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(())
|
||||
}
|
||||
|
||||
+18
-11
@@ -85,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();
|
||||
}
|
||||
@@ -103,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();
|
||||
@@ -136,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);
|
||||
@@ -147,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::video::onscreen_message(&device.ui, "Lost connection to netplay server");
|
||||
ui::video::onscreen_message(
|
||||
"Lost connection to netplay server",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
input = Some(InputEvent {
|
||||
input: 0,
|
||||
plugin: 0,
|
||||
@@ -178,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] {
|
||||
@@ -192,13 +194,18 @@ fn process_incoming(netplay: &mut Netplay, ui: &ui::Ui) {
|
||||
}
|
||||
if current_status != netplay.status {
|
||||
if ((current_status & 0x1) ^ (netplay.status & 0x1)) != 0 {
|
||||
ui::video::onscreen_message(ui, "Netplay desync detected");
|
||||
ui::video::onscreen_message(ui, "Netplay desync detected"); // queue this message twice to ensure they see it
|
||||
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::video::onscreen_message(ui, &format!("Player {dis} disconnected"));
|
||||
ui::video::onscreen_message(
|
||||
&format!("Player {dis} disconnected"),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
}
|
||||
}
|
||||
netplay.status = current_status;
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
+44
-20
@@ -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,28 +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();
|
||||
tokio::spawn(async move {
|
||||
tokio::fs::write(save_path, compressed_file).await.unwrap();
|
||||
});
|
||||
ui::video::onscreen_message(
|
||||
&device.ui,
|
||||
&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();
|
||||
|
||||
@@ -163,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,
|
||||
@@ -198,22 +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());
|
||||
|
||||
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(
|
||||
&device.ui,
|
||||
&format!(
|
||||
"Savestate loaded from slot {}",
|
||||
device.ui.storage.save_state_slot
|
||||
),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
} else {
|
||||
ui::video::onscreen_message(
|
||||
&device.ui,
|
||||
&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,17 +1,24 @@
|
||||
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 {
|
||||
@@ -35,6 +42,10 @@ pub struct Storage {
|
||||
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 {
|
||||
@@ -42,6 +53,14 @@ pub struct Usb {
|
||||
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 {
|
||||
pub dirs: Dirs,
|
||||
pub config: config::Config,
|
||||
@@ -68,12 +87,15 @@ 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,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: [
|
||||
@@ -153,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,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -187,6 +210,10 @@ impl Ui {
|
||||
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,
|
||||
|
||||
+63
-5
@@ -53,6 +53,10 @@ pub fn lower_audio_volume(ui: &mut ui::Ui) {
|
||||
}
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.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) {
|
||||
@@ -63,6 +67,31 @@ pub fn raise_audio_volume(ui: &mut ui::Ui) {
|
||||
}
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.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,
|
||||
);
|
||||
}
|
||||
|
||||
fn adjust_audio_frequency(device: &device::Device, frequency: f32) {
|
||||
if !device.vi.enable_speed_limiter {
|
||||
return;
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play_audio(device: &device::Device, dram_addr: usize, length: u64) {
|
||||
@@ -85,17 +114,46 @@ 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 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 < acceptable_latency
|
||||
&& !unsafe {
|
||||
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,
|
||||
silence_buffer.as_ptr() as *const std::ffi::c_void,
|
||||
silence_buffer.len() as i32,
|
||||
)
|
||||
} {
|
||||
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 < 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();
|
||||
|
||||
@@ -39,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,
|
||||
}
|
||||
|
||||
@@ -48,6 +50,7 @@ pub struct Video {
|
||||
pub integer_scaling: bool,
|
||||
pub fullscreen: bool,
|
||||
pub widescreen: bool,
|
||||
pub vsync: bool,
|
||||
pub crt: bool,
|
||||
}
|
||||
|
||||
@@ -70,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 {
|
||||
@@ -140,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 {
|
||||
@@ -147,6 +153,7 @@ impl Config {
|
||||
integer_scaling: false,
|
||||
fullscreen: false,
|
||||
widescreen: false,
|
||||
vsync: true,
|
||||
crt: false,
|
||||
},
|
||||
emulation: Emulation {
|
||||
@@ -155,6 +162,7 @@ impl Config {
|
||||
usb: false,
|
||||
},
|
||||
rom_dir: std::path::PathBuf::new(),
|
||||
recent_roms: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+353
-268
@@ -1,7 +1,7 @@
|
||||
use crate::device;
|
||||
use crate::netplay;
|
||||
use crate::retroachievements;
|
||||
use crate::ui;
|
||||
use slint::Model;
|
||||
use slint::winit_030::WinitWindowAccessor;
|
||||
|
||||
slint::include_modules!();
|
||||
|
||||
@@ -19,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>) {
|
||||
@@ -64,23 +54,109 @@ fn check_latest_version(weak: slint::Weak<AppWindow>) {
|
||||
});
|
||||
}
|
||||
|
||||
fn local_game_window(app: &AppWindow, controller_paths: &[Option<String>], usb: ui::Usb) {
|
||||
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 mut usb_tx = None;
|
||||
if let Some(usb_tx_inner) = usb.usb_tx.as_ref() {
|
||||
usb_tx = Some(usb_tx_inner.clone());
|
||||
}
|
||||
let mut cart_rx = None;
|
||||
if let Some(cart_rx_inner) = usb.cart_rx.as_ref() {
|
||||
cart_rx = Some(cart_rx_inner.resubscribe());
|
||||
}
|
||||
let controller_paths = owned_controller_paths.clone();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
save_settings(&handle, &controller_paths);
|
||||
open_rom(&handle, ui::Usb { usb_tx, cart_rx })
|
||||
open_rom(&handle)
|
||||
})
|
||||
.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();
|
||||
});
|
||||
@@ -89,6 +165,7 @@ fn local_game_window(app: &AppWindow, controller_paths: &[Option<String>], usb:
|
||||
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> {
|
||||
@@ -103,6 +180,7 @@ 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);
|
||||
@@ -139,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);
|
||||
@@ -167,47 +245,65 @@ 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 || {
|
||||
@@ -220,25 +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,
|
||||
deadzone,
|
||||
);
|
||||
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>]) {
|
||||
@@ -246,6 +404,7 @@ 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();
|
||||
@@ -259,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
|
||||
@@ -269,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,18 +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);
|
||||
let mut usb = ui::Usb {
|
||||
usb_tx: None,
|
||||
cart_rx: None,
|
||||
};
|
||||
let mut shutdown_tx = None;
|
||||
ui::retroachievements::ra_window(&app);
|
||||
let mut controller_paths;
|
||||
{
|
||||
let game_ui = ui::Ui::new();
|
||||
@@ -314,138 +475,116 @@ 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);
|
||||
|
||||
if game_ui.config.emulation.usb {
|
||||
(shutdown_tx, usb) = ui::usb::init(app.as_weak());
|
||||
}
|
||||
local_game_window(&app, &game_ui.config, &controller_paths);
|
||||
}
|
||||
local_game_window(&app, &controller_paths, usb);
|
||||
ui::netplay::netplay_window(&app, &controller_paths);
|
||||
ui::cheats::cheats_window(&app);
|
||||
app.run().unwrap();
|
||||
if let Some(shutdown_tx) = &shutdown_tx {
|
||||
ui::usb::close(shutdown_tx);
|
||||
}
|
||||
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>>>,
|
||||
pub fn run_rom(
|
||||
file_path: std::path::PathBuf,
|
||||
game_settings: ui::GameSettings,
|
||||
netplay: Option<NetplayDevice>,
|
||||
ra_settings: RASettings,
|
||||
weak: slint::Weak<AppWindow>,
|
||||
) {
|
||||
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();
|
||||
weak.upgrade_in_event_loop(move |handle| handle.set_game_running(true))
|
||||
.unwrap();
|
||||
|
||||
vru_dialog.on_vru_button_clicked(move |chosen_word| {
|
||||
notifier.try_send(chosen_word.to_string()).unwrap();
|
||||
vru_dialog_weak.unwrap().window().hide().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();
|
||||
|
||||
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;
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn run_rom(
|
||||
gb_paths: GbPaths,
|
||||
file_path: std::path::PathBuf,
|
||||
mut game_settings: GameSettings,
|
||||
vru_channel: VruChannel,
|
||||
netplay: Option<NetplayDevice>,
|
||||
usb: ui::Usb,
|
||||
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.ui.usb.usb_tx = usb.usb_tx;
|
||||
device.ui.usb.cart_rx = usb.cart_rx;
|
||||
|
||||
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);
|
||||
})
|
||||
.unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn open_rom(app: &AppWindow, usb: ui::Usb) {
|
||||
fn open_rom(app: &AppWindow) {
|
||||
let rom_dir = app.get_rom_dir();
|
||||
let select_rom = if !rom_dir.is_empty()
|
||||
&& let Ok(exists) = std::fs::exists(&rom_dir)
|
||||
@@ -458,86 +597,32 @@ fn open_rom(app: &AppWindow, usb: ui::Usb) {
|
||||
.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", "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", "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,
|
||||
usb,
|
||||
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,11 +37,26 @@ 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;
|
||||
@@ -47,12 +65,16 @@ export component AppWindow inherits Window {
|
||||
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;
|
||||
@@ -70,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")
|
||||
];
|
||||
@@ -85,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 => {
|
||||
@@ -116,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,6 +119,7 @@ export component ControllerConfig inherits Page {
|
||||
current-index: ControllerData.selected_controller[player];
|
||||
selected => {
|
||||
ControllerData.selected_controller[player] = self.current-index;
|
||||
ControllerData.controller_changed[player] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StandardButton, HorizontalBox, VerticalBox, Button, LineEdit, CheckBox, Slider } from "std-widgets.slint";
|
||||
import { StandardButton, HorizontalBox, VerticalBox, Button, LineEdit, Switch, Slider } from "std-widgets.slint";
|
||||
|
||||
export global InputProfileData {
|
||||
in-out property <bool> dinput;
|
||||
@@ -47,7 +47,7 @@ export component InputProfileDialog inherits Window {
|
||||
|
||||
HorizontalBox {
|
||||
alignment: center;
|
||||
CheckBox {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ export component NetplayJoin inherits Window {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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,13 +1,14 @@
|
||||
// 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;
|
||||
@@ -39,7 +40,7 @@ export component Settings inherits Page {
|
||||
|
||||
HorizontalBox {
|
||||
alignment: start;
|
||||
CheckBox {
|
||||
Switch {
|
||||
text: @tr("Integer Scaling");
|
||||
checked: SettingsData.integer_scaling;
|
||||
toggled => {
|
||||
@@ -50,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;
|
||||
@@ -61,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;
|
||||
@@ -72,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 => {
|
||||
@@ -87,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 => {
|
||||
@@ -98,7 +110,7 @@ export component Settings inherits Page {
|
||||
|
||||
HorizontalBox {
|
||||
alignment: start;
|
||||
CheckBox {
|
||||
Switch {
|
||||
text: @tr("Disable Expansion Pak");
|
||||
checked: SettingsData.disable_expansion_pak;
|
||||
toggled => {
|
||||
@@ -109,8 +121,8 @@ export component Settings inherits Page {
|
||||
|
||||
HorizontalBox {
|
||||
alignment: start;
|
||||
CheckBox {
|
||||
text: @tr("Emulate UNFLoader (reload emulator after changing)");
|
||||
Switch {
|
||||
text: @tr("Emulate UNFLoader");
|
||||
checked: SettingsData.emulate_usb;
|
||||
toggled => {
|
||||
SettingsData.emulate_usb = self.checked;
|
||||
|
||||
+69
-36
@@ -42,7 +42,7 @@ pub struct InputData {
|
||||
}
|
||||
|
||||
fn bound_axis(x: &mut f64, y: &mut f64) {
|
||||
let radius = f64::sqrt(70.0 * 70.0 + 70.0 * 70.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));
|
||||
@@ -73,6 +73,10 @@ fn apply_deadzone(x: &mut f64, y: &mut f64, deadzone: i32) {
|
||||
*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,
|
||||
@@ -84,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 {
|
||||
@@ -92,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 {
|
||||
@@ -100,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 {
|
||||
@@ -108,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)
|
||||
@@ -143,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"),
|
||||
}
|
||||
}
|
||||
@@ -161,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 {
|
||||
@@ -172,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 {
|
||||
@@ -183,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 {
|
||||
@@ -194,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)
|
||||
@@ -309,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() {
|
||||
@@ -318,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,
|
||||
)
|
||||
};
|
||||
}
|
||||
@@ -369,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![];
|
||||
|
||||
@@ -385,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;
|
||||
@@ -476,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) }
|
||||
@@ -486,6 +504,13 @@ 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, deadzone: i32) {
|
||||
@@ -523,9 +548,9 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool, d
|
||||
if !unsafe {
|
||||
sdl3_sys::render::SDL_CreateWindowAndRenderer(
|
||||
title.as_ptr(),
|
||||
640,
|
||||
852,
|
||||
480,
|
||||
0,
|
||||
sdl3_sys::video::SDL_WindowFlags(0),
|
||||
&mut window,
|
||||
&mut renderer,
|
||||
)
|
||||
@@ -594,7 +619,7 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool, d
|
||||
|
||||
let text_engine = unsafe { sdl3_ttf_sys::ttf::TTF_CreateRendererTextEngine(renderer) };
|
||||
let font = unsafe {
|
||||
let font_bytes = include_bytes!("../../data/Roboto-Regular.ttf");
|
||||
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,
|
||||
@@ -609,20 +634,26 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool, d
|
||||
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,
|
||||
text_engine,
|
||||
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 {
|
||||
@@ -702,16 +733,18 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool, d
|
||||
}
|
||||
}
|
||||
}
|
||||
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_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);
|
||||
}
|
||||
close_input_profile_window(
|
||||
open_joysticks,
|
||||
open_controllers,
|
||||
renderer,
|
||||
text_engine,
|
||||
font,
|
||||
window,
|
||||
);
|
||||
|
||||
let new_profile = ui::config::InputProfile {
|
||||
keys: new_keys,
|
||||
@@ -936,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() {
|
||||
@@ -963,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 =
|
||||
@@ -982,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 =
|
||||
|
||||
+124
-98
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -190,7 +188,7 @@ fn select_rom<T: ComponentHandle + NetplayPages + 'static>(
|
||||
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()
|
||||
{
|
||||
@@ -222,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();
|
||||
}
|
||||
@@ -249,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>,
|
||||
) {
|
||||
@@ -296,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();
|
||||
@@ -305,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
|
||||
@@ -336,6 +336,7 @@ pub fn setup_create_window(
|
||||
game_cheats.to_string(),
|
||||
password.to_string(),
|
||||
game_settings,
|
||||
ra_settings,
|
||||
weak_app,
|
||||
weak.clone(),
|
||||
);
|
||||
@@ -365,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(),
|
||||
);
|
||||
@@ -448,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();
|
||||
@@ -514,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
|
||||
@@ -571,26 +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()),
|
||||
)));
|
||||
|
||||
let room_ports_vec = slint::VecModel::from(room_ports.to_vec());
|
||||
let room_ports_model: std::rc::Rc<slint::VecModel<i32>> =
|
||||
std::rc::Rc::new(room_ports_vec);
|
||||
handle.set_room_ports(slint::ModelRc::from(room_ports_model));
|
||||
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);
|
||||
@@ -612,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>,
|
||||
) {
|
||||
@@ -634,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,
|
||||
@@ -642,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),
|
||||
@@ -695,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,
|
||||
);
|
||||
@@ -740,7 +748,7 @@ fn join_session(
|
||||
game_hash: String,
|
||||
password: String,
|
||||
room_port: i32,
|
||||
fullscreen: bool,
|
||||
ra_settings: RASettings,
|
||||
weak_app: slint::Weak<AppWindow>,
|
||||
weak: slint::Weak<NetplayJoin>,
|
||||
) {
|
||||
@@ -751,7 +759,7 @@ fn join_session(
|
||||
let join_room = NetplayMessage {
|
||||
message_type: "request_join_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,
|
||||
@@ -759,7 +767,13 @@ fn join_session(
|
||||
rooms: None,
|
||||
player_names: None,
|
||||
auth_time: Some(now_utc),
|
||||
auth: Some(format!("{:x}", hasher.finalize())),
|
||||
auth: Some(
|
||||
hasher
|
||||
.finalize()
|
||||
.iter()
|
||||
.map(|b| format!("{:02x}", b))
|
||||
.collect(),
|
||||
),
|
||||
room: Some(NetplayRoom {
|
||||
room_name: None,
|
||||
password: Some(password),
|
||||
@@ -812,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,
|
||||
);
|
||||
@@ -858,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>,
|
||||
) {
|
||||
@@ -872,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();
|
||||
@@ -1029,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();
|
||||
@@ -1070,37 +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,
|
||||
}),
|
||||
ui::Usb {
|
||||
usb_tx: None,
|
||||
cart_rx: None,
|
||||
},
|
||||
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);
|
||||
}
|
||||
@@ -1127,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): (
|
||||
@@ -1176,7 +1191,7 @@ pub fn setup_join_window(
|
||||
game_hash.to_string(),
|
||||
password.to_string(),
|
||||
room_port,
|
||||
fullscreen,
|
||||
ra_settings.clone(),
|
||||
weak_app.clone(),
|
||||
weak.clone(),
|
||||
);
|
||||
@@ -1199,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,
|
||||
@@ -1224,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
-50
@@ -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,49 +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.save_state_slot.to_string().as_str(),
|
||||
);
|
||||
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>) {
|
||||
@@ -354,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
|
||||
@@ -374,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;
|
||||
}
|
||||
@@ -389,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();
|
||||
});
|
||||
}
|
||||
|
||||
+3
-49
@@ -26,56 +26,12 @@ fn respond_to_handshake(usb_tx: &tokio::sync::broadcast::Sender<UsbData>, data:
|
||||
}
|
||||
}
|
||||
|
||||
fn upload_rom(
|
||||
weak: slint::Weak<ui::gui::AppWindow>,
|
||||
rom: Vec<u8>,
|
||||
usb_tx: &tokio::sync::broadcast::Sender<UsbData>,
|
||||
cart_rx: &tokio::sync::broadcast::Receiver<UsbData>,
|
||||
) {
|
||||
let weak_clone = weak.clone();
|
||||
let usb_tx = usb_tx.clone();
|
||||
let cart_rx = cart_rx.resubscribe();
|
||||
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
let dir = std::env::temp_dir();
|
||||
let rom_path = dir.join("rom_upload.bin");
|
||||
std::fs::write(rom_path.clone(), rom).unwrap();
|
||||
|
||||
ui::gui::run_rom(
|
||||
ui::gui::GbPaths {
|
||||
rom: [None, None, None, None],
|
||||
ram: [None, None, None, None],
|
||||
},
|
||||
rom_path,
|
||||
ui::gui::GameSettings {
|
||||
fullscreen: handle.get_fullscreen(),
|
||||
overclock: handle.get_overclock_n64_cpu(),
|
||||
disable_expansion_pak: handle.get_disable_expansion_pak(),
|
||||
cheats: std::collections::HashMap::new(), // will be filled in later
|
||||
},
|
||||
ui::gui::VruChannel {
|
||||
vru_window_notifier: None,
|
||||
vru_word_receiver: None,
|
||||
},
|
||||
None,
|
||||
ui::Usb {
|
||||
usb_tx: Some(usb_tx),
|
||||
cart_rx: Some(cart_rx),
|
||||
},
|
||||
weak_clone,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
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>,
|
||||
cart_rx: tokio::sync::broadcast::Receiver<UsbData>,
|
||||
weak: slint::Weak<ui::gui::AppWindow>,
|
||||
) {
|
||||
let (mut incoming, mut outgoing) = conn.into_split();
|
||||
|
||||
@@ -150,7 +106,7 @@ async fn handle_connection(
|
||||
if usb_data.data_type == DATATYPE_TCPTEST {
|
||||
respond_to_handshake(&usb_tx,usb_data.data);
|
||||
} else if usb_data.data_type == DATATYPE_ROMUPLOAD {
|
||||
upload_rom(weak.clone(), usb_data.data,&usb_tx,&cart_rx);
|
||||
panic!("ROM upload not supported");
|
||||
} else {
|
||||
cart_tx.send(usb_data).unwrap();
|
||||
}
|
||||
@@ -171,9 +127,7 @@ async fn handle_connection(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(
|
||||
weak: slint::Weak<ui::gui::AppWindow>,
|
||||
) -> (Option<tokio::sync::watch::Sender<()>>, ui::Usb) {
|
||||
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>,
|
||||
@@ -195,7 +149,7 @@ pub fn init(
|
||||
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(),cart_rx.resubscribe(),weak.clone()).await;
|
||||
handle_connection(c,shutdown_rx.clone(),usb_rx.resubscribe(),usb_tx.clone(),cart_tx.clone()).await;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
+106
-55
@@ -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 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,37 +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 {
|
||||
let font_bytes = include_bytes!("../../data/Roboto-Regular.ttf");
|
||||
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) {
|
||||
@@ -109,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);
|
||||
@@ -133,12 +172,13 @@ pub fn load_state(device: &mut device::Device, rdp_state: *const u8) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause_loop() {
|
||||
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(1.0 / 60.0));
|
||||
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;
|
||||
@@ -155,6 +195,18 @@ pub fn check_callback(device: &mut device::Device) -> (bool, 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;
|
||||
@@ -162,9 +214,9 @@ pub fn check_callback(device: &mut device::Device) -> (bool, bool) {
|
||||
}
|
||||
|
||||
if device.ui.storage.save_state_slot != callback.save_state_slot {
|
||||
ui::video::onscreen_message(
|
||||
&device.ui,
|
||||
&format!("Switching savestate slot to {}", 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
|
||||
@@ -172,7 +224,7 @@ pub fn check_callback(device: &mut device::Device) -> (bool, bool) {
|
||||
.storage
|
||||
.paths
|
||||
.savestate_file_path
|
||||
.set_extension("state".to_owned() + callback.save_state_slot.to_string().as_str());
|
||||
.set_extension(format!("state{}", callback.save_state_slot));
|
||||
}
|
||||
if callback.lower_volume {
|
||||
ui::audio::lower_audio_volume(&mut device.ui);
|
||||
@@ -196,8 +248,11 @@ pub fn check_framebuffers(address: u32, length: u32) {
|
||||
unsafe { rdp_check_framebuffers(address, length) }
|
||||
}
|
||||
|
||||
pub fn onscreen_message(_ui: &ui::Ui, message: &str) {
|
||||
unsafe { rdp_onscreen_message(std::ffi::CString::new(message).unwrap().as_ptr()) };
|
||||
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(
|
||||
@@ -210,12 +265,8 @@ pub fn draw_text(
|
||||
let (mut w, mut h) = (0, 0);
|
||||
sdl3_sys::render::SDL_GetRenderOutputSize(renderer, &mut w, &mut h);
|
||||
|
||||
let ttf_text = sdl3_ttf_sys::ttf::TTF_CreateText(
|
||||
text_engine,
|
||||
font,
|
||||
std::ffi::CString::new(text).unwrap().as_ptr(),
|
||||
0,
|
||||
);
|
||||
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);
|
||||
|
||||
sdl3_sys::everything::SDL_RenderClear(renderer);
|
||||
sdl3_ttf_sys::ttf::TTF_DrawRendererText(ttf_text, 20.0, h as f32 / 2.0);
|
||||
|
||||
+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