Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a78f47ecfe | |||
| 1df705c4f9 | |||
| f7ed2c5273 | |||
| 8bd4c936f8 | |||
| 34e55d9cb5 | |||
| 8a8596a66b | |||
| 40ab4ceb2b | |||
| a8dbbba6e3 | |||
| 5b0cc0130e | |||
| bb32a0a8ba | |||
| e23894c2e6 | |||
| e0efa80f44 | |||
| cb734f8df1 | |||
| 3197d415e0 | |||
| 9dfc42c723 | |||
| 14b973ec5f | |||
| 81a3cea9c7 | |||
| dc6138076f | |||
| a9f7285c0f | |||
| 7106a461b5 | |||
| 69e3ebcc26 | |||
| f80f0f5f02 | |||
| a447bc8104 | |||
| 32f54f157b | |||
| 9c9f1a3000 | |||
| 9d7b13d4c9 | |||
| 60cbf59ddf | |||
| 6f00e630e0 | |||
| 7599f71c17 | |||
| 411cb0efa7 | |||
| 00ec554783 | |||
| d25d3a460a | |||
| 8eec90c35c | |||
| a596e01ef2 | |||
| 43ebc37f87 | |||
| 6223f10fae | |||
| c16e927288 | |||
| 6ef33204e8 | |||
| 3f9d62248a | |||
| 18fbd8e56c | |||
| e5c74d05aa | |||
| 82c22dc18e | |||
| 66a74b5693 | |||
| 367b8bdb5c | |||
| b865c88d99 | |||
| 53b39010a0 | |||
| 7ad8bed7df | |||
| 01ea4c422a | |||
| 7eb090a622 | |||
| 48956dc016 | |||
| 2eb8a0dc1d | |||
| c53ffebece | |||
| 06d0aa53bc | |||
| 28b1424357 | |||
| e852439b6a | |||
| 5850c514f2 | |||
| 11a49fd687 | |||
| be9ba1366e | |||
| dbe67f1034 | |||
| 7eecfa3275 | |||
| e1b4039a52 | |||
| 9f2cfb5a9f | |||
| 41e66825d0 | |||
| a78393ca91 | |||
| 8dde32cb21 | |||
| 1538513fb7 | |||
| 30c8aef24e | |||
| 8daa65d93e | |||
| df5fcae69b | |||
| e3b63908de | |||
| 8c53de9cc7 | |||
| 78cb758a26 | |||
| 8fea9ef212 | |||
| 38fdc24a49 | |||
| 069c64ab08 | |||
| 2005fb1421 | |||
| 04b15fee2f | |||
| eda3dda613 | |||
| 03be98b571 | |||
| 93bf103f03 | |||
| c8f485cd93 | |||
| 3a301c517d | |||
| d17c577ccd | |||
| 8c331c25eb | |||
| 8341ef5731 | |||
| 40a29d927f | |||
| 9846c2243c | |||
| c1dc4fe589 | |||
| 06052b729b | |||
| d7c912b53e | |||
| b792ebbd45 | |||
| e7071047e3 | |||
| 6496288c4e | |||
| 1ab8987f2d | |||
| 034b39bec5 | |||
| 5368ae3d37 | |||
| f963742009 | |||
| a8ae9dbc6a | |||
| b55a031971 | |||
| 27ffedbdee | |||
| 493f08afaf | |||
| 0659da24fa | |||
| 5596dad111 | |||
| a5b7561c51 | |||
| fe23e4bad9 | |||
| 5b3b8265e5 | |||
| a7cbf866e8 | |||
| 7fae21d7ac | |||
| 55bf63210a | |||
| 2b72c24dbe | |||
| 455365a71d | |||
| 1f6b575440 | |||
| 86304018dd | |||
| c44ab9602c | |||
| 13be114823 | |||
| f793eb06be | |||
| 61532f7b45 | |||
| a69a74bf37 | |||
| 6d4d5468f9 | |||
| 7015d44da7 | |||
| 172867527b | |||
| 323e8cf1f0 | |||
| 172d10d7cb | |||
| 945d456f9c | |||
| a4f635265b | |||
| f45f951af8 | |||
| 883852a828 | |||
| 90052257bb | |||
| 1184d3d77a | |||
| 12280bb35b | |||
| 17f3f2dc63 | |||
| 3d6ca17a9a | |||
| 01863a70a8 | |||
| 801cc1bc74 | |||
| 9ed8d835c0 | |||
| b754891d45 | |||
| 9cc6d48493 | |||
| b865538d6e | |||
| abacaddf8a | |||
| b86c5daa2c | |||
| df8d7d9ff0 | |||
| 8c7d3d4893 | |||
| b88f041bd8 | |||
| e860b96bd8 | |||
| b2ef544513 | |||
| ca00ee1a42 | |||
| 511d8461c2 | |||
| fe42efadfa | |||
| c1e73320b1 | |||
| 47dd6d61b0 | |||
| 212557610b |
@@ -11,7 +11,10 @@ rustflags = ["-C", "link-arg=/STACK:8388608"]
|
||||
rustflags = ["-C", "target-cpu=x86-64-v3"]
|
||||
|
||||
[target.'cfg(all(target_arch = "aarch64", target_os = "macos"))']
|
||||
rustflags = ["-C", "target-cpu=apple-m1"]
|
||||
rustflags = [
|
||||
"-C", "target-cpu=apple-m1",
|
||||
"-C", "link-arg=-headerpad_max_install_names"
|
||||
]
|
||||
|
||||
[target.'cfg(all(target_arch = "aarch64", not(target_os = "macos")))']
|
||||
rustflags = ["-C", "target-cpu=cortex-a76"]
|
||||
|
||||
@@ -4,15 +4,19 @@ permissions:
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
merge_group:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RA_HARDCORE: ${{ github.ref_type == 'tag' && 'true' || 'false' }}
|
||||
NETPLAY_ID: ${{ github.ref_type == 'tag' && secrets.NETPLAY_ID || 'dev' }}
|
||||
BUILD_PROFILE: ${{ github.ref_type == 'tag' && 'release' || 'dev' }}
|
||||
BUILD_DIR: ${{ github.ref_type == 'tag' && 'release' || 'debug' }}
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
@@ -40,7 +44,8 @@ jobs:
|
||||
rustflags: ""
|
||||
cache: ${{ github.ref_type != 'tag' }}
|
||||
- name: Set LLVM_VERSION
|
||||
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
|
||||
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
|
||||
@@ -53,37 +58,27 @@ jobs:
|
||||
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
|
||||
libdecor-0-dev liburing-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-${{
|
||||
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 }}
|
||||
cargo build --profile ${{ env.BUILD_PROFILE }} -v --target=${{ matrix.target }}
|
||||
mkdir output
|
||||
cp target/${{ matrix.target }}/release/gopher64 output/gopher64-${{ matrix.arch }}
|
||||
env:
|
||||
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
|
||||
cp target/${{ matrix.target }}/${{ env.BUILD_DIR }}/gopher64 output/gopher64-${{ matrix.arch }}
|
||||
- name: Upload file
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gopher64-${{ matrix.arch }}
|
||||
path: output/gopher64-${{ matrix.arch }}
|
||||
archive: false
|
||||
- name: Setup butler
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
uses: remarkablegames/setup-butler@v2
|
||||
- name: Upload to itch.io
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
run: |
|
||||
butler push output/gopher64-${{ matrix.arch }} loganmc10/gopher64:${{ matrix.arch }} --userversion ${GITHUB_REF_NAME#v}
|
||||
env:
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }}
|
||||
|
||||
build-windows:
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -117,18 +112,18 @@ jobs:
|
||||
uses: KyleMayes/install-llvm-action@v2
|
||||
with:
|
||||
version: ${{ env.LLVM_VERSION }}
|
||||
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
|
||||
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 }}
|
||||
cargo build --profile ${{ env.BUILD_PROFILE }} -v --target=${{ matrix.target }}
|
||||
mkdir output
|
||||
cp target/${{ matrix.target }}/release/gopher64.exe output/gopher64-${{ matrix.arch }}.exe
|
||||
cp target/${{ matrix.target }}/${{ env.BUILD_DIR }}/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@v7
|
||||
@@ -157,15 +152,6 @@ jobs:
|
||||
path: output-signed/gopher64-${{ matrix.arch }}.exe
|
||||
archive: false
|
||||
overwrite: true
|
||||
- name: Setup butler
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
uses: remarkablegames/setup-butler@v2
|
||||
- name: Upload to itch.io
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
run: |
|
||||
butler push output-signed/gopher64-${{ matrix.arch }}.exe loganmc10/gopher64:${{ matrix.arch }} --userversion ${GITHUB_REF_NAME#v}
|
||||
env:
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }}
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-15
|
||||
@@ -187,64 +173,129 @@ jobs:
|
||||
rustflags: ""
|
||||
cache: ${{ github.ref_type != 'tag' }}
|
||||
- name: Set LLVM_VERSION
|
||||
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
|
||||
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-${{
|
||||
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 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
|
||||
cargo build --profile ${{ env.BUILD_PROFILE }} --no-default-features -v --target=${{ matrix.target }}
|
||||
mv target/${{ matrix.target }}/${{ env.BUILD_DIR }}/gopher64 target/${{ matrix.target }}/${{ env.BUILD_DIR }}/gopher64-cli
|
||||
cargo bundle --profile ${{ env.BUILD_PROFILE }} --target=${{ matrix.target }} --format osx
|
||||
cp target/${{ matrix.target }}/${{ env.BUILD_DIR }}/gopher64-cli target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app/Contents/MacOS/gopher64-cli
|
||||
env:
|
||||
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
|
||||
MACOSX_DEPLOYMENT_TARGET: "15.0"
|
||||
- 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
|
||||
codesign -f --entitlements data/macos/entitlements_dev.plist --deep --timestamp --options runtime -s - target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app
|
||||
- name: Import Codesign Certs
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
uses: apple-actions/import-codesign-certs@v6
|
||||
uses: apple-actions/import-codesign-certs@v7
|
||||
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
|
||||
codesign -f --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/${{ env.BUILD_DIR }}/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 }}/${{ env.BUILD_DIR }}/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 }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app
|
||||
zip -r gopher64-${{ matrix.arch }}.zip target/${{ matrix.target }}/${{ env.BUILD_DIR }}/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
|
||||
xcrun stapler staple target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app
|
||||
- name: Upload file
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gopher64-${{ matrix.arch }}
|
||||
path: target/${{ matrix.target }}/release/bundle/osx
|
||||
- name: Setup butler
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
uses: remarkablegames/setup-butler@v2
|
||||
- name: Upload to itch.io
|
||||
path: target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx
|
||||
|
||||
build-android:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-24.04
|
||||
platform: 33
|
||||
ndk: 27.3.13750724
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
target: x86_64-linux-android,aarch64-linux-android
|
||||
rustflags: ""
|
||||
cache: ${{ github.ref_type != 'tag' }}
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
packages: "platforms;android-${{ matrix.platform }} ndk;${{ matrix.ndk }}"
|
||||
- name: Decode keystore and create jks
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
working-directory: android-project
|
||||
run: |
|
||||
butler push target/${{ matrix.target }}/release/bundle/osx/Gopher64.app loganmc10/gopher64:${{ matrix.arch }} --userversion ${GITHUB_REF_NAME#v}
|
||||
echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 --decode > app/keystore.jks
|
||||
|
||||
cat <<EOF > keystore.properties
|
||||
storePassword=${{ secrets.ANDROID_STORE_PASSWORD }}
|
||||
keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
keyAlias=gopher64
|
||||
storeFile=keystore.jks
|
||||
EOF
|
||||
- name: Build
|
||||
working-directory: android-project
|
||||
run: |
|
||||
cargo install cargo-ndk
|
||||
./gradlew assemble${{ env.BUILD_PROFILE == 'release' && 'Release' || 'Debug' }}
|
||||
mkdir output
|
||||
cp app/build/outputs/apk/${{ env.BUILD_DIR }}/app-${{ env.BUILD_DIR }}.apk output/gopher64-android.apk
|
||||
- name: Upload file
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gopher64-android
|
||||
path: android-project/output/gopher64-android.apk
|
||||
archive: false
|
||||
|
||||
collect-artifacts:
|
||||
name: Collect release artifacts
|
||||
needs: [build-linux, build-windows, build-macos, build-android]
|
||||
if: github.ref_type == 'tag'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download all workflow artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
skip-decompress: true
|
||||
merge-multiple: true
|
||||
path: release-artifacts
|
||||
- name: Add files to release
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
draft: true
|
||||
files: release-artifacts/*
|
||||
- name: Setup butler
|
||||
uses: remarkablegames/setup-butler@v3
|
||||
- name: Upload to itch.io
|
||||
run: |
|
||||
butler push release-artifacts/gopher64-linux-x86_64 loganmc10/gopher64:linux-x86_64 --userversion ${GITHUB_REF_NAME#v}
|
||||
butler push release-artifacts/gopher64-linux-aarch64 loganmc10/gopher64:linux-aarch64 --userversion ${GITHUB_REF_NAME#v}
|
||||
butler push release-artifacts/gopher64-windows-x86_64.exe loganmc10/gopher64:windows-x86_64 --userversion ${GITHUB_REF_NAME#v}
|
||||
butler push release-artifacts/gopher64-windows-aarch64.exe loganmc10/gopher64:windows-aarch64 --userversion ${GITHUB_REF_NAME#v}
|
||||
butler push release-artifacts/gopher64-macos-aarch64.zip loganmc10/gopher64:macos-aarch64 --userversion ${GITHUB_REF_NAME#v}
|
||||
butler push release-artifacts/gopher64-android.apk loganmc10/gopher64:android --userversion ${GITHUB_REF_NAME#v}
|
||||
env:
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }}
|
||||
|
||||
@@ -2,9 +2,10 @@ name: Rust-lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -19,11 +20,14 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
rustflags: ""
|
||||
components: clippy, rustfmt
|
||||
- name: Set LLVM_VERSION
|
||||
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
|
||||
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
|
||||
@@ -36,17 +40,23 @@ jobs:
|
||||
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
|
||||
libdecor-0-dev liburing-dev libfontconfig-dev gettext
|
||||
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-${{
|
||||
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 install slint-tr-extractor
|
||||
cargo clippy -- -Dwarnings
|
||||
cargo clippy --no-default-features -- -Dwarnings
|
||||
find -name \*.slint | sort | xargs slint-tr-extractor -o /tmp/translations.pot
|
||||
msgcmp --use-untranslated /tmp/translations.pot data/translations/gopher64.pot
|
||||
msgcmp --use-untranslated data/translations/gopher64.pot /tmp/translations.pot
|
||||
find data/translations -name "*.po" | xargs -I {} msgcmp {} data/translations/gopher64.pot
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
[package]
|
||||
name = "gopher64"
|
||||
version = "1.1.19"
|
||||
version = "1.1.22"
|
||||
edition = "2024"
|
||||
rust-version = "1.95.0"
|
||||
rust-version = "1.96.0"
|
||||
|
||||
[features]
|
||||
default = ["gui"]
|
||||
gui = ["dep:rfd", "dep:slint", "dep:slint-build"]
|
||||
gui = ["dep:rfd", "dep:slint", "dep:open", "dep:slint-build"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
dirs = "6.0"
|
||||
zip = "8.0"
|
||||
sevenz-rust2 = "0.20"
|
||||
sevenz-rust2 = "0.21"
|
||||
chrono = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde-big-array = "0.5"
|
||||
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.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"
|
||||
@@ -35,11 +32,29 @@ fatfs = "0.3"
|
||||
semver = "1.0"
|
||||
postcard = { version = "1.1", features = ["use-std"] }
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
reqwest = { version = "0.13", features = ["json", "query"] }
|
||||
reqwest = { version = "0.13", default-features = false, features = ["native-tls-vendored", "charset", "http2", "system-proxy", "json", "query"] }
|
||||
discord-rich-presence = "1.1"
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "unstable-winit-030", "backend-winit", "renderer-femtovg-wgpu", "accessibility"], optional = true }
|
||||
rfd = {version = "0.17", optional = true }
|
||||
dirs = "6.0"
|
||||
open = { version = "5.3", optional = true }
|
||||
sdl3-sys = { version = "0.6", features = ["build-from-source-static"] }
|
||||
sdl3-ttf-sys = { version = "0.6", features = ["build-static-vendored", "no-sdlttf-harfbuzz", "no-sdlttf-plutosvg"] }
|
||||
sdl3-image-sys = { version = "0.6", features = ["build-static-vendored"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "backend-android-activity-06", "accessibility"], optional = true }
|
||||
jni = "0.22"
|
||||
sdl3-sys = { version = "0.6", features = ["build-from-source"] }
|
||||
sdl3-ttf-sys = { version = "0.6", features = ["build-from-source", "no-sdlttf-harfbuzz", "no-sdlttf-plutosvg"] }
|
||||
sdl3-image-sys = { version = "0.6", features = ["build-from-source"] }
|
||||
|
||||
[build-dependencies]
|
||||
winresource = "0.1"
|
||||
bindgen = "0.72"
|
||||
sdl3-src = "3.4"
|
||||
slint-build = { version = "1.12", optional = true }
|
||||
cc = { version = "1.2", features = ["parallel"] }
|
||||
|
||||
@@ -59,5 +74,5 @@ 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_frameworks = ["/opt/homebrew/opt/molten-vk/lib/libMoltenVK.dylib"]
|
||||
osx_minimum_system_version = "15.0"
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
jniLibs/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
keystore.properties
|
||||
|
||||
# Log/OS Files
|
||||
*.log
|
||||
|
||||
# Android Studio generated files and folders
|
||||
captures/
|
||||
.externalNativeBuild/
|
||||
.cxx/
|
||||
*.aab
|
||||
*.apk
|
||||
output-metadata.json
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/
|
||||
misc.xml
|
||||
deploymentTargetDropDown.xml
|
||||
render.experimental.xml
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
app/src/main/java/org/libsdl/app/
|
||||
@@ -0,0 +1,4 @@
|
||||
set(ANDROID_PLATFORM $ENV{ANDROID_PLATFORM})
|
||||
set(ANDROID_ABI $ENV{ANDROID_ABI})
|
||||
set(ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES ON)
|
||||
include("$ENV{ANDROID_NDK_HOME}/build/cmake/android.toolchain.cmake")
|
||||
@@ -0,0 +1,160 @@
|
||||
import groovy.json.JsonSlurper
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
val keystoreProperties = Properties()
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.github.gopher64.gopher64"
|
||||
compileSdk {
|
||||
version = release(36) {
|
||||
minorApiLevel = 1
|
||||
}
|
||||
}
|
||||
|
||||
ndkVersion = "27.3.13750724"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "io.github.gopher64.gopher64"
|
||||
minSdk = 33
|
||||
targetSdk = 36
|
||||
versionCode = semverToVersionCode(cargoPackageVersion())
|
||||
versionName = cargoPackageVersion()
|
||||
ndk {
|
||||
abiFilters.addAll(listOf("arm64-v8a", "x86_64"))
|
||||
}
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["keyPassword"] as String
|
||||
storeFile = file(keystoreProperties["storeFile"] as String)
|
||||
storePassword = keystoreProperties["storePassword"] as String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
signingConfig = signingConfigs["release"]
|
||||
}
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
excludes.add("lib/**/libsevenz_rust2*.so")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun cargoPackageVersion(packageName: String = "gopher64"): String {
|
||||
val repoRoot = rootDir.parentFile
|
||||
val jsonText = providers.exec {
|
||||
workingDir(repoRoot)
|
||||
commandLine("cargo", "metadata", "--format-version", "1", "--no-deps")
|
||||
}.standardOutput.asText.get()
|
||||
|
||||
val parsedJson = JsonSlurper().parseText(jsonText) as Map<String, Any>
|
||||
val packages = parsedJson["packages"] as List<Map<String, Any>>
|
||||
return packages.first { it["name"] == packageName }["version"] as String
|
||||
}
|
||||
|
||||
fun semverToVersionCode(version: String): Int {
|
||||
val parts = version.substringBefore('-').split(".")
|
||||
val (major, minor, patch) = Triple(parts[0].toInt(), parts[1].toInt(), parts[2].toInt())
|
||||
|
||||
return major * 1_000_000 + minor * 1_000 + patch
|
||||
}
|
||||
|
||||
val ndkBuild = tasks.register<Exec>("ndkBuild") {
|
||||
val isRelease = gradle.startParameter.taskNames.any { it.endsWith("Release", ignoreCase = true) }
|
||||
workingDir = rootDir.parentFile
|
||||
val toolchainPath = "$rootDir/android.toolchain.cmake"
|
||||
environment("CMAKE_TOOLCHAIN_FILE", toolchainPath)
|
||||
|
||||
var minSdk = android.defaultConfig.minSdk
|
||||
var ndkDir = androidComponents.sdkComponents.ndkDirectory.get().asFile.absolutePath
|
||||
environment("ANDROID_NDK_HOME", "$ndkDir")
|
||||
environment("LIBCLANG_PATH", "$ndkDir/toolchains/llvm/prebuilt/linux-x86_64/musl/lib")
|
||||
|
||||
val jniType = if (isRelease) "release" else "debug"
|
||||
val jniLibsFolder = "$rootDir/app/src/$jniType/jniLibs"
|
||||
|
||||
commandLine(
|
||||
"cargo", "ndk",
|
||||
"--link-libcxx-shared",
|
||||
"-P", "$minSdk",
|
||||
"-t", "arm64-v8a",
|
||||
"-t", "x86_64",
|
||||
"-o", jniLibsFolder,
|
||||
"build", "--lib",
|
||||
"--profile", if (isRelease) "release" else "dev",
|
||||
)
|
||||
}
|
||||
|
||||
val sdlLibsArm64 = tasks.register<Copy>("sdlLibsArm64") {
|
||||
val isRelease = gradle.startParameter.taskNames.any { it.endsWith("Release", ignoreCase = true) }
|
||||
val jniType = if (isRelease) "release" else "debug"
|
||||
val jniLibsFolder = "$rootDir/app/src/$jniType/jniLibs/arm64-v8a"
|
||||
|
||||
from("$rootDir/../target/aarch64-linux-android/$jniType")
|
||||
into(jniLibsFolder)
|
||||
include("libSDL*")
|
||||
}
|
||||
|
||||
val sdlLibsX64 = tasks.register<Copy>("sdlLibsX64") {
|
||||
val isRelease = gradle.startParameter.taskNames.any { it.endsWith("Release", ignoreCase = true) }
|
||||
val jniType = if (isRelease) "release" else "debug"
|
||||
val jniLibsFolder = "$rootDir/app/src/$jniType/jniLibs/x86_64"
|
||||
|
||||
from("$rootDir/../target/x86_64-linux-android/$jniType")
|
||||
into(jniLibsFolder)
|
||||
include("libSDL*")
|
||||
}
|
||||
|
||||
tasks.named("preBuild") {
|
||||
dependsOn(sdlLibsArm64)
|
||||
dependsOn(sdlLibsX64)
|
||||
}
|
||||
|
||||
tasks.named("sdlLibsArm64") {
|
||||
dependsOn(ndkBuild)
|
||||
}
|
||||
|
||||
tasks.named("sdlLibsX64") {
|
||||
dependsOn(ndkBuild)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.material)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLActivity {
|
||||
java.lang.String nativeGetHint(java.lang.String); # Java-side doesn't use this, so it gets minified, but C-side still tries to register it
|
||||
java.lang.String clipboardGetText();
|
||||
boolean clipboardHasText();
|
||||
void clipboardSetText(java.lang.String);
|
||||
int createCustomCursor(int[], int, int, int, int);
|
||||
void destroyCustomCursor(int);
|
||||
android.app.Activity getContext();
|
||||
boolean getManifestEnvironmentVariables();
|
||||
android.view.Surface getNativeSurface();
|
||||
void initTouch();
|
||||
boolean isAndroidTV();
|
||||
boolean isChromebook();
|
||||
boolean isDeXMode();
|
||||
boolean isTablet();
|
||||
void manualBackButton();
|
||||
int messageboxShowMessageBox(int, java.lang.String, java.lang.String, int[], int[], java.lang.String[], int[]);
|
||||
void minimizeWindow();
|
||||
boolean openURL(java.lang.String);
|
||||
void requestPermission(java.lang.String, int);
|
||||
boolean showToast(java.lang.String, int, int, int, int);
|
||||
boolean sendMessage(int, int);
|
||||
boolean setActivityTitle(java.lang.String);
|
||||
boolean setCustomCursor(int);
|
||||
void setOrientation(int, int, boolean, java.lang.String);
|
||||
boolean setRelativeMouseEnabled(boolean);
|
||||
boolean setSystemCursor(int);
|
||||
void setWindowStyle(boolean);
|
||||
boolean shouldMinimizeOnFocusLoss();
|
||||
boolean showTextInput(int, int, int, int, int);
|
||||
boolean supportsRelativeMouse();
|
||||
int openFileDescriptor(java.lang.String, java.lang.String);
|
||||
boolean showFileDialog(java.lang.String[], boolean, boolean, int);
|
||||
java.lang.String getPreferredLocales();
|
||||
java.lang.String formatLocale(java.util.Locale);
|
||||
}
|
||||
|
||||
-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.HIDDeviceManager {
|
||||
void closeDevice(int);
|
||||
boolean initialize(boolean, boolean);
|
||||
boolean openDevice(int);
|
||||
boolean readReport(int, byte[], boolean);
|
||||
int writeReport(int, byte[], boolean);
|
||||
}
|
||||
|
||||
-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLAudioManager {
|
||||
void registerAudioDeviceCallback();
|
||||
void unregisterAudioDeviceCallback();
|
||||
void audioSetThreadPriority(boolean, int);
|
||||
}
|
||||
|
||||
-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLControllerManager {
|
||||
void joystickSetSensorsEnabled(int, boolean);
|
||||
void pollInputDevices();
|
||||
void joystickSetLED(int, int, int, int);
|
||||
void pollHapticDevices();
|
||||
void hapticRun(int, float, int);
|
||||
void hapticRumble(int, float, float, int);
|
||||
void hapticStop(int);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature android:name="android.hardware.vulkan.version" android:required="true"
|
||||
android:version="0x00401000" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.bluetooth"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.gamepad"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.usb.host"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="Gopher64"
|
||||
android:appCategory="game"
|
||||
android:hardwareAccelerated="true">
|
||||
<activity android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
android:name=".SlintActivity" android:exported="true">
|
||||
<meta-data android:name="android.app.lib_name" android:value="gopher64" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".N64Activity"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="layoutDirection|locale|grammaticalGender|fontScale|fontWeightAdjustment|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
|
||||
android:preferMinimalPostProcessing="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,49 @@
|
||||
package io.github.gopher64.gopher64
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.PowerManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import org.libsdl.app.SDLActivity
|
||||
|
||||
class N64Activity : SDLActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val powerManager = getContext().getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
|
||||
if (powerManager.isSustainedPerformanceModeSupported) {
|
||||
Log.v("SDL", "Enabling sustained performance mode")
|
||||
window.setSustainedPerformanceMode(true)
|
||||
} else {
|
||||
Log.v("SDL", "Sustained performance mode not supported")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLibraries(): Array<String> = arrayOf(
|
||||
"SDL3",
|
||||
"SDL3_ttf",
|
||||
"SDL3_image",
|
||||
"gopher64"
|
||||
)
|
||||
|
||||
override fun getMainFunction(): String = "gopher64_sdl_main"
|
||||
|
||||
override fun getArguments(): Array<String> {
|
||||
val intent = intent ?: return super.getArguments()
|
||||
val args = intent.getStringArrayExtra("args") ?: return super.getArguments()
|
||||
|
||||
val dataIntent = Intent()
|
||||
val file_path = intent.getStringExtra("file_path")
|
||||
if (file_path != null) {
|
||||
dataIntent.putExtra("file_path", file_path)
|
||||
}
|
||||
val cheats_path = intent.getStringExtra("cheats_path")
|
||||
if (cheats_path != null) {
|
||||
dataIntent.putExtra("cheats_path", cheats_path)
|
||||
}
|
||||
setResult(RESULT_OK, dataIntent)
|
||||
return args
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package io.github.gopher64.gopher64
|
||||
|
||||
import android.app.NativeActivity
|
||||
import android.content.Intent
|
||||
|
||||
class SlintActivity : NativeActivity() {
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("gopher64")
|
||||
}
|
||||
}
|
||||
private external fun nativeOnActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
nativeOnActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#419567</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
@@ -0,0 +1,12 @@
|
||||
#This file is generated by updateDaemonJvm
|
||||
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
|
||||
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
|
||||
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
|
||||
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
|
||||
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect
|
||||
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect
|
||||
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
|
||||
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
|
||||
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect
|
||||
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect
|
||||
toolchainVersion=21
|
||||
@@ -0,0 +1,20 @@
|
||||
[versions]
|
||||
agp = "9.2.1"
|
||||
coreKtx = "1.18.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
appcompat = "1.7.1"
|
||||
material = "1.14.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#Thu May 21 08:40:22 CEST 2026
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,251 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,94 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,26 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "Gopher64"
|
||||
include(":app")
|
||||
@@ -2,10 +2,13 @@ fn main() {
|
||||
println!("cargo::rerun-if-changed=parallel-rdp");
|
||||
println!("cargo::rerun-if-changed=retroachievements");
|
||||
println!("cargo::rerun-if-changed=src/compat");
|
||||
println!("cargo::rerun-if-changed=data/translations");
|
||||
println!("cargo::rerun-if-changed=data/ui");
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
{
|
||||
let slint_config = slint_build::CompilerConfiguration::new();
|
||||
let slint_config = slint_build::CompilerConfiguration::new()
|
||||
.with_bundled_translations("data/translations");
|
||||
slint_build::compile_with_config("src/ui/gui/appwindow.slint", slint_config).unwrap();
|
||||
}
|
||||
|
||||
@@ -133,9 +136,6 @@ fn main() {
|
||||
.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()
|
||||
@@ -146,9 +146,6 @@ fn main() {
|
||||
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");
|
||||
@@ -169,7 +166,7 @@ fn main() {
|
||||
.allowlist_function("ra_get_hardcore")
|
||||
.allowlist_function("ra_load_game")
|
||||
.allowlist_function("ra_unload_game")
|
||||
.allowlist_function("ra_set_dmem")
|
||||
.allowlist_function("ra_set_rdram")
|
||||
.allowlist_function("ra_do_frame")
|
||||
.allowlist_function("ra_do_idle")
|
||||
.allowlist_function("ra_http_callback")
|
||||
@@ -177,13 +174,13 @@ fn main() {
|
||||
.allowlist_function("ra_login_token_user")
|
||||
.allowlist_function("ra_state_size")
|
||||
.allowlist_function("ra_save_state")
|
||||
.allowlist_function("ra_load_state");
|
||||
.allowlist_function("ra_load_state")
|
||||
.allowlist_function("ra_get_rich_presence");
|
||||
|
||||
if cfg!(feature = "gui") {
|
||||
retroachievements_builder = retroachievements_builder
|
||||
.allowlist_function("ra_logout_user")
|
||||
.allowlist_function("ra_get_username")
|
||||
.allowlist_function("ra_get_token");
|
||||
.allowlist_function("ra_get_username");
|
||||
}
|
||||
|
||||
let retroachievements_bindings = retroachievements_builder
|
||||
@@ -205,12 +202,12 @@ fn main() {
|
||||
.allowlist_function("rdp_process_commands")
|
||||
.allowlist_function("rdp_onscreen_message")
|
||||
.allowlist_function("rdp_check_callback")
|
||||
.allowlist_function("rdp_new_processor")
|
||||
.allowlist_function("rdp_check_framebuffers")
|
||||
.allowlist_function("rdp_state_size")
|
||||
.allowlist_function("rdp_save_state")
|
||||
.allowlist_function("rdp_load_state")
|
||||
.allowlist_function("rdp_set_fps")
|
||||
.allowlist_function("get_joystick_event")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
.generate()
|
||||
.expect("Unable to generate bindings");
|
||||
@@ -308,4 +305,29 @@ fn main() {
|
||||
{
|
||||
println!("cargo:rustc-cfg=ra_hardcore_enabled");
|
||||
}
|
||||
if os == "android" {
|
||||
copy_dir_all(
|
||||
std::path::Path::new(sdl3_src::SOURCE_DIR)
|
||||
.join("android-project/app/src/main/java/org/libsdl/app"),
|
||||
"android-project/app/src/main/java/org/libsdl/app",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_dir_all(
|
||||
src: impl AsRef<std::path::Path>,
|
||||
dst: impl AsRef<std::path::Path>,
|
||||
) -> std::io::Result<()> {
|
||||
std::fs::create_dir_all(&dst)?;
|
||||
for entry in std::fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let ty = entry.file_type()?;
|
||||
if ty.is_dir() {
|
||||
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
|
||||
} else {
|
||||
std::fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Adding translations to Gopher64
|
||||
1. Copy and modify the `gopher64.pot` file in this directory with your translations.
|
||||
2. Rename it to `gopher64.po` and put it here:
|
||||
`data/translations/<lang>/LC_MESSAGES/gopher64.po`
|
||||
|
||||
See [here](https://docs.slint.dev/latest/docs/slint/guide/development/translations/#translating-strings) for more details on creating a .po file. I recommend using a program like [poedit](https://poedit.com) to create the .po file from the template.
|
||||
|
||||
---
|
||||
|
||||
For example, in the .pot file you will see:
|
||||
```
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr ""
|
||||
```
|
||||
|
||||
A translation will look something like this:
|
||||
```
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr "Nova versão disponível! Clique aqui para baixar"
|
||||
```
|
||||
|
||||
The only line you modify is the `msgstr`. The `msgctxt` and `msgid` need to remain unchanged.
|
||||
@@ -0,0 +1,564 @@
|
||||
# gopher64 - German Translation
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-05-25 08:28+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr "Neue Version verfügbar! Zum Herunterladen hier klicken"
|
||||
|
||||
#: src/ui/gui/about_page.slint:33
|
||||
msgctxt "About"
|
||||
msgid "About"
|
||||
msgstr "Über"
|
||||
|
||||
#: src/ui/gui/about_page.slint:40
|
||||
msgctxt "About"
|
||||
msgid "Wiki"
|
||||
msgstr "Wiki"
|
||||
|
||||
#: src/ui/gui/about_page.slint:50
|
||||
msgctxt "About"
|
||||
msgid "Discord Server"
|
||||
msgstr "Discord-Server"
|
||||
|
||||
#: src/ui/gui/about_page.slint:60
|
||||
msgctxt "About"
|
||||
msgid "Source Code"
|
||||
msgstr "Quellcode"
|
||||
|
||||
#: src/ui/gui/about_page.slint:70
|
||||
msgctxt "About"
|
||||
msgid "Donate (via Patreon)"
|
||||
msgstr "Spenden (über Patreon)"
|
||||
|
||||
#: src/ui/gui/about_page.slint:77
|
||||
msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Spenden (über GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:151
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Lokales Spiel"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:155
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Controller-Konfiguration"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:159
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:163
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Cheats"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:167
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:171
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:175
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "Über"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:15
|
||||
msgctxt "Cheats"
|
||||
msgid "Cheats"
|
||||
msgstr "Cheats"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:25
|
||||
msgctxt "Cheats"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROM auswählen"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:41
|
||||
msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Cheats für alle ROMs löschen"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:88
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "Keine Cheats verfügbar"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:80
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Enabled"
|
||||
msgstr "Aktiviert"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:93
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Transfer Pak"
|
||||
msgstr "Transfer Pak"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:107
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:157
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Configuration"
|
||||
msgstr "Controller-Konfiguration"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:171
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Configure Input Profile"
|
||||
msgstr "Eingabeprofil konfigurieren"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:180
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Settings"
|
||||
msgstr "Controller-Einstellungen"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:190
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Assignment"
|
||||
msgstr "Controller-Zuweisung"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:12
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Create/Modify Input Profile"
|
||||
msgstr "Eingabeprofil erstellen/bearbeiten"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:25
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Enter profile name to create or modify"
|
||||
msgstr "Profilname zum Erstellen oder Bearbeiten eingeben"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:38
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Deadzone Percentage"
|
||||
msgstr "Deadzone Größe in Prozent"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:61
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Use DirectInput"
|
||||
msgstr "DirectInput verwenden"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:74
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Start Profile Creation Wizard"
|
||||
msgstr "Assistent zur Profilerstellung starten"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:82
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:19
|
||||
msgctxt "LocalGame"
|
||||
msgid "Local Game"
|
||||
msgstr "Lokales Spiel"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:26
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open ROM"
|
||||
msgstr "ROM öffnen"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:37
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open Saves Folder"
|
||||
msgstr "Speicherordner öffnen"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:54
|
||||
msgctxt "LocalGame"
|
||||
msgid "Recent ROMs"
|
||||
msgstr "Zuletzt verwendete ROMs"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:7
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Netplay: Sitzung erstellen"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:23
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Sitzungsname:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:27
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Sitzungsname eingeben"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:37
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Passwort (optional):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:41
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Sitzungspasswort eingeben"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:52
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Spielername:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:56
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter player name"
|
||||
msgstr "Spielername eingeben"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:66
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Server:"
|
||||
msgstr "Server:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:86
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROM auswählen"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:102
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr ""
|
||||
"Für dieses Spiel sind Cheats aktiviert. Cheats werden mit anderen Spielern "
|
||||
"synchronisiert."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:112
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Sitzung erstellen"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:132
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:155
|
||||
msgctxt "CustomServerDialog"
|
||||
msgid "Enter a custom server URL"
|
||||
msgstr "Eigene Server-URL eingeben"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:7
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Netplay: Join Session"
|
||||
msgstr "Netplay: Sitzung beitreten"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:18
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Player Name (required):"
|
||||
msgstr "Spielername (erforderlich):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:22
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Enter player name"
|
||||
msgstr "Spielername eingeben"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:31
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password (if required)"
|
||||
msgstr "Passwort (falls erforderlich)"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:41
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Custom Server URL (optional):"
|
||||
msgstr "Eigene Server-URL (optional):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:53
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROM auswählen"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:68
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Sitzungsliste wird aktualisiert, bitte warten …"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr "Auf eine Sitzung klicken um diese auszuwählen"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:78
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "No sessions available. Please refresh to check again."
|
||||
msgstr "Keine Sitzungen verfügbar. Bitte aktualisieren, um erneut zu prüfen."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:84
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Server"
|
||||
msgstr "Server"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:85
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Sitzungsname"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:86
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Spielname"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:87
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Passwort"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:88
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Cheats"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:102
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Sitzungsliste aktualisieren"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:119
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Sitzung beitreten"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:132
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:37
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:45
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Sitzung erstellen"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:56
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Sitzung beitreten"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:70
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Netplay-Discord-Kanal"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:77
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Feedback geben"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:15
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Netplay: Warteraum"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:27
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Sitzungsname:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:33
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Game Name:"
|
||||
msgstr "Spielname:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:39
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Ping:"
|
||||
msgstr "Ping:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:48
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Spieler 1:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:52
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Spieler 2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:58
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Spieler 3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:62
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Spieler 4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:76
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Chatnachricht eingeben"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:87
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Nachricht senden"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:105
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Spiel starten"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:113
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:19
|
||||
msgctxt "RAData"
|
||||
msgid "Not currently logged in"
|
||||
msgstr "Derzeit nicht angemeldet"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:27
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:36
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Enabled"
|
||||
msgstr "Aktiviert"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:51
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Hardcore mode"
|
||||
msgstr "Hardcore-Modus"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:60
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Challenge Indicators"
|
||||
msgstr "Challenge Tipps"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:69
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Leaderboard Trackers"
|
||||
msgstr "Bestenlisten-Tracker"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:78
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Discord Rich Presence"
|
||||
msgstr "Rich Presence für Discord"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:96
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Profile"
|
||||
msgstr "Profil anzeigen"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:106
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Username"
|
||||
msgstr "Benutzername"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:116
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Password"
|
||||
msgstr "Passwort"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:125
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Login"
|
||||
msgstr "Anmelden"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:141
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Unterstützte Spiele anzeigen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:23
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:34
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Auflösung:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:48
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "SSAA-Herunterskalierung"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:56
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Pixelgenaue Skalierung"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:64
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Vollbild"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:72
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "16:9 (gestreckt)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:80
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "VSync"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:88
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "CRT-Shader anwenden"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:100
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "N64-CPU übertakten (kann Fehler verursachen)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:108
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Expansion Pak deaktivieren"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:116
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "UNFLoader emulieren"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:124
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "VRU emulieren"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "Was möchten Sie sagen?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
#~ "Server wird erstellt, bitte warten … Dies kann etwa 30 Sekunden dauern."
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Port"
|
||||
#~ msgstr "Port"
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Controller"
|
||||
#~ msgstr "Controller"
|
||||
@@ -0,0 +1,563 @@
|
||||
# gopher64 - Spanish Translation
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-05-25 08:28+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: Gemini\n"
|
||||
"Language-Team: Spanish (Spain)\n"
|
||||
"Language: es_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr "¡Nueva versión disponible! Pulsa aquí para descargar"
|
||||
|
||||
#: src/ui/gui/about_page.slint:33
|
||||
msgctxt "About"
|
||||
msgid "About"
|
||||
msgstr "Acerca de"
|
||||
|
||||
#: src/ui/gui/about_page.slint:40
|
||||
msgctxt "About"
|
||||
msgid "Wiki"
|
||||
msgstr "Wiki"
|
||||
|
||||
#: src/ui/gui/about_page.slint:50
|
||||
msgctxt "About"
|
||||
msgid "Discord Server"
|
||||
msgstr "Servidor de Discord"
|
||||
|
||||
#: src/ui/gui/about_page.slint:60
|
||||
msgctxt "About"
|
||||
msgid "Source Code"
|
||||
msgstr "Código fuente"
|
||||
|
||||
#: src/ui/gui/about_page.slint:70
|
||||
msgctxt "About"
|
||||
msgid "Donate (via Patreon)"
|
||||
msgstr "Donar (a través de Patreon)"
|
||||
|
||||
#: src/ui/gui/about_page.slint:77
|
||||
msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Donar (a través de GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:151
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Juego local"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:155
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Config. del mando"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:159
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:163
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Trucos"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:167
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:171
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Ajustes"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:175
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "Acerca de"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:15
|
||||
msgctxt "Cheats"
|
||||
msgid "Cheats"
|
||||
msgstr "Trucos"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:25
|
||||
msgctxt "Cheats"
|
||||
msgid "Select ROM"
|
||||
msgstr "Seleccionar ROM"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:41
|
||||
msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Borrar trucos de todas las ROMs"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:88
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "No hay trucos disponibles"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:80
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Enabled"
|
||||
msgstr "Activado"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:93
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Transfer Pak"
|
||||
msgstr "Transfer Pak"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:107
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Profile"
|
||||
msgstr "Perfil"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:157
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Configuration"
|
||||
msgstr "Configuración del mando"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:171
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Configure Input Profile"
|
||||
msgstr "Configurar perfil de entrada"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:180
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Settings"
|
||||
msgstr "Ajustes del mando"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:190
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Assignment"
|
||||
msgstr "Asignación del mando"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:12
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Create/Modify Input Profile"
|
||||
msgstr "Crear/Modificar perfil de entrada"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:25
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Enter profile name to create or modify"
|
||||
msgstr "Introduce el nombre del perfil para crear o modificar"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:38
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Deadzone Percentage"
|
||||
msgstr "Porcentaje de zona muerta"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:61
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Use DirectInput"
|
||||
msgstr "Usar DirectInput"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:74
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Start Profile Creation Wizard"
|
||||
msgstr "Iniciar asistente de creación de perfil"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:82
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:19
|
||||
msgctxt "LocalGame"
|
||||
msgid "Local Game"
|
||||
msgstr "Juego local"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:26
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open ROM"
|
||||
msgstr "Abrir ROM"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:37
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open Saves Folder"
|
||||
msgstr "Abrir carpeta de guardados"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:54
|
||||
msgctxt "LocalGame"
|
||||
msgid "Recent ROMs"
|
||||
msgstr "ROMs recientes"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:7
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Netplay: Crear sesión"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:23
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nombre de la sesión:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:27
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Introduce el nombre de la sesión"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:37
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Contraseña (Opcional):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:41
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Introduce la contraseña"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:52
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Nombre del jugador:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:56
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter player name"
|
||||
msgstr "Introduce tu nombre"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:66
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Server:"
|
||||
msgstr "Servidor:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:86
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Select ROM"
|
||||
msgstr "Seleccionar ROM"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:102
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr ""
|
||||
"Este juego tiene trucos activados. Los trucos se sincronizarán con los otros "
|
||||
"jugadores."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:112
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Crear sesión"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:132
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:155
|
||||
msgctxt "CustomServerDialog"
|
||||
msgid "Enter a custom server URL"
|
||||
msgstr "Introduce la URL de servidor personalizado"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:7
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Netplay: Join Session"
|
||||
msgstr "Netplay: Unirse a la sesión"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:18
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Player Name (required):"
|
||||
msgstr "Nombre de jugador (requerido):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:22
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Enter player name"
|
||||
msgstr "Introduce tu nombre"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:31
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password (if required)"
|
||||
msgstr "Contraseña (si es necesaria)"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:41
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Custom Server URL (optional):"
|
||||
msgstr "URL de servidor personalizado (opcional):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:53
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "Seleccionar ROM"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:68
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Actualizando lista de sesiones, espera..."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr "Pulsa una sesión para seleccionarla"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:78
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "No sessions available. Please refresh to check again."
|
||||
msgstr "No hay sesiones disponibles. Actualiza para comprobar de nuevo."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:84
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Server"
|
||||
msgstr "Servidor"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:85
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Nombre de la sesión"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:86
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Nombre del juego"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:87
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Contraseña"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:88
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Trucos"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:102
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Actualizar lista"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:119
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Unirse a la sesión"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:132
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:37
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:45
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Crear sesión"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:56
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Unirse a la sesión"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:70
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Canal de Discord de Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:77
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Enviar comentarios"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:15
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Netplay: Sala de espera"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:27
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nombre de la sesión:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:33
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Game Name:"
|
||||
msgstr "Nombre del juego:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:39
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Ping:"
|
||||
msgstr "Latencia (Ping):"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:48
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Jugador 1:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:52
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Jugador 2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:58
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Jugador 3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:62
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Jugador 4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:76
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Escribe un mensaje en el chat"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:87
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Enviar mensaje"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:105
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Iniciar juego"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:113
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:19
|
||||
msgctxt "RAData"
|
||||
msgid "Not currently logged in"
|
||||
msgstr "Sesión no iniciada"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:27
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:36
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Enabled"
|
||||
msgstr "Activado"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:51
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Hardcore mode"
|
||||
msgstr "Modo Hardcore"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:60
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Challenge Indicators"
|
||||
msgstr "Indicadores de desafío"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:69
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Leaderboard Trackers"
|
||||
msgstr "Indicadores de tabla de clasificación"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:78
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Discord Rich Presence"
|
||||
msgstr "Rich Presence en Discord"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:96
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Profile"
|
||||
msgstr "Ver perfil"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:106
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Username"
|
||||
msgstr "Usuario"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:116
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Password"
|
||||
msgstr "Contraseña"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:125
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Login"
|
||||
msgstr "Entrar"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:141
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Ver juegos compatibles"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:23
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Ajustes"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:34
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Resolución:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:48
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "Reducción de escala SSAA"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:56
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Escalado por enteros"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:64
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Pantalla completa"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:72
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "Pantalla ancha (estirado)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:80
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "Sincronización vertical (VSync)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:88
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "Aplicar sombreador CRT"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:100
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "Overclock CPU N64 (puede causar errores)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:108
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Desactivar Expansion Pak"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:116
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "Emular UNFLoader"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:124
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "Emular VRU"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "¿Qué te gustaría decir?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr "Creando servidor, espera... Puede tardar unos 30 segundos."
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Port"
|
||||
#~ msgstr "Puerto"
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Controller"
|
||||
#~ msgstr "Mando"
|
||||
@@ -0,0 +1,566 @@
|
||||
# gopher64 - French (France) Translation
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-05-25 08:28+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French (France)\n"
|
||||
"Language: fr_FR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr "Nouvelle version disponible ! Cliquez ici pour télécharger"
|
||||
|
||||
#: src/ui/gui/about_page.slint:33
|
||||
msgctxt "About"
|
||||
msgid "About"
|
||||
msgstr "À propos"
|
||||
|
||||
#: src/ui/gui/about_page.slint:40
|
||||
msgctxt "About"
|
||||
msgid "Wiki"
|
||||
msgstr "Wiki"
|
||||
|
||||
#: src/ui/gui/about_page.slint:50
|
||||
msgctxt "About"
|
||||
msgid "Discord Server"
|
||||
msgstr "Serveur Discord"
|
||||
|
||||
#: src/ui/gui/about_page.slint:60
|
||||
msgctxt "About"
|
||||
msgid "Source Code"
|
||||
msgstr "Code source"
|
||||
|
||||
#: src/ui/gui/about_page.slint:70
|
||||
msgctxt "About"
|
||||
msgid "Donate (via Patreon)"
|
||||
msgstr "Faire un don (via Patreon)"
|
||||
|
||||
#: src/ui/gui/about_page.slint:77
|
||||
msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Faire un don (via GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:151
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Jeu local"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:155
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Configuration manette"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:159
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:163
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Triche"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:167
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:171
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Paramètres"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:175
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "À propos"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:15
|
||||
msgctxt "Cheats"
|
||||
msgid "Cheats"
|
||||
msgstr "Triche"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:25
|
||||
msgctxt "Cheats"
|
||||
msgid "Select ROM"
|
||||
msgstr "Sélectionner une ROM"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:41
|
||||
msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Effacer la triche pour toutes les ROMs"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:88
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "Aucune triche disponible"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:80
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Enabled"
|
||||
msgstr "Activé"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:93
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Transfer Pak"
|
||||
msgstr "Transfer Pak"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:107
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:157
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Configuration"
|
||||
msgstr "Configuration de la manette"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:171
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Configure Input Profile"
|
||||
msgstr "Configurer le profil d'entrée"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:180
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Settings"
|
||||
msgstr "Paramètres de la manette"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:190
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Assignment"
|
||||
msgstr "Attribution de la manette"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:12
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Create/Modify Input Profile"
|
||||
msgstr "Créer / modifier le profil d'entrée"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:25
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Enter profile name to create or modify"
|
||||
msgstr "Saisir le nom du profil à créer ou à modifier"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:38
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Deadzone Percentage"
|
||||
msgstr "Pourcentage de zone morte"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:61
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Use DirectInput"
|
||||
msgstr "Utiliser DirectInput"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:74
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Start Profile Creation Wizard"
|
||||
msgstr "Lancer l'assistant de création de profil"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:82
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:19
|
||||
msgctxt "LocalGame"
|
||||
msgid "Local Game"
|
||||
msgstr "Jeu local"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:26
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open ROM"
|
||||
msgstr "Ouvrir une ROM"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:37
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open Saves Folder"
|
||||
msgstr "Ouvrir le dossier des sauvegardes"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:54
|
||||
msgctxt "LocalGame"
|
||||
msgid "Recent ROMs"
|
||||
msgstr "ROMs récentes"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:7
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Netplay : créer une session"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:23
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nom de la session :"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:27
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Saisir le nom de la session"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:37
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Mot de passe (facultatif) :"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:41
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Saisir le mot de passe de la session"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:52
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Nom du joueur :"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:56
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter player name"
|
||||
msgstr "Saisir le nom du joueur"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:66
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Server:"
|
||||
msgstr "Serveur :"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:86
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Select ROM"
|
||||
msgstr "Sélectionner une ROM"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:102
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr ""
|
||||
"La triche est activée pour ce jeu. Elle sera synchronisée avec les autres "
|
||||
"joueurs."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:112
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Créer la session"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:132
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:155
|
||||
msgctxt "CustomServerDialog"
|
||||
msgid "Enter a custom server URL"
|
||||
msgstr "Saisir l'URL personnalisée du serveur"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:7
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Netplay: Join Session"
|
||||
msgstr "Netplay : rejoindre une session"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:18
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Player Name (required):"
|
||||
msgstr "Nom du joueur (obligatoire) :"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:22
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Enter player name"
|
||||
msgstr "Saisir le nom du joueur"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:31
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password (if required)"
|
||||
msgstr "Mot de passe (si requis)"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:41
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Custom Server URL (optional):"
|
||||
msgstr "URL personnalisée du serveur (facultatif) :"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:53
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "Sélectionner une ROM"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:68
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Actualisation de la liste des sessions, veuillez patienter…"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr "Cliquez sur une session ci-dessous pour la sélectionner"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:78
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "No sessions available. Please refresh to check again."
|
||||
msgstr ""
|
||||
"Aucune session disponible. Veuillez actualiser pour vérifier à nouveau."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:84
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Server"
|
||||
msgstr "Serveur"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:85
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Nom de la session"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:86
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Nom du jeu"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:87
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Mot de passe"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:88
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Triche"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:102
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Actualiser la liste"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:119
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Rejoindre la session"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:132
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:37
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:45
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Créer une session"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:56
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Rejoindre une session"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:70
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Salon Discord Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:77
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Donner votre avis"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:15
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Netplay : salle d'attente"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:27
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nom de la session :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:33
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Game Name:"
|
||||
msgstr "Nom du jeu :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:39
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Ping:"
|
||||
msgstr "Ping :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:48
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Joueur 1 :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:52
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Joueur 2 :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:58
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Joueur 3 :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:62
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Joueur 4 :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:76
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Saisir un message"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:87
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Envoyer"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:105
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Lancer la partie"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:113
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:19
|
||||
msgctxt "RAData"
|
||||
msgid "Not currently logged in"
|
||||
msgstr "Non connecté"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:27
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:36
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Enabled"
|
||||
msgstr "Activé"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:51
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Hardcore mode"
|
||||
msgstr "Mode hardcore"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:60
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Challenge Indicators"
|
||||
msgstr "Indicateurs de défi"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:69
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Leaderboard Trackers"
|
||||
msgstr "Suivi des classements"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:78
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Discord Rich Presence"
|
||||
msgstr "Rich Presence Discord"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:96
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Profile"
|
||||
msgstr "Voir le profil"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:106
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Username"
|
||||
msgstr "Nom d'utilisateur"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:116
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Password"
|
||||
msgstr "Mot de passe"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:125
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Login"
|
||||
msgstr "Connexion"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:141
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Voir les jeux pris en charge"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:23
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Paramètres"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:34
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Résolution :"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:48
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "Réduction SSAA"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:56
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Mise à l'échelle par entiers"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:64
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Plein écran"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:72
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "Écran large (étiré)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:80
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "Synchro verticale"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:88
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "Appliquer le filtre CRT"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:100
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "Surcharger le CPU N64 (risque de bugs)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:108
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Désactiver l'Expansion Pak"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:116
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "Émuler UNFLoader"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:124
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "Émuler le VRU"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "Que souhaitez-vous dire ?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
#~ "Création du serveur, veuillez patienter… Cela peut prendre environ 30 "
|
||||
#~ "secondes."
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Port"
|
||||
#~ msgstr "Port"
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Controller"
|
||||
#~ msgstr "Manette"
|
||||
@@ -0,0 +1,549 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2026-05-25 08:28+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/about_page.slint:33
|
||||
msgctxt "About"
|
||||
msgid "About"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/about_page.slint:40
|
||||
msgctxt "About"
|
||||
msgid "Wiki"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/about_page.slint:50
|
||||
msgctxt "About"
|
||||
msgid "Discord Server"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/about_page.slint:60
|
||||
msgctxt "About"
|
||||
msgid "Source Code"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/about_page.slint:70
|
||||
msgctxt "About"
|
||||
msgid "Donate (via Patreon)"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/about_page.slint:77
|
||||
msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:151
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:155
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:159
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:163
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:167
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:171
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:175
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:15
|
||||
msgctxt "Cheats"
|
||||
msgid "Cheats"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:25
|
||||
msgctxt "Cheats"
|
||||
msgid "Select ROM"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:41
|
||||
msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:88
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/controller_page.slint:80
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/controller_page.slint:93
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Transfer Pak"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/controller_page.slint:107
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/controller_page.slint:157
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Configuration"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/controller_page.slint:171
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Configure Input Profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/controller_page.slint:180
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/controller_page.slint:190
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Assignment"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/input_profile.slint:12
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Create/Modify Input Profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/input_profile.slint:25
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Enter profile name to create or modify"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/input_profile.slint:38
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Deadzone Percentage"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/input_profile.slint:61
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Use DirectInput"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/input_profile.slint:74
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Start Profile Creation Wizard"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/input_profile.slint:82
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:19
|
||||
msgctxt "LocalGame"
|
||||
msgid "Local Game"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:26
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open ROM"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:37
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open Saves Folder"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:54
|
||||
msgctxt "LocalGame"
|
||||
msgid "Recent ROMs"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:7
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:23
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:27
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:37
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:41
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:52
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:56
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter player name"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:66
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Server:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:86
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Select ROM"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:102
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:112
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:132
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:155
|
||||
msgctxt "CustomServerDialog"
|
||||
msgid "Enter a custom server URL"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:7
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Netplay: Join Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:18
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Player Name (required):"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:22
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Enter player name"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:31
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password (if required)"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:41
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Custom Server URL (optional):"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:53
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:68
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:78
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "No sessions available. Please refresh to check again."
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:84
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Server"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:85
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:86
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:87
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:88
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:102
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:119
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:132
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:37
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:45
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:56
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:70
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:77
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:15
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:27
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:33
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Game Name:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:39
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Ping:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:48
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:52
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:58
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:62
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:76
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:87
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:105
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:113
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:19
|
||||
msgctxt "RAData"
|
||||
msgid "Not currently logged in"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:27
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "RetroAchievements"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:36
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:51
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Hardcore mode"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:60
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Challenge Indicators"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:69
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Leaderboard Trackers"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:78
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Discord Rich Presence"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:96
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:106
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:116
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:125
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:141
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:23
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:34
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:48
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:56
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:64
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:72
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:80
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:88
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:100
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:108
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:116
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:124
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,563 @@
|
||||
# gopher64 - Japanese Translation
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-05-25 08:28+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Japanese\n"
|
||||
"Language: ja\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr "新しいバージョンがあります。クリックしてダウンロード"
|
||||
|
||||
#: src/ui/gui/about_page.slint:33
|
||||
msgctxt "About"
|
||||
msgid "About"
|
||||
msgstr "バージョン情報"
|
||||
|
||||
#: src/ui/gui/about_page.slint:40
|
||||
msgctxt "About"
|
||||
msgid "Wiki"
|
||||
msgstr "Wiki"
|
||||
|
||||
#: src/ui/gui/about_page.slint:50
|
||||
msgctxt "About"
|
||||
msgid "Discord Server"
|
||||
msgstr "Discordサーバー"
|
||||
|
||||
#: src/ui/gui/about_page.slint:60
|
||||
msgctxt "About"
|
||||
msgid "Source Code"
|
||||
msgstr "ソースコード"
|
||||
|
||||
#: src/ui/gui/about_page.slint:70
|
||||
msgctxt "About"
|
||||
msgid "Donate (via Patreon)"
|
||||
msgstr "Patreonで寄付"
|
||||
|
||||
#: src/ui/gui/about_page.slint:77
|
||||
msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "GitHubで寄付"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:151
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "ローカルゲーム"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:155
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "コントローラー設定"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:159
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "ネットプレイ"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:163
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "チート"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:167
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:171
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "設定"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:175
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "バージョン情報"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:15
|
||||
msgctxt "Cheats"
|
||||
msgid "Cheats"
|
||||
msgstr "チート"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:25
|
||||
msgctxt "Cheats"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROMを選択"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:41
|
||||
msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "すべてのROMのチートをクリア"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:88
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "利用できるチートはありません"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:80
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Enabled"
|
||||
msgstr "有効"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:93
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Transfer Pak"
|
||||
msgstr "Transfer Pak"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:107
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Profile"
|
||||
msgstr "プロファイル"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:157
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Configuration"
|
||||
msgstr "コントローラー設定"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:171
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Configure Input Profile"
|
||||
msgstr "入力プロファイルを設定"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:180
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Settings"
|
||||
msgstr "コントローラー詳細設定"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:190
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Assignment"
|
||||
msgstr "コントローラー割り当て"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:12
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Create/Modify Input Profile"
|
||||
msgstr "入力プロファイルの作成/変更"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:25
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Enter profile name to create or modify"
|
||||
msgstr "作成または変更するプロファイル名を入力"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:38
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Deadzone Percentage"
|
||||
msgstr "デッドゾーン(%)"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:61
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Use DirectInput"
|
||||
msgstr "DirectInputを使用"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:74
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Start Profile Creation Wizard"
|
||||
msgstr "プロファイル作成ウィザードを開始"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:82
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Cancel"
|
||||
msgstr "キャンセル"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:19
|
||||
msgctxt "LocalGame"
|
||||
msgid "Local Game"
|
||||
msgstr "ローカルゲーム"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:26
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open ROM"
|
||||
msgstr "ROMを開く"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:37
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open Saves Folder"
|
||||
msgstr "セーブフォルダーを開く"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:54
|
||||
msgctxt "LocalGame"
|
||||
msgid "Recent ROMs"
|
||||
msgstr "最近使ったROM"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:7
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "ネットプレイ:セッション作成"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:23
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "セッション名:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:27
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "セッション名を入力"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:37
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "パスワード(任意):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:41
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "セッションのパスワードを入力"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:52
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "プレイヤー名:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:56
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter player name"
|
||||
msgstr "プレイヤー名を入力"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:66
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Server:"
|
||||
msgstr "サーバー:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:86
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROMを選択"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:102
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr "このゲームではチートが有効です。チートは他のプレイヤーと同期されます。"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:112
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "セッションを作成"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:132
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Cancel"
|
||||
msgstr "キャンセル"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:155
|
||||
msgctxt "CustomServerDialog"
|
||||
msgid "Enter a custom server URL"
|
||||
msgstr "カスタムサーバーURLを入力"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:7
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Netplay: Join Session"
|
||||
msgstr "ネットプレイ:セッションに参加"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:18
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Player Name (required):"
|
||||
msgstr "プレイヤー名(必須):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:22
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Enter player name"
|
||||
msgstr "プレイヤー名を入力"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:31
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password (if required)"
|
||||
msgstr "パスワード(必要な場合)"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:41
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Custom Server URL (optional):"
|
||||
msgstr "カスタムサーバーURL(任意):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:53
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROMを選択"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:68
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "セッション一覧を更新しています。しばらくお待ちください…"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr "下の一覧からセッションをクリックして選択"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:78
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "No sessions available. Please refresh to check again."
|
||||
msgstr "利用できるセッションがありません。更新して再度確認してください。"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:84
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Server"
|
||||
msgstr "サーバー"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:85
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "セッション名"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:86
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "ゲーム名"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:87
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "パスワード"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:88
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "チート"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:102
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "セッション一覧を更新"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:119
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "セッションに参加"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:132
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "キャンセル"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:37
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "ネットプレイ"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:45
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "セッションを作成"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:56
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "セッションに参加"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:70
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "ネットプレイ用Discordチャンネル"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:77
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "フィードバックを送る"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:15
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "ネットプレイ:待機室"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:27
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "セッション名:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:33
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Game Name:"
|
||||
msgstr "ゲーム名:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:39
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Ping:"
|
||||
msgstr "Ping:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:48
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "プレイヤー1:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:52
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "プレイヤー2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:58
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "プレイヤー3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:62
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "プレイヤー4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:76
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "チャットメッセージを入力"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:87
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "送信"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:105
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "ゲーム開始"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:113
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "キャンセル"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:19
|
||||
msgctxt "RAData"
|
||||
msgid "Not currently logged in"
|
||||
msgstr "ログインしていません"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:27
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:36
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Enabled"
|
||||
msgstr "有効"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:51
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Hardcore mode"
|
||||
msgstr "ハードコアモード"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:60
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Challenge Indicators"
|
||||
msgstr "チャレンジ表示"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:69
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Leaderboard Trackers"
|
||||
msgstr "リーダーボード追跡"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:78
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Discord Rich Presence"
|
||||
msgstr "Discord のリッチプレゼンス"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:96
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Profile"
|
||||
msgstr "プロフィールを表示"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:106
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Username"
|
||||
msgstr "ユーザー名"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:116
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Password"
|
||||
msgstr "パスワード"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:125
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Login"
|
||||
msgstr "ログイン"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:141
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "対応ゲームを表示"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:23
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "設定"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:34
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "解像度:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:48
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "SSAAダウンスケール"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:56
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "整数倍スケーリング"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:64
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "全画面表示"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:72
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "ワイド画面(伸ばす)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:80
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "垂直同期(VSync)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:88
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "CRTシェーダーを適用"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:100
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "N64のCPUをオーバークロック(不具合の原因になることがあります)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:108
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "拡張パックを無効にする"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:116
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "UNFLoaderをエミュレート"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:124
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "VRUをエミュレート"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "何と言いますか?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
#~ "サーバーを作成しています。しばらくお待ちください… 最大約30秒かかることがあ"
|
||||
#~ "ります。"
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Port"
|
||||
#~ msgstr "ポート"
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Controller"
|
||||
#~ msgstr "コントローラー"
|
||||
@@ -0,0 +1,564 @@
|
||||
# gopher64 - Dutch (Netherlands) Translation
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-05-25 08:28+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch (Netherlands)\n"
|
||||
"Language: nl_NL\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr "Nieuwe versie beschikbaar! Klik hier om te downloaden"
|
||||
|
||||
#: src/ui/gui/about_page.slint:33
|
||||
msgctxt "About"
|
||||
msgid "About"
|
||||
msgstr "Over"
|
||||
|
||||
#: src/ui/gui/about_page.slint:40
|
||||
msgctxt "About"
|
||||
msgid "Wiki"
|
||||
msgstr "Wiki"
|
||||
|
||||
#: src/ui/gui/about_page.slint:50
|
||||
msgctxt "About"
|
||||
msgid "Discord Server"
|
||||
msgstr "Discord-server"
|
||||
|
||||
#: src/ui/gui/about_page.slint:60
|
||||
msgctxt "About"
|
||||
msgid "Source Code"
|
||||
msgstr "Broncode"
|
||||
|
||||
#: src/ui/gui/about_page.slint:70
|
||||
msgctxt "About"
|
||||
msgid "Donate (via Patreon)"
|
||||
msgstr "Doneren (via Patreon)"
|
||||
|
||||
#: src/ui/gui/about_page.slint:77
|
||||
msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Doneren (via GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:151
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Lokaal spel"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:155
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Controllerconfiguratie"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:159
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:163
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Cheats"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:167
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:171
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Instellingen"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:175
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "Over"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:15
|
||||
msgctxt "Cheats"
|
||||
msgid "Cheats"
|
||||
msgstr "Cheats"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:25
|
||||
msgctxt "Cheats"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROM selecteren"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:41
|
||||
msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Cheats wissen voor alle ROM's"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:88
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "Geen cheats beschikbaar"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:80
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Enabled"
|
||||
msgstr "Ingeschakeld"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:93
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Transfer Pak"
|
||||
msgstr "Transfer Pak"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:107
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Profile"
|
||||
msgstr "Profiel"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:157
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Configuration"
|
||||
msgstr "Controllerconfiguratie"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:171
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Configure Input Profile"
|
||||
msgstr "Invoerprofiel configureren"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:180
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Settings"
|
||||
msgstr "Controllerinstellingen"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:190
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Assignment"
|
||||
msgstr "Controllertoewijzing"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:12
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Create/Modify Input Profile"
|
||||
msgstr "Invoerprofiel maken/wijzigen"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:25
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Enter profile name to create or modify"
|
||||
msgstr "Profielnaam invoeren om aan te maken of te wijzigen"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:38
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Deadzone Percentage"
|
||||
msgstr "Dode-zonepercentage"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:61
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Use DirectInput"
|
||||
msgstr "DirectInput gebruiken"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:74
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Start Profile Creation Wizard"
|
||||
msgstr "Wizard voor profiel maken starten"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:82
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuleren"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:19
|
||||
msgctxt "LocalGame"
|
||||
msgid "Local Game"
|
||||
msgstr "Lokaal spel"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:26
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open ROM"
|
||||
msgstr "ROM openen"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:37
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open Saves Folder"
|
||||
msgstr "Map met opslagen openen"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:54
|
||||
msgctxt "LocalGame"
|
||||
msgid "Recent ROMs"
|
||||
msgstr "Recent gebruikte ROM's"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:7
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Netplay: sessie aanmaken"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:23
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Sessienaam:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:27
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Sessienaam invoeren"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:37
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Wachtwoord (optioneel):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:41
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Sessiewachtwoord invoeren"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:52
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Spelernaam:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:56
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter player name"
|
||||
msgstr "Spelernaam invoeren"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:66
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Server:"
|
||||
msgstr "Server:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:86
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROM selecteren"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:102
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr ""
|
||||
"Voor dit spel zijn cheats ingeschakeld. Cheats worden gesynchroniseerd met "
|
||||
"andere spelers."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:112
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Sessie aanmaken"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:132
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuleren"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:155
|
||||
msgctxt "CustomServerDialog"
|
||||
msgid "Enter a custom server URL"
|
||||
msgstr "Aangepaste server-URL invoeren"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:7
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Netplay: Join Session"
|
||||
msgstr "Netplay: deelnemen aan sessie"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:18
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Player Name (required):"
|
||||
msgstr "Spelernaam (verplicht):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:22
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Enter player name"
|
||||
msgstr "Spelernaam invoeren"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:31
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password (if required)"
|
||||
msgstr "Wachtwoord (indien vereist)"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:41
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Custom Server URL (optional):"
|
||||
msgstr "Aangepaste server-URL (optioneel):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:53
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROM selecteren"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:68
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Sessielijst wordt vernieuwd, even geduld…"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr "Klik hieronder op een sessie om die te selecteren"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:78
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "No sessions available. Please refresh to check again."
|
||||
msgstr "Geen sessies beschikbaar. Vernieuw om opnieuw te controleren."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:84
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Server"
|
||||
msgstr "Server"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:85
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Sessienaam"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:86
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Spelnaam"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:87
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Wachtwoord"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:88
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Cheats"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:102
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Sessielijst vernieuwen"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:119
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Deelnemen aan sessie"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:132
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuleren"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:37
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:45
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Sessie aanmaken"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:56
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Deelnemen aan sessie"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:70
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Netplay-Discordkanaal"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:77
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Feedback geven"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:15
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Netplay: wachtruimte"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:27
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Sessienaam:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:33
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Game Name:"
|
||||
msgstr "Spelnaam:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:39
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Ping:"
|
||||
msgstr "Ping:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:48
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Speler 1:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:52
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Speler 2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:58
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Speler 3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:62
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Speler 4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:76
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Chatbericht invoeren"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:87
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Bericht verzenden"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:105
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Spel starten"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:113
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuleren"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:19
|
||||
msgctxt "RAData"
|
||||
msgid "Not currently logged in"
|
||||
msgstr "Momenteel niet ingelogd"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:27
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:36
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Enabled"
|
||||
msgstr "Ingeschakeld"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:51
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Hardcore mode"
|
||||
msgstr "Hardcore-modus"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:60
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Challenge Indicators"
|
||||
msgstr "Challenge-indicatoren"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:69
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Leaderboard Trackers"
|
||||
msgstr "Leaderboard-trackers"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:78
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Discord Rich Presence"
|
||||
msgstr "Discord Rich Presence"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:96
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Profile"
|
||||
msgstr "Profiel bekijken"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:106
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Username"
|
||||
msgstr "Gebruikersnaam"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:116
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Password"
|
||||
msgstr "Wachtwoord"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:125
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Login"
|
||||
msgstr "Inloggen"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:141
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Ondersteunde spellen bekijken"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:23
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Instellingen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:34
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Resolutie:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:48
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "SSAA-downscaling"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:56
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Schalen in hele getallen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:64
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Volledig scherm"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:72
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "Breedbeeld (uitgerekt)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:80
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "VSync"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:88
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "CRT-shader toepassen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:100
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "N64-CPU overklokken (kan bugs veroorzaken)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:108
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Expansion Pak uitschakelen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:116
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "UNFLoader emuleren"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:124
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "VRU emuleren"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "Wat wil je zeggen?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
#~ "Server wordt aangemaakt, even geduld… Dit kan ongeveer 30 seconden duren."
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Port"
|
||||
#~ msgstr "Poort"
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Controller"
|
||||
#~ msgstr "Controller"
|
||||
@@ -0,0 +1,565 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2026-05-25 08:28+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 18:11-3000\n"
|
||||
"Last-Translator: EDUARDO CARVALHO <eduardotcarvalho@gmail.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: Brazilian Portuguese\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr "Nova versão disponível! Clique aqui para baixar"
|
||||
|
||||
#: src/ui/gui/about_page.slint:33
|
||||
msgctxt "About"
|
||||
msgid "About"
|
||||
msgstr "Sobre"
|
||||
|
||||
#: src/ui/gui/about_page.slint:40
|
||||
msgctxt "About"
|
||||
msgid "Wiki"
|
||||
msgstr "Wiki"
|
||||
|
||||
#: src/ui/gui/about_page.slint:50
|
||||
msgctxt "About"
|
||||
msgid "Discord Server"
|
||||
msgstr "Servidor do Discord"
|
||||
|
||||
#: src/ui/gui/about_page.slint:60
|
||||
msgctxt "About"
|
||||
msgid "Source Code"
|
||||
msgstr "Código Fonte"
|
||||
|
||||
#: src/ui/gui/about_page.slint:70
|
||||
msgctxt "About"
|
||||
msgid "Donate (via Patreon)"
|
||||
msgstr "Doações (via Patreon)"
|
||||
|
||||
#: src/ui/gui/about_page.slint:77
|
||||
msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Doações (via GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:151
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Jogo Local"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:155
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Configurar Controle"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:159
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:163
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Trapaças"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:167
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:171
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Configurações"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:175
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "Sobre"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:15
|
||||
msgctxt "Cheats"
|
||||
msgid "Cheats"
|
||||
msgstr "Trapaças"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:25
|
||||
msgctxt "Cheats"
|
||||
msgid "Select ROM"
|
||||
msgstr "Selecionar ROM"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:41
|
||||
msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Limpar Trapaças para todas as ROMs"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:88
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "Sem trapaças disponíveis"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:80
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Enabled"
|
||||
msgstr "Habilitado"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:93
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Transfer Pak"
|
||||
msgstr "Transfer Pak"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:107
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Profile"
|
||||
msgstr "Perfil"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:157
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Configuration"
|
||||
msgstr "Configurar Controles"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:171
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Configure Input Profile"
|
||||
msgstr "Configurar Perfil de Controle"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:180
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Settings"
|
||||
msgstr "Configurações do controle"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:190
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Assignment"
|
||||
msgstr "Atribuição do controle"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:12
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Create/Modify Input Profile"
|
||||
msgstr "Criar/Modificar Perfil de Comandos"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:25
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Enter profile name to create or modify"
|
||||
msgstr "Insira o nome de perfil para criar ou modificar"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:38
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Deadzone Percentage"
|
||||
msgstr "Porcentagem de Zona-morta"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:61
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Use DirectInput"
|
||||
msgstr "Usar DirectInput"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:74
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Start Profile Creation Wizard"
|
||||
msgstr "Iniciar Assistente de Criação de Perfil"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:82
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:19
|
||||
msgctxt "LocalGame"
|
||||
msgid "Local Game"
|
||||
msgstr "Jogo Local"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:26
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open ROM"
|
||||
msgstr "Abrir ROM"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:37
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open Saves Folder"
|
||||
msgstr "Abrir Partes de Saves"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:54
|
||||
msgctxt "LocalGame"
|
||||
msgid "Recent ROMs"
|
||||
msgstr "ROMs Recentes"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:7
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Netplay: Criar Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:23
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nome da Sessão:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:27
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Insira nome da sessão:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:37
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Senha: (Opcional):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:41
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Insira a senha da sessão"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:52
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Nome de Jogador:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:56
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter player name"
|
||||
msgstr "Insira nome de jogador"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:66
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Server:"
|
||||
msgstr "Servidor:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:86
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Select ROM"
|
||||
msgstr "Selecionar ROM"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:102
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr ""
|
||||
"Este jogo tem trapaças habilitadas. As trapaças serão sincronizadas com "
|
||||
"outros jogadores."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:112
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Criar Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:132
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:155
|
||||
msgctxt "CustomServerDialog"
|
||||
msgid "Enter a custom server URL"
|
||||
msgstr "Insira a URL personalizada do servidor"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:7
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Netplay: Join Session"
|
||||
msgstr "Netplay: Entrar na sessão"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:18
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Player Name (required):"
|
||||
msgstr "Nome de Jogador (obrigatório):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:22
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Enter player name"
|
||||
msgstr "Insira o nome de jogador"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:31
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password (if required)"
|
||||
msgstr "Senha (caso obrigatória)"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:41
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Custom Server URL (optional):"
|
||||
msgstr "URL Personalizada de Servidor (opcional):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:53
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "Selecionar ROM"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:68
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Recarregando lista de sessões, por favor aguarde..."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr "Clique em uma sessão abaixo para selecionar"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:78
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "No sessions available. Please refresh to check again."
|
||||
msgstr ""
|
||||
"Sem sessões disponíveis. Por favor recarregue para verificar novamente."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:84
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Server"
|
||||
msgstr "Servidor"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:85
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Nome da Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:86
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Nome do Jogo"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:87
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Senha"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:88
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Trapaças"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:102
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Recarregar Lista de Sessões"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:119
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Entrar na Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:132
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:37
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:45
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Criar Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:56
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Entrar na Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:70
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Canal de Netplay do Discord"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:77
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Fornecer Feedback"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:15
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Netplay: Aguardando Sala"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:27
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nome da Sessão:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:33
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Game Name:"
|
||||
msgstr "Nome do Jogo:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:39
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Ping:"
|
||||
msgstr "Ping:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:48
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Jogador 1"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:52
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Jogador 2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:58
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Jogador 3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:62
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Jogador 4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:76
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Escrever mensagem no chat"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:87
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Enviar Mensagem"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:105
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Iniciar jogo"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:113
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:19
|
||||
msgctxt "RAData"
|
||||
msgid "Not currently logged in"
|
||||
msgstr "Não está logado."
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:27
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:36
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Enabled"
|
||||
msgstr "Habilitado"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:51
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Hardcore mode"
|
||||
msgstr "Modo Hardcore"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:60
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Challenge Indicators"
|
||||
msgstr "Indicadores de Desafio"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:69
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Leaderboard Trackers"
|
||||
msgstr "Rastreadores de Tabela"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:78
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Discord Rich Presence"
|
||||
msgstr "Rich Presence no Discord"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:96
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Profile"
|
||||
msgstr "Ver Perfil"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:106
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Username"
|
||||
msgstr "Usuário"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:116
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Password"
|
||||
msgstr "Senha"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:125
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Login"
|
||||
msgstr "Login"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:141
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Ver Jogos com Suporte"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:23
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Configurações"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:34
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Resolução:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:48
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "Redução de SSAA"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:56
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Proporção Íntegra"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:64
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Tela Cheia"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:72
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "Widescreen (esticar)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:80
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "VSync"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:88
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "Aplicar filtro de CRT"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:100
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "Overclock da CPU do N64 (pode causar bugs)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:108
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Desabilitar Expansion Pak"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:116
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "Emular UNFLoader"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:124
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "Emular VRU"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "O que você quer falar?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
#~ "Criando servidor, por favor aguarde...Isto poderá levar até 30 segundos."
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Port"
|
||||
#~ msgstr "Entrada"
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Controller"
|
||||
#~ msgstr "Controle"
|
||||
@@ -0,0 +1,564 @@
|
||||
# gopher64 - Russian Translation
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-05-25 08:28+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Russian\n"
|
||||
"Language: ru\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
msgid "New version available! Click here to download"
|
||||
msgstr "Доступна новая версия! Нажмите здесь для загрузки"
|
||||
|
||||
#: src/ui/gui/about_page.slint:33
|
||||
msgctxt "About"
|
||||
msgid "About"
|
||||
msgstr "О программе"
|
||||
|
||||
#: src/ui/gui/about_page.slint:40
|
||||
msgctxt "About"
|
||||
msgid "Wiki"
|
||||
msgstr "Вики"
|
||||
|
||||
#: src/ui/gui/about_page.slint:50
|
||||
msgctxt "About"
|
||||
msgid "Discord Server"
|
||||
msgstr "Сервер Discord"
|
||||
|
||||
#: src/ui/gui/about_page.slint:60
|
||||
msgctxt "About"
|
||||
msgid "Source Code"
|
||||
msgstr "Исходный код"
|
||||
|
||||
#: src/ui/gui/about_page.slint:70
|
||||
msgctxt "About"
|
||||
msgid "Donate (via Patreon)"
|
||||
msgstr "Пожертвовать (через Patreon)"
|
||||
|
||||
#: src/ui/gui/about_page.slint:77
|
||||
msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Пожертвовать (через GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:151
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Локальная игра"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:155
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Настройка контроллера"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:159
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Сетевая игра"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:163
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Читы"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:167
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:171
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Настройки"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:175
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "О программе"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:15
|
||||
msgctxt "Cheats"
|
||||
msgid "Cheats"
|
||||
msgstr "Читы"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:25
|
||||
msgctxt "Cheats"
|
||||
msgid "Select ROM"
|
||||
msgstr "Выбрать ROM"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:41
|
||||
msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Очистить читы для всех ROMов"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:88
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "Читы недоступны"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:80
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Enabled"
|
||||
msgstr "Включено"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:93
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Transfer Pak"
|
||||
msgstr "Transfer Pak"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:107
|
||||
msgctxt "ControllerSettingsGrid"
|
||||
msgid "Profile"
|
||||
msgstr "Профиль"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:157
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Configuration"
|
||||
msgstr "Настройка контроллера"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:171
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Configure Input Profile"
|
||||
msgstr "Настроить профиль ввода"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:180
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Settings"
|
||||
msgstr "Настройки контроллера"
|
||||
|
||||
#: src/ui/gui/controller_page.slint:190
|
||||
msgctxt "ControllerConfig"
|
||||
msgid "Controller Assignment"
|
||||
msgstr "Назначение контроллера"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:12
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Create/Modify Input Profile"
|
||||
msgstr "Создать/изменить профиль ввода"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:25
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Enter profile name to create or modify"
|
||||
msgstr "Введите имя профиля для создания или изменения"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:38
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Deadzone Percentage"
|
||||
msgstr "Процент мёртвой зоны"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:61
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Use DirectInput"
|
||||
msgstr "Использовать DirectInput"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:74
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Start Profile Creation Wizard"
|
||||
msgstr "Запустить мастер создания профиля"
|
||||
|
||||
#: src/ui/gui/input_profile.slint:82
|
||||
msgctxt "InputProfileDialog"
|
||||
msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:19
|
||||
msgctxt "LocalGame"
|
||||
msgid "Local Game"
|
||||
msgstr "Локальная игра"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:26
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open ROM"
|
||||
msgstr "Открыть ROM"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:37
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open Saves Folder"
|
||||
msgstr "Открыть папку сохранений"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:54
|
||||
msgctxt "LocalGame"
|
||||
msgid "Recent ROMs"
|
||||
msgstr "Недавние ROMы"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:7
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Сетевая игра: Создать сессию"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:23
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Имя сессии:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:27
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Введите имя сессии"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:37
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Пароль (необязательно):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:41
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Введите пароль сессии"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:52
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Имя игрока:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:56
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter player name"
|
||||
msgstr "Введите имя игрока"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:66
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Server:"
|
||||
msgstr "Сервер:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:86
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Select ROM"
|
||||
msgstr "Выбрать ROM"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:102
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr ""
|
||||
"Для этой игры включены читы. Читы будут синхронизированы с другими игроками."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:112
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Создать сессию"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:132
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:155
|
||||
msgctxt "CustomServerDialog"
|
||||
msgid "Enter a custom server URL"
|
||||
msgstr "Введите URL пользовательского сервера"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:7
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Netplay: Join Session"
|
||||
msgstr "Сетевая игра: Присоединиться к сессии"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:18
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Player Name (required):"
|
||||
msgstr "Имя игрока (обязательно):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:22
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Enter player name"
|
||||
msgstr "Введите имя игрока"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:31
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password (if required)"
|
||||
msgstr "Пароль (если требуется)"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:41
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Custom Server URL (optional):"
|
||||
msgstr "URL пользовательского сервера (необязательно):"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:53
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "Выбрать ROM"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:68
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Обновление списка сессий, пожалуйста, подождите..."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr "Нажмите на сессию ниже, чтобы выбрать её"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:78
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "No sessions available. Please refresh to check again."
|
||||
msgstr "Нет доступных сессий. Пожалуйста, обновите, чтобы проверить снова."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:84
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Server"
|
||||
msgstr "Сервер"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:85
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Имя сессии"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:86
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Название игры"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:87
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Пароль"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:88
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Читы"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:102
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Обновить список сессий"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:119
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Присоединиться к сессии"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:132
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:37
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Сетевая игра"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:45
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Создать сессию"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:56
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Присоединиться к сессии"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:70
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Канал Discord для сетевой игры"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:77
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Оставить отзыв"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:15
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Сетевая игра: Комната ожидания"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:27
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Имя сессии:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:33
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Game Name:"
|
||||
msgstr "Название игры:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:39
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Ping:"
|
||||
msgstr "Пинг:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:48
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Игрок 1:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:52
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Игрок 2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:58
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Игрок 3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:62
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Игрок 4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:76
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Введите сообщение чата"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:87
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Отправить сообщение"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:105
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Начать игру"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:113
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:19
|
||||
msgctxt "RAData"
|
||||
msgid "Not currently logged in"
|
||||
msgstr "В данный момент не выполнен вход"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:27
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:36
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Enabled"
|
||||
msgstr "Включено"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:51
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Hardcore mode"
|
||||
msgstr "Хардкорный режим"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:60
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Challenge Indicators"
|
||||
msgstr "Индикаторы испытаний"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:69
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Leaderboard Trackers"
|
||||
msgstr "Трекеры таблицы лидеров"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:78
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Discord Rich Presence"
|
||||
msgstr "Расширенный статус Discord"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:96
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Profile"
|
||||
msgstr "Просмотреть профиль"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:106
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Username"
|
||||
msgstr "Имя пользователя"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:116
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Password"
|
||||
msgstr "Пароль"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:125
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "Login"
|
||||
msgstr "Войти"
|
||||
|
||||
#: src/ui/gui/retroachievements_page.slint:141
|
||||
msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Просмотреть поддерживаемые игры"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:23
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Настройки"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:34
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Разрешение:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:48
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "SSAA даунскейлинг"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:56
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Целочисленное масштабирование"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:64
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Полноэкранный режим"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:72
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "Широкоэкранный режим (растянуто)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:80
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "Вертикальная синхронизация"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:88
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "Применить шейдер ЭЛТ"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:100
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "Разгон процессора N64 (может вызвать ошибки)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:108
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Отключить Expansion Pak"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:116
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "Эмулировать UNFLoader"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:124
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "Эмулировать VRU"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "Что вы хотите сказать?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
#~ "Создание сервера, пожалуйста, подождите... Это может занять около 30 "
|
||||
#~ "секунд."
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Port"
|
||||
#~ msgstr "Порт"
|
||||
|
||||
#~ msgctxt "ControllerConfig"
|
||||
#~ msgid "Controller"
|
||||
#~ msgstr "Контроллер"
|
||||
|
After Width: | Height: | Size: 1.0 MiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M440-280h80v-240h-80v240Zm68.5-331.5Q520-623 520-640t-11.5-28.5Q497-680 480-680t-28.5 11.5Q440-657 440-640t11.5 28.5Q463-600 480-600t28.5-11.5ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
||||
|
After Width: | Height: | Size: 553 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M280-120v-80h160v-124q-49-11-87.5-41.5T296-442q-75-9-125.5-65.5T120-640v-40q0-33 23.5-56.5T200-760h80v-80h400v80h80q33 0 56.5 23.5T840-680v40q0 76-50.5 132.5T664-442q-18 46-56.5 76.5T520-324v124h160v80H280Zm0-408v-152h-80v40q0 38 22 68.5t58 43.5Zm285 93q35-35 35-85v-240H360v240q0 50 35 85t85 35q50 0 85-35Zm115-93q36-13 58-43.5t22-68.5v-40h-80v152Zm-200-52Z"/></svg>
|
||||
|
After Width: | Height: | Size: 483 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M480-160v-80h320v80H480ZM220-320l-56-56 183-184-183-184 56-56 240 240-240 240Z"/></svg>
|
||||
|
After Width: | Height: | Size: 203 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M189-160q-60 0-102.5-43T42-307q0-9 1-18t3-18l84-336q14-54 57-87.5t98-33.5h390q55 0 98 33.5t57 87.5l84 336q2 9 3.5 18.5T919-306q0 61-43.5 103.5T771-160q-42 0-78-22t-54-60l-28-58q-5-10-15-15t-21-5H385q-11 0-21 5t-15 15l-28 58q-18 38-54 60t-78 22Zm3-80q19 0 34.5-10t23.5-27l28-57q15-31 44-48.5t63-17.5h190q34 0 63 18t45 48l28 57q8 17 23.5 27t34.5 10q28 0 48-18.5t21-46.5q0 1-2-19l-84-335q-7-27-28-44t-49-17H285q-28 0-49.5 17T208-659l-84 335q-2 6-2 18 0 28 20.5 47t49.5 19Zm376.5-291.5Q580-543 580-560t-11.5-28.5Q557-600 540-600t-28.5 11.5Q500-577 500-560t11.5 28.5Q523-520 540-520t28.5-11.5Zm80-80Q660-623 660-640t-11.5-28.5Q637-680 620-680t-28.5 11.5Q580-657 580-640t11.5 28.5Q603-600 620-600t28.5-11.5Zm0 160Q660-463 660-480t-11.5-28.5Q637-520 620-520t-28.5 11.5Q580-497 580-480t11.5 28.5Q603-440 620-440t28.5-11.5Zm80-80Q740-543 740-560t-11.5-28.5Q717-600 700-600t-28.5 11.5Q660-577 660-560t11.5 28.5Q683-520 700-520t28.5-11.5Zm-367 63Q370-477 370-490v-40h40q13 0 21.5-8.5T440-560q0-13-8.5-21.5T410-590h-40v-40q0-13-8.5-21.5T340-660q-13 0-21.5 8.5T310-630v40h-40q-13 0-21.5 8.5T240-560q0 13 8.5 21.5T270-530h40v40q0 13 8.5 21.5T340-460q13 0 21.5-8.5ZM480-480Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M240-200h120v-240h240v240h120v-360L480-740 240-560v360Zm-80 80v-480l320-240 320 240v480H520v-240h-80v240H160Zm320-350Z"/></svg>
|
||||
|
After Width: | Height: | Size: 243 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M260-160q-91 0-155.5-63T40-377q0-78 47-139t123-78q25-92 100-149t170-57q117 0 198.5 81.5T760-520q69 8 114.5 59.5T920-340q0 75-52.5 127.5T740-160H260Zm0-80h480q42 0 71-29t29-71q0-42-29-71t-71-29h-60v-80q0-83-58.5-141.5T480-720q-83 0-141.5 58.5T280-520h-20q-58 0-99 41t-41 99q0 58 41 99t99 41Zm220-240Z"/></svg>
|
||||
|
After Width: | Height: | Size: 424 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/></svg>
|
||||
|
After Width: | Height: | Size: 771 B |
@@ -54,6 +54,13 @@ enum vi_registers {
|
||||
VI_REGS_COUNT
|
||||
};
|
||||
|
||||
enum user_event_codes {
|
||||
USER_EVENT_SAVE_STATE = 1,
|
||||
USER_EVENT_LOAD_STATE = 2,
|
||||
USER_EVENT_EXIT_GAME = 3,
|
||||
USER_EVENT_FAST_FORWARD = 4,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
uint32_t depthbuffer_address;
|
||||
uint32_t framebuffer_address;
|
||||
@@ -98,8 +105,8 @@ static GFX_INFO gfx_info;
|
||||
static const uint32_t *fragment_spirv;
|
||||
static size_t fragment_size;
|
||||
|
||||
std::vector<bool> rdram_dirty;
|
||||
uint64_t sync_signal;
|
||||
static std::vector<bool> rdram_dirty;
|
||||
static uint64_t sync_signal;
|
||||
|
||||
static TTF_Font *message_font;
|
||||
static std::queue<Message> messages;
|
||||
@@ -114,6 +121,8 @@ static bool display_challenge_indicator;
|
||||
static bool display_fps;
|
||||
static Vulkan::ImageHandle fps_image;
|
||||
|
||||
static std::queue<JoystickEvent> joystick_events;
|
||||
|
||||
typedef struct {
|
||||
float SourceSize[4];
|
||||
float OutputSize[4];
|
||||
@@ -125,6 +134,12 @@ static const unsigned cmd_len_lut[64] = {
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
};
|
||||
|
||||
static void add_joystick_event(void *userdata) {
|
||||
JoystickEvent *joystick_event = (JoystickEvent *)userdata;
|
||||
joystick_events.push(*joystick_event);
|
||||
delete joystick_event;
|
||||
}
|
||||
|
||||
bool sdl_event_filter(void *userdata, SDL_Event *event) {
|
||||
if (event->type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
|
||||
callback.paused = false;
|
||||
@@ -132,6 +147,7 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
|
||||
} else if (event->type == SDL_EVENT_WINDOW_RESIZED && callback.emu_running) {
|
||||
wsi_platform->do_resize();
|
||||
} else if (event->type == SDL_EVENT_KEY_DOWN && !event->key.repeat) {
|
||||
SDL_Event user_event;
|
||||
switch (event->key.scancode) {
|
||||
case SDL_SCANCODE_RETURN:
|
||||
if (event->key.mod & SDL_KMOD_ALT) {
|
||||
@@ -141,7 +157,10 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
|
||||
break;
|
||||
case SDL_SCANCODE_F:
|
||||
if (event->key.mod & SDL_KMOD_ALT) {
|
||||
callback.enable_speedlimiter = !callback.enable_speedlimiter;
|
||||
SDL_zero(user_event);
|
||||
user_event.type = SDL_EVENT_USER;
|
||||
user_event.user.code = USER_EVENT_FAST_FORWARD;
|
||||
SDL_PushEvent(&user_event);
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_P:
|
||||
@@ -149,9 +168,14 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
|
||||
callback.paused = !callback.paused;
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_AC_BACK:
|
||||
case SDL_SCANCODE_ESCAPE:
|
||||
if (gfx_info.fullscreen)
|
||||
callback.emu_running = false;
|
||||
if (gfx_info.fullscreen) {
|
||||
SDL_zero(user_event);
|
||||
user_event.type = SDL_EVENT_USER;
|
||||
user_event.user.code = USER_EVENT_EXIT_GAME;
|
||||
SDL_PushEvent(&user_event);
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_F1:
|
||||
display_fps = !display_fps;
|
||||
@@ -160,10 +184,16 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
|
||||
crop_letterbox = !crop_letterbox;
|
||||
break;
|
||||
case SDL_SCANCODE_F5:
|
||||
callback.save_state = true;
|
||||
SDL_zero(user_event);
|
||||
user_event.type = SDL_EVENT_USER;
|
||||
user_event.user.code = USER_EVENT_SAVE_STATE;
|
||||
SDL_PushEvent(&user_event);
|
||||
break;
|
||||
case SDL_SCANCODE_F7:
|
||||
callback.load_state = true;
|
||||
SDL_zero(user_event);
|
||||
user_event.type = SDL_EVENT_USER;
|
||||
user_event.user.code = USER_EVENT_LOAD_STATE;
|
||||
SDL_PushEvent(&user_event);
|
||||
break;
|
||||
case SDL_SCANCODE_F8:
|
||||
if (messages.empty())
|
||||
@@ -175,7 +205,7 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
|
||||
std::format("Challenge indicators: {}",
|
||||
display_challenge_indicator ? "ON" : "OFF")
|
||||
.c_str(),
|
||||
MESSAGE_SHORT);
|
||||
MESSAGE_VERY_SHORT);
|
||||
break;
|
||||
case SDL_SCANCODE_F12:
|
||||
callback.reset_game = true;
|
||||
@@ -209,20 +239,53 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (event->type == SDL_EVENT_USER) {
|
||||
switch (event->user.code) {
|
||||
case USER_EVENT_SAVE_STATE:
|
||||
callback.save_state = true;
|
||||
break;
|
||||
case USER_EVENT_LOAD_STATE:
|
||||
callback.load_state = true;
|
||||
break;
|
||||
case USER_EVENT_EXIT_GAME:
|
||||
callback.emu_running = false;
|
||||
break;
|
||||
case USER_EVENT_FAST_FORWARD:
|
||||
callback.enable_speedlimiter = !callback.enable_speedlimiter;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (event->type == SDL_EVENT_JOYSTICK_ADDED) {
|
||||
JoystickEvent *joystick_event = new JoystickEvent;
|
||||
joystick_event->joystick_id = event->jdevice.which;
|
||||
joystick_event->connected = true;
|
||||
SDL_RunOnMainThread(add_joystick_event, joystick_event, false);
|
||||
} else if (event->type == SDL_EVENT_JOYSTICK_REMOVED) {
|
||||
JoystickEvent *joystick_event = new JoystickEvent;
|
||||
joystick_event->joystick_id = event->jdevice.which;
|
||||
joystick_event->connected = false;
|
||||
SDL_RunOnMainThread(add_joystick_event, joystick_event, false);
|
||||
} else if (event->type == SDL_EVENT_WILL_ENTER_BACKGROUND) {
|
||||
wsi->end_frame();
|
||||
wsi->deinit_surface_and_swapchain();
|
||||
} else if (event->type == SDL_EVENT_RENDER_DEVICE_RESET) {
|
||||
wsi->init_surface_swapchain();
|
||||
wsi->begin_frame();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void rdp_new_processor(GFX_INFO _gfx_info) {
|
||||
gfx_info = _gfx_info;
|
||||
JoystickEvent get_joystick_event() {
|
||||
if (joystick_events.empty())
|
||||
return JoystickEvent{0, false};
|
||||
JoystickEvent joystick_event = joystick_events.front();
|
||||
joystick_events.pop();
|
||||
return joystick_event;
|
||||
}
|
||||
|
||||
sync_signal = 0;
|
||||
rdram_dirty.assign(gfx_info.RDRAM_SIZE >> 3, false);
|
||||
|
||||
if (processor) {
|
||||
delete processor;
|
||||
}
|
||||
static void rdp_new_processor() {
|
||||
RDP::CommandProcessorFlags flags = 0;
|
||||
|
||||
if (gfx_info.upscale == 2) {
|
||||
@@ -234,6 +297,8 @@ void rdp_new_processor(GFX_INFO _gfx_info) {
|
||||
} else if (gfx_info.upscale == 8) {
|
||||
flags |= RDP::COMMAND_PROCESSOR_FLAG_SUPER_SAMPLED_DITHER_BIT;
|
||||
flags |= RDP::COMMAND_PROCESSOR_FLAG_UPSCALING_8X_BIT;
|
||||
} else {
|
||||
gfx_info.upscale = 1;
|
||||
}
|
||||
|
||||
processor = new RDP::CommandProcessor(wsi->get_device(), gfx_info.RDRAM, 0,
|
||||
@@ -307,7 +372,7 @@ void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
|
||||
return;
|
||||
}
|
||||
|
||||
rdp_new_processor(gfx_info);
|
||||
rdp_new_processor();
|
||||
|
||||
if (!processor->device_is_supported()) {
|
||||
rdp_close();
|
||||
@@ -340,9 +405,14 @@ void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
|
||||
achievement_progress_indicator_image = Vulkan::ImageHandle();
|
||||
fps_image = Vulkan::ImageHandle();
|
||||
display_fps = false;
|
||||
|
||||
sync_signal = 0;
|
||||
rdram_dirty.assign(gfx_info.RDRAM_SIZE >> 3, false);
|
||||
}
|
||||
|
||||
void rdp_close() {
|
||||
display_fps = false;
|
||||
|
||||
messages = std::queue<Message>();
|
||||
achievement_challenge_indicator_image = Vulkan::ImageHandle();
|
||||
achievement_progress_indicator_image = Vulkan::ImageHandle();
|
||||
@@ -371,6 +441,8 @@ void rdp_close() {
|
||||
delete wsi_platform;
|
||||
wsi_platform = nullptr;
|
||||
}
|
||||
|
||||
SDL_RemoveEventWatch(sdl_event_filter, nullptr);
|
||||
}
|
||||
|
||||
static void calculate_viewport(float *x, float *y, float *width, float *height,
|
||||
@@ -451,8 +523,24 @@ static uint32_t pop_message_callback(void *userdata, SDL_TimerID timerID,
|
||||
static void render_frame(Vulkan::Device &device) {
|
||||
RDP::ScanoutOptions options = {};
|
||||
options.persist_frame_on_invalid_input = true;
|
||||
options.blend_previous_frame = true;
|
||||
options.upscale_deinterlacing = false;
|
||||
options.blend_previous_frame = !gfx_info.ssaa;
|
||||
options.upscale_deinterlacing = gfx_info.ssaa;
|
||||
if (gfx_info.ssaa) {
|
||||
switch (gfx_info.upscale) {
|
||||
case 2:
|
||||
options.downscale_steps = 1;
|
||||
break;
|
||||
case 4:
|
||||
options.downscale_steps = 2;
|
||||
break;
|
||||
case 8:
|
||||
options.downscale_steps = 3;
|
||||
break;
|
||||
default:
|
||||
options.downscale_steps = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (crop_letterbox && gfx_info.widescreen) {
|
||||
options.crop_rect.enable = true;
|
||||
@@ -561,6 +649,9 @@ void rdp_render_frame() {
|
||||
}
|
||||
|
||||
void rdp_update_screen() {
|
||||
if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED) {
|
||||
return;
|
||||
}
|
||||
wsi->end_frame();
|
||||
wsi->begin_frame();
|
||||
}
|
||||
@@ -604,7 +695,11 @@ void rdp_save_state(uint8_t *state) {
|
||||
memcpy(state, &rdp_device, sizeof(RDP_DEVICE));
|
||||
}
|
||||
|
||||
void rdp_load_state(const uint8_t *state) {
|
||||
void rdp_load_state(GFX_INFO _gfx_info, const uint8_t *state) {
|
||||
sync_signal = 0;
|
||||
rdram_dirty.assign(gfx_info.RDRAM_SIZE >> 3, false);
|
||||
|
||||
gfx_info = _gfx_info;
|
||||
memcpy(&rdp_device, state, sizeof(RDP_DEVICE));
|
||||
}
|
||||
|
||||
@@ -853,6 +948,10 @@ uint64_t rdp_process_commands() {
|
||||
|
||||
static void rdp_set_fps_callback(void *userdata) {
|
||||
FPS_DATA *data = (FPS_DATA *)userdata;
|
||||
if (!wsi) {
|
||||
delete data;
|
||||
return;
|
||||
}
|
||||
fps_image = create_message_image(
|
||||
wsi->get_device(), 0, achievement_challenge_indicator_font,
|
||||
std::format("FPS: {} VI/S: {}", data->fps, data->vis).c_str());
|
||||
|
||||
@@ -21,6 +21,7 @@ typedef struct {
|
||||
bool vsync;
|
||||
bool integer_scaling;
|
||||
uint32_t upscale;
|
||||
bool ssaa;
|
||||
bool crt;
|
||||
} GFX_INFO;
|
||||
|
||||
@@ -37,6 +38,11 @@ typedef struct {
|
||||
uint32_t save_state_slot;
|
||||
} CALL_BACK;
|
||||
|
||||
typedef struct {
|
||||
uint32_t joystick_id;
|
||||
bool connected;
|
||||
} JoystickEvent;
|
||||
|
||||
typedef enum {
|
||||
MESSAGE_VERY_SHORT = 500,
|
||||
MESSAGE_SHORT = 3000,
|
||||
@@ -52,13 +58,14 @@ 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_load_state(GFX_INFO _gfx_info, const uint8_t *state);
|
||||
void rdp_set_fps(uint32_t fps, uint32_t vis);
|
||||
|
||||
JoystickEvent get_joystick_event();
|
||||
|
||||
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,
|
||||
|
||||
@@ -16,18 +16,20 @@ void store_retroachievements_credentials(const char *username,
|
||||
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 const uint8_t *g_rdram = NULL;
|
||||
static size_t g_rdram_size = 0;
|
||||
static bool g_challenge = false;
|
||||
static bool g_leaderboard = false;
|
||||
static rc_client_leaderboard_list_t *g_leaderboard_list = NULL;
|
||||
|
||||
static char g_rich_presence_buffer[512];
|
||||
|
||||
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)
|
||||
if (address + num_bytes > g_rdram_size)
|
||||
memset(buffer, 0, num_bytes);
|
||||
else
|
||||
memcpy(buffer, &g_dmem[address], num_bytes);
|
||||
memcpy(buffer, &g_rdram[address], num_bytes);
|
||||
return num_bytes;
|
||||
}
|
||||
|
||||
@@ -84,10 +86,6 @@ 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); }
|
||||
|
||||
@@ -128,9 +126,12 @@ static void load_game_callback(int result, const char *error_message,
|
||||
notify_load_game(userdata);
|
||||
}
|
||||
|
||||
void ra_welcome() {
|
||||
if (!g_client)
|
||||
void ra_welcome(const char **game_title, const char **game_image_url) {
|
||||
if (!g_client) {
|
||||
*game_title = NULL;
|
||||
*game_image_url = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
char buffer[512];
|
||||
|
||||
@@ -140,10 +141,18 @@ void ra_welcome() {
|
||||
rdp_onscreen_message(buffer, MESSAGE_LONG);
|
||||
free(error_message);
|
||||
rc_client_set_userdata(g_client, NULL);
|
||||
|
||||
*game_title = NULL;
|
||||
*game_image_url = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
const rc_client_game_t *game = rc_client_get_game_info(g_client);
|
||||
if (!game) {
|
||||
*game_title = NULL;
|
||||
*game_image_url = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
rc_client_user_game_summary_t summary;
|
||||
rc_client_get_user_game_summary(g_client, &summary);
|
||||
@@ -161,6 +170,9 @@ void ra_welcome() {
|
||||
"Game has no achievements");
|
||||
}
|
||||
rdp_onscreen_message(buffer, MESSAGE_LONG);
|
||||
|
||||
*game_title = game->title;
|
||||
*game_image_url = game->badge_url;
|
||||
}
|
||||
|
||||
void ra_load_game(const uint8_t *rom, size_t rom_size, void *userdata) {
|
||||
@@ -177,9 +189,9 @@ void ra_unload_game() {
|
||||
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;
|
||||
void ra_set_rdram(const uint8_t *rdram, size_t rdram_size) {
|
||||
g_rdram = rdram;
|
||||
g_rdram_size = rdram_size;
|
||||
}
|
||||
|
||||
static void leaderboard_submitted(const rc_client_leaderboard_t *leaderboard) {
|
||||
@@ -315,9 +327,10 @@ static void event_handler(const rc_client_event_t *event, rc_client_t *client) {
|
||||
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;
|
||||
if (!g_client)
|
||||
g_client = rc_client_create(read_memory, server_call);
|
||||
g_rdram = NULL;
|
||||
g_rdram_size = 0;
|
||||
|
||||
// Provide a logging function to simplify debugging
|
||||
rc_client_enable_logging(g_client, RC_CLIENT_LOG_LEVEL_WARN, log_message);
|
||||
@@ -344,6 +357,15 @@ void ra_do_frame() { rc_client_do_frame(g_client); }
|
||||
|
||||
void ra_do_idle() { rc_client_idle(g_client); }
|
||||
|
||||
const char *ra_get_rich_presence() {
|
||||
size_t size = rc_client_get_rich_presence_message(
|
||||
g_client, g_rich_presence_buffer, sizeof(g_rich_presence_buffer));
|
||||
if (size == 0) {
|
||||
return NULL;
|
||||
}
|
||||
return g_rich_presence_buffer;
|
||||
}
|
||||
|
||||
size_t ra_state_size() { return rc_client_progress_size(g_client); }
|
||||
|
||||
void ra_save_state(uint8_t *state, size_t state_size) {
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
void ra_init_client(bool hardcore, bool challenge, bool leaderboard);
|
||||
void ra_shutdown_client();
|
||||
void ra_welcome();
|
||||
void ra_welcome(const char **game_title, const char **game_image_url);
|
||||
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_set_rdram(const uint8_t *rdram, size_t rdram_size);
|
||||
void ra_do_frame();
|
||||
void ra_do_idle();
|
||||
void ra_http_callback(const char *content, size_t content_size, int status_code,
|
||||
@@ -18,11 +18,12 @@ 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);
|
||||
|
||||
const char *ra_get_rich_presence();
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "1.95.0"
|
||||
channel = "1.96.0"
|
||||
|
||||
@@ -48,7 +48,8 @@ pub fn init(
|
||||
{
|
||||
option_value = Some(found_option_value.to_string());
|
||||
} else {
|
||||
panic!("Cheat option: {option} not found");
|
||||
eprintln!("Cheat option: {option} not found");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,15 +66,21 @@ pub fn init(
|
||||
}
|
||||
let mut split = result.split_whitespace();
|
||||
let first_part = u32::from_str_radix(split.next().unwrap(), 16).unwrap();
|
||||
decoded_cheat.push(DecodedCheat {
|
||||
code_type: (first_part >> 24) as u8,
|
||||
address: first_part & 0x00FFFFFF,
|
||||
data: u16::from_str_radix(split.next().unwrap(), 16).unwrap(),
|
||||
});
|
||||
if let Ok(data) = u16::from_str_radix(split.next().unwrap(), 16) {
|
||||
decoded_cheat.push(DecodedCheat {
|
||||
code_type: (first_part >> 24) as u8,
|
||||
address: first_part & 0x00FFFFFF,
|
||||
data,
|
||||
})
|
||||
} else {
|
||||
eprintln!("Could not parse data for: {}", cheat_setting.0);
|
||||
};
|
||||
}
|
||||
if !decoded_cheat.is_empty() {
|
||||
device.cheats.cheats.push(decoded_cheat);
|
||||
}
|
||||
device.cheats.cheats.push(decoded_cheat);
|
||||
} else {
|
||||
println!("Could not find cheat: {}", cheat_setting.0);
|
||||
eprintln!("Could not find cheat: {}", cheat_setting.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,13 @@ include!(concat!(env!("OUT_DIR"), "/simd_bindings.rs"));
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
use std::arch::x86_64::*;
|
||||
|
||||
const N64_EXTENSIONS_UNCOMPRESSED: [&str; 8] =
|
||||
["n64", "v64", "z64", "bin", "N64", "V64", "Z64", "BIN"];
|
||||
|
||||
use rand::{Rng, SeedableRng};
|
||||
|
||||
use crate::{cheats, netplay, retroachievements, ui};
|
||||
use std::{collections::HashMap, fs, io::Read};
|
||||
use std::{collections::HashMap, io::Read};
|
||||
|
||||
pub mod ai;
|
||||
pub mod cache;
|
||||
@@ -44,7 +47,12 @@ pub mod tlb;
|
||||
pub mod unmapped;
|
||||
pub mod vi;
|
||||
|
||||
pub fn run_game(device: &mut Device, rom_contents: &[u8], game_settings: ui::GameSettings) {
|
||||
pub async fn run_game(
|
||||
device: &mut Device,
|
||||
rom_contents: &[u8],
|
||||
game_settings: ui::GameSettings,
|
||||
ra_config: retroachievements::RAConfig,
|
||||
) {
|
||||
device.cpu.overclock = game_settings.overclock;
|
||||
if game_settings.disable_expansion_pak {
|
||||
device.rdram.size = 0x400000;
|
||||
@@ -62,11 +70,16 @@ pub fn run_game(device: &mut Device, rom_contents: &[u8], game_settings: ui::Gam
|
||||
rdram::init(device);
|
||||
|
||||
ui::video::init(device);
|
||||
ui::audio::init(&mut device.ui, device.ai.freq);
|
||||
ui::audio::init(device);
|
||||
ui::input::init(&mut device.ui);
|
||||
|
||||
// must be after video init
|
||||
retroachievements::welcome();
|
||||
let (discord_watch_tx, discord_handle) = if ra_config.enabled && device.netplay.is_none() {
|
||||
retroachievements::load_game(rom_contents, rom_contents.len(), ra_config.rich_presence)
|
||||
.await
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
mi::init(device);
|
||||
pif::init(device);
|
||||
@@ -82,6 +95,7 @@ pub fn run_game(device: &mut Device, rom_contents: &[u8], game_settings: ui::Gam
|
||||
|
||||
ui::storage::init(&mut device.ui, &device.cart.rom);
|
||||
ui::storage::load_saves(&mut device.ui, &mut device.netplay);
|
||||
ui::storage::format_saves(device);
|
||||
|
||||
if !retroachievements::get_hardcore() {
|
||||
cheats::init(device, game_settings.cheats);
|
||||
@@ -89,7 +103,7 @@ pub fn run_game(device: &mut Device, rom_contents: &[u8], game_settings: ui::Gam
|
||||
|
||||
cpu::run(device);
|
||||
|
||||
retroachievements::unload_game();
|
||||
retroachievements::unload_game(discord_watch_tx, discord_handle).await;
|
||||
|
||||
ui::storage::write_saves(device);
|
||||
ui::input::close(&mut device.ui);
|
||||
@@ -139,24 +153,23 @@ fn swap_rom(contents: Vec<u8>) -> Option<Vec<u8>> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
|
||||
pub fn get_rom_contents(file_path: &std::path::PathBuf) -> Option<Vec<u8>> {
|
||||
let mut contents = vec![];
|
||||
if file_path
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.eq_ignore_ascii_case("zip")
|
||||
{
|
||||
let zip_file = fs::File::open(file_path).unwrap();
|
||||
#[cfg(target_os = "android")]
|
||||
let zip_file = ui::android::get_file_from_uri(file_path).unwrap();
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let zip_file = std::fs::File::open(file_path).unwrap();
|
||||
let mut archive = zip::ZipArchive::new(zip_file).unwrap();
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i).unwrap();
|
||||
let extension = file
|
||||
.enclosed_name()
|
||||
.unwrap()
|
||||
.extension()
|
||||
.unwrap()
|
||||
.to_ascii_lowercase();
|
||||
if extension == "z64" || extension == "n64" || extension == "v64" {
|
||||
if let Some(extension) = file.enclosed_name().unwrap().extension()
|
||||
&& N64_EXTENSIONS_UNCOMPRESSED.contains(&extension.to_str().unwrap())
|
||||
{
|
||||
file.read_to_end(&mut contents)
|
||||
.expect("could not read zip file");
|
||||
break;
|
||||
@@ -167,16 +180,20 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
|
||||
.unwrap_or_default()
|
||||
.eq_ignore_ascii_case("7z")
|
||||
{
|
||||
#[cfg(target_os = "android")]
|
||||
let sevenz_file = ui::android::get_file_from_uri(file_path).unwrap();
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let sevenz_file = std::fs::File::open(file_path).unwrap();
|
||||
let mut archive =
|
||||
sevenz_rust2::ArchiveReader::open(file_path, sevenz_rust2::Password::empty()).unwrap();
|
||||
sevenz_rust2::ArchiveReader::new(sevenz_file, sevenz_rust2::Password::empty()).unwrap();
|
||||
|
||||
let mut found = false;
|
||||
archive
|
||||
.for_each_entries(
|
||||
&mut |entry: &sevenz_rust2::ArchiveEntry, reader: &mut dyn std::io::Read| {
|
||||
let name = entry.name().to_ascii_lowercase();
|
||||
if !found
|
||||
&& (name.ends_with("z64") || name.ends_with("n64") || name.ends_with("v64"))
|
||||
&& let Some(extension) = std::path::PathBuf::from(entry.name()).extension()
|
||||
&& N64_EXTENSIONS_UNCOMPRESSED.contains(&extension.to_str().unwrap())
|
||||
{
|
||||
reader
|
||||
.read_to_end(&mut contents)
|
||||
@@ -191,8 +208,11 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
|
||||
)
|
||||
.expect("ok");
|
||||
} else {
|
||||
contents = fs::read(file_path)
|
||||
.unwrap_or_else(|_| panic!("Could not read ROM file: {}", file_path.display()));
|
||||
#[cfg(target_os = "android")]
|
||||
let mut file = ui::android::get_file_from_uri(file_path).unwrap();
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let mut file = std::fs::File::open(file_path).unwrap();
|
||||
file.read_to_end(&mut contents).unwrap();
|
||||
}
|
||||
|
||||
if contents.is_empty() {
|
||||
@@ -206,7 +226,7 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
|
||||
pub struct Device {
|
||||
#[serde(skip)]
|
||||
pub netplay: Option<netplay::Netplay>,
|
||||
#[serde(skip, default = "ui::Ui::default")]
|
||||
#[serde(skip, default = "ui::Ui::new")]
|
||||
pub ui: ui::Ui,
|
||||
pub byte_swap: usize,
|
||||
pub save_state: bool,
|
||||
@@ -252,8 +272,8 @@ impl Device {
|
||||
load_state: false,
|
||||
cpu: cpu::Cpu {
|
||||
cop0: cop0::Cop0 {
|
||||
regs: [0; cop0::COP0_REGS_COUNT as usize],
|
||||
reg_write_masks: [0; cop0::COP0_REGS_COUNT as usize],
|
||||
regs: [0; cop0::COP0_REGS_COUNT],
|
||||
reg_write_masks: [0; cop0::COP0_REGS_COUNT],
|
||||
reg_latch: 0,
|
||||
is_event: false,
|
||||
instrs: [cop0::reserved; 32],
|
||||
@@ -357,9 +377,9 @@ impl Device {
|
||||
rtc_timestamp: 0,
|
||||
rtc: cart::AfRtc { control: 0x0200 },
|
||||
sc64: cart::sc64::Sc64 {
|
||||
regs: [0; cart::sc64::SC64_REGS_COUNT as usize],
|
||||
regs: [0; cart::sc64::SC64_REGS_COUNT],
|
||||
regs_locked: true,
|
||||
cfg: [0; cart::sc64::SC64_CFG_COUNT as usize],
|
||||
cfg: [0; cart::sc64::SC64_CFG_COUNT],
|
||||
sector: 0,
|
||||
buffer: vec![0; 8192],
|
||||
writeback_sector: vec![0; 256],
|
||||
@@ -395,7 +415,7 @@ impl Device {
|
||||
rdram: rdram::Rdram {
|
||||
mem: vec![],
|
||||
size: 0x800000,
|
||||
regs: [[0; rdram::RDRAM_REGS_COUNT as usize]; 4],
|
||||
regs: [[0; rdram::RDRAM_REGS_COUNT]; 4],
|
||||
},
|
||||
rsp: rsp_interface::Rsp {
|
||||
cpu: rsp_cpu::Cpu {
|
||||
@@ -440,8 +460,8 @@ impl Device {
|
||||
instrs: [rsp_su_instructions::reserved; 64],
|
||||
vec_instrs: [rsp_vu_instructions::reserved; 64],
|
||||
},
|
||||
regs: [0; rsp_interface::SP_REGS_COUNT as usize],
|
||||
regs2: [0; rsp_interface::SP_REGS2_COUNT as usize],
|
||||
regs: [0; rsp_interface::SP_REGS_COUNT],
|
||||
regs2: [0; rsp_interface::SP_REGS2_COUNT],
|
||||
mem: [0; 0x2000],
|
||||
last_status_value: 0,
|
||||
run_after_dma: false,
|
||||
@@ -453,19 +473,19 @@ impl Device {
|
||||
}; 2],
|
||||
},
|
||||
rdp: rdp::Rdp {
|
||||
regs_dpc: [0; rdp::DPC_REGS_COUNT as usize],
|
||||
regs_dps: [0; rdp::DPS_REGS_COUNT as usize],
|
||||
regs_dpc: [0; rdp::DPC_REGS_COUNT],
|
||||
regs_dps: [0; rdp::DPS_REGS_COUNT],
|
||||
wait_frozen: false,
|
||||
last_status_value: 0,
|
||||
},
|
||||
mi: mi::Mi {
|
||||
regs: [0; mi::MI_REGS_COUNT as usize],
|
||||
regs: [0; mi::MI_REGS_COUNT],
|
||||
},
|
||||
pi: pi::Pi {
|
||||
regs: [0; pi::PI_REGS_COUNT as usize],
|
||||
regs: [0; pi::PI_REGS_COUNT],
|
||||
},
|
||||
ai: ai::Ai {
|
||||
regs: [0; ai::AI_REGS_COUNT as usize],
|
||||
regs: [0; ai::AI_REGS_COUNT],
|
||||
last_read: 0,
|
||||
freq: 33600,
|
||||
delayed_carry: false,
|
||||
@@ -476,15 +496,15 @@ impl Device {
|
||||
}; 2],
|
||||
},
|
||||
si: si::Si {
|
||||
regs: [0; si::SI_REGS_COUNT as usize],
|
||||
regs: [0; si::SI_REGS_COUNT],
|
||||
dma_dir: si::DmaDir::None,
|
||||
},
|
||||
ri: ri::Ri {
|
||||
regs: [0; ri::RI_REGS_COUNT as usize],
|
||||
regs: [0; ri::RI_REGS_COUNT],
|
||||
ram_init: false,
|
||||
},
|
||||
vi: vi::Vi {
|
||||
regs: [0; vi::VI_REGS_COUNT as usize],
|
||||
regs: [0; vi::VI_REGS_COUNT],
|
||||
clock: 0,
|
||||
field: 0,
|
||||
delay: 0,
|
||||
@@ -494,6 +514,7 @@ impl Device {
|
||||
vi_counter: 0,
|
||||
min_wait_time: std::time::Duration::from_secs(1),
|
||||
frame_time: 0.0,
|
||||
elapsed_time: 0.0,
|
||||
limit_freq: 2,
|
||||
limit_freq_check: std::time::Instant::now(),
|
||||
},
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use crate::{device, ui};
|
||||
|
||||
const AI_DRAM_ADDR_REG: u32 = 0;
|
||||
const AI_LEN_REG: u32 = 1;
|
||||
// const AI_CONTROL_REG: u32 = 2;
|
||||
const AI_STATUS_REG: u32 = 3;
|
||||
const AI_DACRATE_REG: u32 = 4;
|
||||
// const AI_BITRATE_REG: u32 = 5;
|
||||
pub const AI_REGS_COUNT: u32 = 6;
|
||||
const AI_DRAM_ADDR_REG: usize = 0;
|
||||
const AI_LEN_REG: usize = 1;
|
||||
// const AI_CONTROL_REG: usize = 2;
|
||||
pub const AI_STATUS_REG: usize = 3;
|
||||
const AI_DACRATE_REG: usize = 4;
|
||||
// const AI_BITRATE_REG: usize = 5;
|
||||
pub const AI_REGS_COUNT: usize = 6;
|
||||
|
||||
const AI_STATUS_BUSY: u32 = 0x40000000;
|
||||
pub const AI_STATUS_BUSY: u32 = 0x40000000;
|
||||
const AI_STATUS_FULL: u32 = 0x80000000;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Ai {
|
||||
pub regs: [u32; AI_REGS_COUNT as usize],
|
||||
pub regs: [u32; AI_REGS_COUNT],
|
||||
pub fifo: [AiDma; 2],
|
||||
pub last_read: u64,
|
||||
pub delayed_carry: bool,
|
||||
@@ -38,7 +38,7 @@ fn get_remaining_dma_length(device: &device::Device) -> u64 {
|
||||
}
|
||||
|
||||
let remaining_dma_duration =
|
||||
next_ai_event.unwrap().count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize];
|
||||
next_ai_event.unwrap().count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG];
|
||||
|
||||
let dma_length = remaining_dma_duration * device.ai.fifo[0].length / device.ai.fifo[0].duration;
|
||||
dma_length & !7
|
||||
@@ -46,7 +46,7 @@ fn get_remaining_dma_length(device: &device::Device) -> u64 {
|
||||
|
||||
fn get_dma_duration(device: &device::Device) -> 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;
|
||||
let length = (device.ai.regs[AI_LEN_REG] & !7) as u64;
|
||||
|
||||
(length * device.cpu.clock_rate) / (bytes_per_sample * device.ai.freq)
|
||||
}
|
||||
@@ -73,34 +73,36 @@ fn do_dma(device: &mut device::Device) {
|
||||
fn fifo_push(device: &mut device::Device) {
|
||||
let duration = get_dma_duration(device);
|
||||
|
||||
if (device.ai.regs[AI_STATUS_REG as usize] & AI_STATUS_BUSY) != 0 {
|
||||
if (device.ai.regs[AI_STATUS_REG] & AI_STATUS_BUSY) != 0 {
|
||||
device.ai.fifo[1].address =
|
||||
device.ai.regs[AI_DRAM_ADDR_REG as usize] as u64 & device::rdram::RDRAM_MASK as u64;
|
||||
device.ai.fifo[1].length = (device.ai.regs[AI_LEN_REG as usize] & !7) as u64;
|
||||
device.ai.regs[AI_DRAM_ADDR_REG] as u64 & device::rdram::RDRAM_MASK as u64;
|
||||
device.ai.fifo[1].length = (device.ai.regs[AI_LEN_REG] & !7) as u64;
|
||||
device.ai.fifo[1].duration = duration;
|
||||
device.ai.regs[AI_STATUS_REG as usize] |= AI_STATUS_FULL;
|
||||
device.ai.regs[AI_STATUS_REG] |= AI_STATUS_FULL;
|
||||
} else {
|
||||
device.ai.fifo[0].address =
|
||||
device.ai.regs[AI_DRAM_ADDR_REG as usize] as u64 & device::rdram::RDRAM_MASK as u64;
|
||||
device.ai.fifo[0].length = (device.ai.regs[AI_LEN_REG as usize] & !7) as u64;
|
||||
device.ai.regs[AI_DRAM_ADDR_REG] as u64 & device::rdram::RDRAM_MASK as u64;
|
||||
device.ai.fifo[0].length = (device.ai.regs[AI_LEN_REG] & !7) as u64;
|
||||
device.ai.fifo[0].duration = duration;
|
||||
device.ai.regs[AI_STATUS_REG as usize] |= AI_STATUS_BUSY;
|
||||
device.ai.regs[AI_STATUS_REG] |= AI_STATUS_BUSY;
|
||||
|
||||
do_dma(device);
|
||||
ui::audio::resume_game_audio(&mut device.ui);
|
||||
}
|
||||
}
|
||||
|
||||
fn fifo_pop(device: &mut device::Device) {
|
||||
if device.ai.regs[AI_STATUS_REG as usize] & AI_STATUS_FULL != 0 {
|
||||
if device.ai.regs[AI_STATUS_REG] & AI_STATUS_FULL != 0 {
|
||||
device.ai.fifo[0].address = device.ai.fifo[1].address;
|
||||
device.ai.fifo[0].length = device.ai.fifo[1].length;
|
||||
device.ai.fifo[0].duration = device.ai.fifo[1].duration;
|
||||
device.ai.regs[AI_STATUS_REG as usize] &= !AI_STATUS_FULL;
|
||||
device.ai.regs[AI_STATUS_REG] &= !AI_STATUS_FULL;
|
||||
|
||||
do_dma(device);
|
||||
} else {
|
||||
device.ai.regs[AI_STATUS_REG as usize] &= !AI_STATUS_BUSY;
|
||||
device.ai.regs[AI_STATUS_REG] &= !AI_STATUS_BUSY;
|
||||
device.ai.delayed_carry = false;
|
||||
ui::audio::pause_game_audio(&mut device.ui);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +112,7 @@ pub fn read_regs(
|
||||
_access_size: device::memory::AccessSize,
|
||||
) -> u32 {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
let return_value = match reg as u32 {
|
||||
let return_value = match reg as usize {
|
||||
AI_LEN_REG => {
|
||||
let value = get_remaining_dma_length(device);
|
||||
if value < device.ai.last_read {
|
||||
@@ -134,7 +136,7 @@ pub fn read_regs(
|
||||
|
||||
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
AI_LEN_REG => {
|
||||
device::memory::masked_write_32(&mut device.ai.regs[reg as usize], value, mask);
|
||||
if device.ai.regs[reg as usize] != 0 {
|
||||
@@ -146,7 +148,7 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
if device.ai.regs[reg as usize] != value & mask {
|
||||
device.ai.freq = device.vi.clock / (1 + (value & mask)) as u64;
|
||||
ui::audio::close_game_audio(&mut device.ui);
|
||||
ui::audio::init_game_audio(&mut device.ui, device.ai.freq)
|
||||
ui::audio::init_game_audio(device)
|
||||
}
|
||||
device::memory::masked_write_32(&mut device.ai.regs[reg as usize], value, mask)
|
||||
}
|
||||
|
||||
@@ -130,8 +130,6 @@ pub fn format_eeprom(device: &mut device::Device) {
|
||||
fn eeprom_read_block(device: &mut device::Device, block: usize, offset: usize) {
|
||||
let address = device.pif.ram[block] as usize * EEPROM_BLOCK_SIZE;
|
||||
|
||||
format_eeprom(device);
|
||||
|
||||
device.pif.ram[offset..offset + EEPROM_BLOCK_SIZE].copy_from_slice(
|
||||
&device.ui.storage.saves.eeprom.data[address..address + EEPROM_BLOCK_SIZE],
|
||||
);
|
||||
@@ -140,8 +138,6 @@ fn eeprom_read_block(device: &mut device::Device, block: usize, offset: usize) {
|
||||
fn eeprom_write_block(device: &mut device::Device, block: usize, offset: usize, status: usize) {
|
||||
let address = device.pif.ram[block] as usize * EEPROM_BLOCK_SIZE;
|
||||
|
||||
format_eeprom(device);
|
||||
|
||||
device.ui.storage.saves.eeprom.data[address..address + EEPROM_BLOCK_SIZE]
|
||||
.copy_from_slice(&device.pif.ram[offset..offset + EEPROM_BLOCK_SIZE]);
|
||||
|
||||
@@ -151,9 +147,10 @@ fn eeprom_write_block(device: &mut device::Device, block: usize, offset: usize,
|
||||
}
|
||||
|
||||
fn time2data(device: &mut device::Device, offset: usize) {
|
||||
let timestamp =
|
||||
device.cart.rtc_timestamp + (device.vi.frame_time * device.vi.vi_counter as f64) as i64;
|
||||
let now = chrono::DateTime::from_timestamp(timestamp, 0).unwrap();
|
||||
let timestamp = device.cart.rtc_timestamp + device.vi.elapsed_time as i64;
|
||||
let now = chrono::DateTime::from_timestamp(timestamp, 0)
|
||||
.unwrap()
|
||||
.with_timezone(&chrono::Local);
|
||||
|
||||
device.pif.ram[offset] = byte2bcd(now.second());
|
||||
device.pif.ram[offset + 1] = byte2bcd(now.minute());
|
||||
|
||||
@@ -6,22 +6,14 @@ use sha2::{Digest, Sha256};
|
||||
pub const CART_MASK: usize = 0xFFFFFFF;
|
||||
|
||||
fn read_cart_word(device: &device::Device, address: usize) -> u32 {
|
||||
let mut data: [u8; 4] = [0; 4];
|
||||
for i in 0..4 {
|
||||
if let Some(value) = device
|
||||
.ui
|
||||
.storage
|
||||
.saves
|
||||
.romsave
|
||||
.data
|
||||
.get(&(address as u32 + i))
|
||||
{
|
||||
data[i as usize] = *value;
|
||||
} else {
|
||||
data[i as usize] = *device.cart.rom.get(address + i as usize).unwrap_or(&0);
|
||||
}
|
||||
}
|
||||
u32::from_be_bytes(data)
|
||||
let romsave = &device.ui.storage.saves.romsave.data;
|
||||
let rom = &device.cart.rom;
|
||||
u32::from_be_bytes(std::array::from_fn(|i| {
|
||||
romsave
|
||||
.get(&((address + i) as u32))
|
||||
.copied()
|
||||
.unwrap_or_else(|| *rom.get(address + i).unwrap_or(&0))
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn read_mem_fast(
|
||||
@@ -41,8 +33,8 @@ pub fn read_mem(
|
||||
let cycles = device::pi::calculate_cycles(device, 1, 4);
|
||||
device::cop0::add_cycles(device, cycles);
|
||||
|
||||
// well known cart ROM oddity, if a read is perfomed while PI_STATUS_IO_BUSY is set, the latched value is returned rather than the data at the specified address
|
||||
if device.pi.regs[device::pi::PI_STATUS_REG as usize] & device::pi::PI_STATUS_IO_BUSY != 0 {
|
||||
// well known cart ROM oddity, if a read is performed while PI_STATUS_IO_BUSY is set, the latched value is returned rather than the data at the specified address
|
||||
if device.pi.regs[device::pi::PI_STATUS_REG] & device::pi::PI_STATUS_IO_BUSY != 0 {
|
||||
device.cart.latch
|
||||
} else {
|
||||
let masked_address = address as usize & CART_MASK;
|
||||
@@ -69,7 +61,7 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
|
||||
|
||||
device.cart.latch = value & mask;
|
||||
|
||||
device.pi.regs[device::pi::PI_STATUS_REG as usize] |= device::pi::PI_STATUS_IO_BUSY;
|
||||
device.pi.regs[device::pi::PI_STATUS_REG] |= device::pi::PI_STATUS_IO_BUSY;
|
||||
|
||||
let cycles = device::pi::calculate_cycles(device, 1, 4);
|
||||
device::events::create_event(device, device::events::EVENT_TYPE_PI, cycles);
|
||||
@@ -113,19 +105,15 @@ pub fn dma_write(
|
||||
let mut i = dram_addr;
|
||||
let mut j = cart_addr;
|
||||
while i < dram_addr + length {
|
||||
if let Some(value) = device.ui.storage.saves.romsave.data.get(&j) {
|
||||
*device
|
||||
.rdram
|
||||
.mem
|
||||
.get_mut(i as usize ^ device.byte_swap)
|
||||
.unwrap_or(&mut 0) = *value;
|
||||
*device
|
||||
.rdram
|
||||
.mem
|
||||
.get_mut(i as usize ^ device.byte_swap)
|
||||
.unwrap_or(&mut 0) = if let Some(value) = device.ui.storage.saves.romsave.data.get(&j) {
|
||||
*value
|
||||
} else {
|
||||
*device
|
||||
.rdram
|
||||
.mem
|
||||
.get_mut(i as usize ^ device.byte_swap)
|
||||
.unwrap_or(&mut 0) = *device.cart.rom.get(j as usize).unwrap_or(&0);
|
||||
}
|
||||
*device.cart.rom.get(j as usize).unwrap_or(&0)
|
||||
};
|
||||
i += 1;
|
||||
j += 1;
|
||||
}
|
||||
@@ -134,8 +122,10 @@ pub fn dma_write(
|
||||
}
|
||||
|
||||
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();
|
||||
device.cart.rtc_timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
if let Some(netplay) = &mut device.netplay {
|
||||
if netplay.player_number == 0 {
|
||||
netplay::send_rtc(netplay, device.cart.rtc_timestamp);
|
||||
@@ -159,7 +149,7 @@ pub fn init(device: &mut device::Device, rom_file: &[u8]) {
|
||||
}
|
||||
|
||||
fn is_system_pal(country: u8) -> bool {
|
||||
let pal_codes: [u8; 8] = [b'D', b'F', b'I', b'P', b'S', b'U', b'X', b'Y'];
|
||||
let pal_codes: [u8; 8] = *b"DFIPSUXY";
|
||||
for i in pal_codes {
|
||||
if country == i {
|
||||
return true;
|
||||
|
||||
@@ -3,19 +3,19 @@ use crate::ui;
|
||||
|
||||
pub const SDCARD_SIZE: usize = 0x4000000;
|
||||
|
||||
const SC64_SCR_REG: u32 = 0;
|
||||
const SC64_DATA0_REG: u32 = 1;
|
||||
const SC64_DATA1_REG: u32 = 2;
|
||||
const SC64_IDENTIFIER_REG: u32 = 3;
|
||||
const SC64_KEY_REG: u32 = 4;
|
||||
//const SC64_IRQ_REG: u32 = 5;
|
||||
//const SC64_AUX_REG: u32 = 6;
|
||||
pub const SC64_REGS_COUNT: u32 = 7;
|
||||
const SC64_SCR_REG: usize = 0;
|
||||
const SC64_DATA0_REG: usize = 1;
|
||||
const SC64_DATA1_REG: usize = 2;
|
||||
const SC64_IDENTIFIER_REG: usize = 3;
|
||||
const SC64_KEY_REG: usize = 4;
|
||||
//const SC64_IRQ_REG: usize = 5;
|
||||
//const SC64_AUX_REG: usize = 6;
|
||||
pub const SC64_REGS_COUNT: usize = 7;
|
||||
|
||||
pub const SC64_BOOTLOADER_SWITCH: u32 = 0;
|
||||
pub const SC64_ROM_WRITE_ENABLE: u32 = 1;
|
||||
pub const SC64_SAVE_TYPE: u32 = 6;
|
||||
pub const SC64_CFG_COUNT: u32 = 15;
|
||||
pub const SC64_CFG_COUNT: usize = 15;
|
||||
|
||||
const SC64_BUFFER_MASK: usize = 0x1FFF;
|
||||
const SC64_EEPROM_MASK: usize = 0xFFF;
|
||||
@@ -23,9 +23,9 @@ const SC64_EEPROM_MASK: usize = 0xFFF;
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Sc64 {
|
||||
pub buffer: Vec<u8>,
|
||||
pub regs: [u32; SC64_REGS_COUNT as usize],
|
||||
pub regs: [u32; SC64_REGS_COUNT],
|
||||
pub regs_locked: bool,
|
||||
pub cfg: [u32; SC64_CFG_COUNT as usize],
|
||||
pub cfg: [u32; SC64_CFG_COUNT],
|
||||
pub sector: u32,
|
||||
pub writeback_sector: Vec<u32>,
|
||||
pub usb_buffer: Vec<u8>,
|
||||
@@ -49,7 +49,7 @@ pub fn read_regs(
|
||||
return 0;
|
||||
}
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
SC64_SCR_REG | SC64_DATA0_REG | SC64_DATA1_REG => device.cart.sc64.regs[reg as usize],
|
||||
SC64_IDENTIFIER_REG => 0x53437632,
|
||||
_ => panic!("unknown read reg {reg} address {address:#x}"),
|
||||
@@ -58,12 +58,12 @@ pub fn read_regs(
|
||||
|
||||
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
SC64_KEY_REG => {
|
||||
device::memory::masked_write_32(&mut device.cart.sc64.regs[reg as usize], value, mask);
|
||||
if device.cart.sc64.regs[SC64_KEY_REG as usize] == 0x4F434B5F {
|
||||
if device.cart.sc64.regs[SC64_KEY_REG] == 0x4F434B5F {
|
||||
device.cart.sc64.regs_locked = false;
|
||||
} else if device.cart.sc64.regs[SC64_KEY_REG as usize] == 0xFFFFFFFF {
|
||||
} else if device.cart.sc64.regs[SC64_KEY_REG] == 0xFFFFFFFF {
|
||||
device.cart.sc64.regs_locked = true;
|
||||
}
|
||||
}
|
||||
@@ -81,22 +81,22 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
match char::from_u32(value & mask).unwrap() {
|
||||
'V' => {
|
||||
// get version
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] = (2 << 16) | 20;
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize] = 2;
|
||||
device.cart.sc64.regs[SC64_DATA0_REG] = (2 << 16) | 20;
|
||||
device.cart.sc64.regs[SC64_DATA1_REG] = 2;
|
||||
}
|
||||
'c' => {
|
||||
// get config
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize] = device.cart.sc64.cfg
|
||||
[device.cart.sc64.regs[SC64_DATA0_REG as usize] as usize]
|
||||
device.cart.sc64.regs[SC64_DATA1_REG] =
|
||||
device.cart.sc64.cfg[device.cart.sc64.regs[SC64_DATA0_REG] as usize]
|
||||
}
|
||||
'C' => {
|
||||
// set config
|
||||
if device.cart.sc64.regs[SC64_DATA0_REG as usize] == SC64_SAVE_TYPE {
|
||||
if device.cart.sc64.regs[SC64_DATA0_REG] == SC64_SAVE_TYPE {
|
||||
// if save type is being written, we are probably booting a game using the flash cart menu
|
||||
// we shouldn't write saves to disk in this case (they are written to the SD card)
|
||||
device.ui.storage.saves.write_to_disk = false;
|
||||
device.ui.storage.save_type =
|
||||
match device.cart.sc64.regs[SC64_DATA1_REG as usize] {
|
||||
match device.cart.sc64.regs[SC64_DATA1_REG] {
|
||||
0 => {
|
||||
vec![]
|
||||
}
|
||||
@@ -115,20 +115,20 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
_ => {
|
||||
panic!(
|
||||
"unknown sc64 save type: {}",
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize]
|
||||
device.cart.sc64.regs[SC64_DATA1_REG]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
std::mem::swap(
|
||||
&mut device.cart.sc64.cfg
|
||||
[device.cart.sc64.regs[SC64_DATA0_REG as usize] as usize],
|
||||
&mut device.cart.sc64.regs[SC64_DATA1_REG as usize],
|
||||
[device.cart.sc64.regs[SC64_DATA0_REG] as usize],
|
||||
&mut device.cart.sc64.regs[SC64_DATA1_REG],
|
||||
);
|
||||
}
|
||||
'i' => {
|
||||
// sd card operation
|
||||
match device.cart.sc64.regs[SC64_DATA1_REG as usize] {
|
||||
match device.cart.sc64.regs[SC64_DATA1_REG] {
|
||||
0 => { //Init SD card
|
||||
}
|
||||
1 => { //Deinit SD card
|
||||
@@ -136,23 +136,21 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
_ => {
|
||||
panic!(
|
||||
"unknown sc64 sd card operation: {}",
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize]
|
||||
device.cart.sc64.regs[SC64_DATA1_REG]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
'I' => {
|
||||
// set sd sector
|
||||
device.cart.sc64.sector = device.cart.sc64.regs[SC64_DATA0_REG as usize];
|
||||
device.cart.sc64.sector = device.cart.sc64.regs[SC64_DATA0_REG];
|
||||
}
|
||||
's' => {
|
||||
format_sdcard(device);
|
||||
// read sd card
|
||||
let address =
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64 & 0x1FFFFFFF;
|
||||
let address = device.cart.sc64.regs[SC64_DATA0_REG] as u64 & 0x1FFFFFFF;
|
||||
let offset = (device.cart.sc64.sector * 512) as usize;
|
||||
let length =
|
||||
(device.cart.sc64.regs[SC64_DATA1_REG as usize] * 512) as usize;
|
||||
let length = (device.cart.sc64.regs[SC64_DATA1_REG] * 512) as usize;
|
||||
let mut i = 0;
|
||||
|
||||
while i < length {
|
||||
@@ -182,11 +180,9 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
'S' => {
|
||||
format_sdcard(device);
|
||||
// write sd card
|
||||
let address =
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64 & 0x1FFFFFFF;
|
||||
let address = device.cart.sc64.regs[SC64_DATA0_REG] as u64 & 0x1FFFFFFF;
|
||||
let offset = (device.cart.sc64.sector * 512) as usize;
|
||||
let length =
|
||||
(device.cart.sc64.regs[SC64_DATA1_REG as usize] * 512) as usize;
|
||||
let length = (device.cart.sc64.regs[SC64_DATA1_REG] * 512) as usize;
|
||||
let mut i = 0;
|
||||
|
||||
while i < length {
|
||||
@@ -211,16 +207,15 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
ui::storage::schedule_save(device, ui::storage::SaveTypes::Sdcard);
|
||||
}
|
||||
'U' => {
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0;
|
||||
device.cart.sc64.regs[SC64_DATA0_REG] = 0;
|
||||
}
|
||||
'u' => {
|
||||
// used to notify the game that there is data to read
|
||||
if let Some(cart_rx) = device.ui.usb.cart_rx.as_mut() {
|
||||
match cart_rx.try_recv() {
|
||||
Ok(data) => {
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] =
|
||||
data.data_type & 0xFF; // read_status/type
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize] =
|
||||
device.cart.sc64.regs[SC64_DATA0_REG] = data.data_type & 0xFF; // read_status/type
|
||||
device.cart.sc64.regs[SC64_DATA1_REG] =
|
||||
data.data_size & 0xFFFFFF; // length
|
||||
device.cart.sc64.usb_buffer = data.data; // store the data to be read
|
||||
}
|
||||
@@ -230,23 +225,21 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
panic!("cart_rx lagged: {err}");
|
||||
}
|
||||
_ => {
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0; // read_status/type
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize] = 0; // length
|
||||
device.cart.sc64.regs[SC64_DATA0_REG] = 0; // read_status/type
|
||||
device.cart.sc64.regs[SC64_DATA1_REG] = 0; // length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0; // read_status/type
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize] = 0; // length
|
||||
device.cart.sc64.regs[SC64_DATA0_REG] = 0; // read_status/type
|
||||
device.cart.sc64.regs[SC64_DATA1_REG] = 0; // length
|
||||
}
|
||||
}
|
||||
'M' => {
|
||||
// Send data from from flashcart to USB
|
||||
let address =
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64 & 0x1FFFFFFF;
|
||||
let length =
|
||||
device.cart.sc64.regs[SC64_DATA1_REG as usize] as usize & 0xFFFFFF;
|
||||
let address = device.cart.sc64.regs[SC64_DATA0_REG] as u64 & 0x1FFFFFFF;
|
||||
let length = device.cart.sc64.regs[SC64_DATA1_REG] as usize & 0xFFFFFF;
|
||||
|
||||
if let Some(usb_tx) = device.ui.usb.usb_tx.as_mut() {
|
||||
let mut usb_buffer = vec![0; length];
|
||||
@@ -297,18 +290,16 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
usb_tx,
|
||||
ui::usb::UsbData {
|
||||
data: usb_buffer,
|
||||
data_type: device.cart.sc64.regs[SC64_DATA1_REG as usize] >> 24,
|
||||
data_size: device.cart.sc64.regs[SC64_DATA1_REG as usize]
|
||||
& 0xFFFFFF,
|
||||
data_type: device.cart.sc64.regs[SC64_DATA1_REG] >> 24,
|
||||
data_size: device.cart.sc64.regs[SC64_DATA1_REG] & 0xFFFFFF,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
'm' => {
|
||||
// Receive data from USB to flashcart
|
||||
let address =
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64 & 0x1FFFFFFF;
|
||||
let length = device.cart.sc64.regs[SC64_DATA1_REG as usize] as usize;
|
||||
let address = device.cart.sc64.regs[SC64_DATA0_REG] as u64 & 0x1FFFFFFF;
|
||||
let length = device.cart.sc64.regs[SC64_DATA1_REG] as usize;
|
||||
|
||||
let mut i = 0;
|
||||
|
||||
@@ -338,11 +329,11 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
}
|
||||
'w' => {
|
||||
// SD card writeback pending
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0;
|
||||
device.cart.sc64.regs[SC64_DATA0_REG] = 0;
|
||||
}
|
||||
'W' => {
|
||||
let writeback_sectors_address =
|
||||
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64;
|
||||
device.cart.sc64.regs[SC64_DATA0_REG] as u64;
|
||||
for i in 0..256 {
|
||||
device.cart.sc64.writeback_sector[i] = device::memory::data_read(
|
||||
device,
|
||||
@@ -376,7 +367,6 @@ pub fn read_mem(
|
||||
_access_size: device::memory::AccessSize,
|
||||
) -> u32 {
|
||||
if address & 0x2000 != 0 {
|
||||
device::cart::format_eeprom(device);
|
||||
let masked_address = address as usize & SC64_EEPROM_MASK;
|
||||
u32::from_be_bytes(
|
||||
device.ui.storage.saves.eeprom.data[masked_address..masked_address + 4]
|
||||
@@ -395,7 +385,6 @@ pub fn read_mem(
|
||||
|
||||
pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
if address & 0x2000 != 0 {
|
||||
device::cart::format_eeprom(device);
|
||||
let masked_address = address as usize & SC64_EEPROM_MASK;
|
||||
let mut data = u32::from_be_bytes(
|
||||
device.ui.storage.saves.eeprom.data[masked_address..masked_address + 4]
|
||||
@@ -427,7 +416,6 @@ pub fn dma_read(
|
||||
) -> u64 {
|
||||
dram_addr &= device::rdram::RDRAM_MASK as u32;
|
||||
let buffer = if cart_addr & 0x2000 != 0 {
|
||||
device::cart::format_eeprom(device);
|
||||
cart_addr &= SC64_EEPROM_MASK as u32;
|
||||
ui::storage::schedule_save(device, ui::storage::SaveTypes::Eeprom4k);
|
||||
&mut device.ui.storage.saves.eeprom.data
|
||||
@@ -459,7 +447,6 @@ pub fn dma_write(
|
||||
) -> u64 {
|
||||
dram_addr &= device::rdram::RDRAM_MASK as u32;
|
||||
let buffer = if cart_addr & 0x2000 != 0 {
|
||||
device::cart::format_eeprom(device);
|
||||
cart_addr &= SC64_EEPROM_MASK as u32;
|
||||
&device.ui.storage.saves.eeprom.data
|
||||
} else {
|
||||
|
||||
@@ -28,13 +28,13 @@ pub struct Flashram {
|
||||
pub silicon_id: [u32; 2],
|
||||
}
|
||||
|
||||
fn format_sram(device: &mut device::Device) {
|
||||
pub fn format_sram(device: &mut device::Device) {
|
||||
if device.ui.storage.saves.sram.data.len() < SRAM_SIZE {
|
||||
device.ui.storage.saves.sram.data.resize(SRAM_SIZE, 0xFF)
|
||||
}
|
||||
}
|
||||
|
||||
fn format_flash(device: &mut device::Device) {
|
||||
pub fn format_flash(device: &mut device::Device) {
|
||||
if device.ui.storage.saves.flash.data.len() < FLASHRAM_SIZE {
|
||||
device
|
||||
.ui
|
||||
@@ -49,8 +49,6 @@ fn format_flash(device: &mut device::Device) {
|
||||
fn read_mem_sram(device: &mut device::Device, address: u64) -> u32 {
|
||||
let masked_address = address as usize & SRAM_MASK;
|
||||
|
||||
format_sram(device);
|
||||
|
||||
u32::from_be_bytes(
|
||||
device.ui.storage.saves.sram.data[masked_address..masked_address + 4]
|
||||
.try_into()
|
||||
@@ -96,8 +94,6 @@ pub fn read_mem(
|
||||
fn write_mem_sram(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let masked_address = address as usize & SRAM_MASK;
|
||||
|
||||
format_sram(device);
|
||||
|
||||
let mut data = u32::from_be_bytes(
|
||||
device.ui.storage.saves.sram.data[masked_address..masked_address + 4]
|
||||
.try_into()
|
||||
@@ -116,7 +112,6 @@ fn write_mem_flash(device: &mut device::Device, address: u64, value: u32, mask:
|
||||
device.cart.flashram.status = (value & mask) & 0xff;
|
||||
} else if (address & 0x1ffff) == 0x10000 {
|
||||
/* set command */
|
||||
format_flash(device);
|
||||
flashram_command(device, value & mask);
|
||||
} else {
|
||||
/* other accesses are not implemented */
|
||||
@@ -136,7 +131,7 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
|
||||
write_mem_flash(device, address, value, mask)
|
||||
}
|
||||
|
||||
device.pi.regs[device::pi::PI_STATUS_REG as usize] |= device::pi::PI_STATUS_IO_BUSY;
|
||||
device.pi.regs[device::pi::PI_STATUS_REG] |= device::pi::PI_STATUS_IO_BUSY;
|
||||
|
||||
let cycles = device::pi::calculate_cycles(device, 2, 4);
|
||||
device::events::create_event(device, device::events::EVENT_TYPE_PI, cycles);
|
||||
@@ -148,8 +143,6 @@ fn dma_read_sram(device: &mut device::Device, mut cart_addr: u32, mut dram_addr:
|
||||
let mut i = dram_addr;
|
||||
let mut j = cart_addr;
|
||||
|
||||
format_sram(device);
|
||||
|
||||
while i < dram_addr + length {
|
||||
*device
|
||||
.ui
|
||||
@@ -171,8 +164,6 @@ fn dma_read_sram(device: &mut device::Device, mut cart_addr: u32, mut dram_addr:
|
||||
}
|
||||
|
||||
fn dma_read_flash(device: &mut device::Device, cart_addr: u32, dram_addr: u32, length: u32) {
|
||||
format_flash(device);
|
||||
|
||||
if (cart_addr & 0x1ffff) == 0x00000
|
||||
&& length == 128
|
||||
&& device.cart.flashram.mode == FlashramMode::PageProgram
|
||||
@@ -217,8 +208,6 @@ fn dma_write_sram(
|
||||
let mut i = dram_addr;
|
||||
let mut j = cart_addr;
|
||||
|
||||
format_sram(device);
|
||||
|
||||
while i < dram_addr + length {
|
||||
*device
|
||||
.rdram
|
||||
@@ -266,7 +255,6 @@ fn dma_write_flash(
|
||||
} else if (cart_addr & 0x1ffff) < 0x10000
|
||||
&& device.cart.flashram.mode == FlashramMode::ReadArray
|
||||
{
|
||||
format_flash(device);
|
||||
/* adjust flashram address before starting DMA. */
|
||||
if device.cart.flashram.silicon_id[1] == MX29L1100_ID
|
||||
|| device.cart.flashram.silicon_id[1] == MX29L0000_ID
|
||||
|
||||
@@ -58,12 +58,12 @@ pub fn process(device: &mut device::Device, channel: usize) {
|
||||
let offset = device.pif.channels[channel].rx_buf.unwrap();
|
||||
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);
|
||||
let local_input = ui::input::get(&mut device.ui, 0);
|
||||
netplay::send_input(netplay, local_input);
|
||||
}
|
||||
netplay::get_input(device, channel)
|
||||
} else {
|
||||
ui::input::get(&device.ui, channel)
|
||||
ui::input::get(&mut device.ui, channel)
|
||||
};
|
||||
|
||||
device.pif.ram[offset..offset + 4].copy_from_slice(&input.data.to_ne_bytes());
|
||||
@@ -76,7 +76,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
|
||||
device::events::create_event(
|
||||
device,
|
||||
device::events::EVENT_TYPE_PAK,
|
||||
device.cpu.clock_rate, // 1 second
|
||||
device.cpu.clock_rate / 2, // 500ms
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
|
||||
device.pif.channels[channel].rx_buf.unwrap(),
|
||||
channel,
|
||||
),
|
||||
_ => println!("unknown controller command {cmd}"),
|
||||
_ => eprintln!("unknown controller command {cmd}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ pub fn pak_switch_event(device: &mut device::Device) {
|
||||
pak_type: new_pak_type,
|
||||
});
|
||||
ui::video::onscreen_message(
|
||||
"MemPak selected",
|
||||
&format!("P{} MemPak selected", i + 1),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
} else if new_pak_type == PakType::RumblePak {
|
||||
@@ -201,7 +201,7 @@ pub fn pak_switch_event(device: &mut device::Device) {
|
||||
pak_type: new_pak_type,
|
||||
});
|
||||
ui::video::onscreen_message(
|
||||
"RumblePak selected",
|
||||
&format!("P{} RumblePak selected", i + 1),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
} else if new_pak_type == PakType::TransferPak {
|
||||
@@ -211,7 +211,7 @@ pub fn pak_switch_event(device: &mut device::Device) {
|
||||
pak_type: new_pak_type,
|
||||
});
|
||||
ui::video::onscreen_message(
|
||||
"TransferPak selected",
|
||||
&format!("P{} TransferPak selected", i + 1),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
use crate::device;
|
||||
|
||||
use std::io::Write;
|
||||
const MBC3_RTC_SECONDS: usize = 0;
|
||||
const MBC3_RTC_MINUTES: usize = 1;
|
||||
const MBC3_RTC_HOURS: usize = 2;
|
||||
const MBC3_RTC_DAYS_L: usize = 3;
|
||||
const MBC3_RTC_DAYS_H: usize = 4;
|
||||
const MBC3_RTC_REGS_COUNT: usize = 5;
|
||||
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct GbCart {
|
||||
pub enabled: bool,
|
||||
@@ -11,14 +19,13 @@ pub struct GbCart {
|
||||
pub mbc1_mode: bool,
|
||||
pub ram_bank: u16,
|
||||
pub rom_bank: u32,
|
||||
pub set_latch: bool,
|
||||
pub latch_second: u8,
|
||||
pub latch_minute: u8,
|
||||
pub latch_hour: u8,
|
||||
pub latch_day: u16,
|
||||
pub latch: bool,
|
||||
pub rtc_regs: [u8; MBC3_RTC_REGS_COUNT],
|
||||
pub rtc_regs_latch: [u8; MBC3_RTC_REGS_COUNT],
|
||||
pub last_elapsed_time: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Default, Copy, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum CartType {
|
||||
#[default]
|
||||
None,
|
||||
@@ -28,6 +35,135 @@ pub enum CartType {
|
||||
MBC5RamBatt,
|
||||
}
|
||||
|
||||
pub fn init(gb_cart: &mut device::controller::gbcart::GbCart, rom: &[u8], ram: &[u8]) {
|
||||
gb_cart.rom = rom.to_vec();
|
||||
let offset = match ram.len() % 8192 {
|
||||
44 => {
|
||||
gb_cart.ram = ram[..ram.len() - 44].to_vec();
|
||||
ram.len() - 44
|
||||
}
|
||||
48 => {
|
||||
gb_cart.ram = ram[..ram.len() - 48].to_vec();
|
||||
ram.len() - 48
|
||||
}
|
||||
_ => {
|
||||
gb_cart.ram = ram.to_vec();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
gb_cart.rtc_regs[MBC3_RTC_SECONDS] =
|
||||
u32::from_le_bytes(ram[offset..offset + 4].try_into().unwrap()) as u8;
|
||||
gb_cart.rtc_regs[MBC3_RTC_MINUTES] =
|
||||
u32::from_le_bytes(ram[offset + 4..offset + 8].try_into().unwrap()) as u8;
|
||||
gb_cart.rtc_regs[MBC3_RTC_HOURS] =
|
||||
u32::from_le_bytes(ram[offset + 8..offset + 12].try_into().unwrap()) as u8;
|
||||
gb_cart.rtc_regs[MBC3_RTC_DAYS_L] =
|
||||
u32::from_le_bytes(ram[offset + 12..offset + 16].try_into().unwrap()) as u8;
|
||||
gb_cart.rtc_regs[MBC3_RTC_DAYS_H] =
|
||||
u32::from_le_bytes(ram[offset + 16..offset + 20].try_into().unwrap()) as u8;
|
||||
|
||||
gb_cart.rtc_regs_latch[MBC3_RTC_SECONDS] =
|
||||
u32::from_le_bytes(ram[offset + 20..offset + 24].try_into().unwrap()) as u8;
|
||||
gb_cart.rtc_regs_latch[MBC3_RTC_MINUTES] =
|
||||
u32::from_le_bytes(ram[offset + 24..offset + 28].try_into().unwrap()) as u8;
|
||||
gb_cart.rtc_regs_latch[MBC3_RTC_HOURS] =
|
||||
u32::from_le_bytes(ram[offset + 28..offset + 32].try_into().unwrap()) as u8;
|
||||
gb_cart.rtc_regs_latch[MBC3_RTC_DAYS_L] =
|
||||
u32::from_le_bytes(ram[offset + 32..offset + 36].try_into().unwrap()) as u8;
|
||||
gb_cart.rtc_regs_latch[MBC3_RTC_DAYS_H] =
|
||||
u32::from_le_bytes(ram[offset + 36..offset + 40].try_into().unwrap()) as u8;
|
||||
}
|
||||
|
||||
pub fn save(
|
||||
gb_cart: &mut device::controller::gbcart::GbCart,
|
||||
elapsed_time: i64,
|
||||
ram_path: &std::string::String,
|
||||
) {
|
||||
if cfg!(target_os = "android") {
|
||||
return;
|
||||
}
|
||||
update_rtc_regs(gb_cart, elapsed_time);
|
||||
|
||||
if let Ok(file) = std::fs::File::create(ram_path) {
|
||||
let mut f = std::io::BufWriter::new(file);
|
||||
f.write_all(&gb_cart.ram).unwrap();
|
||||
|
||||
if gb_cart.cart_type == CartType::MBC3RamBattRtc {
|
||||
f.write_all(&(gb_cart.rtc_regs[MBC3_RTC_SECONDS] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
f.write_all(&(gb_cart.rtc_regs[MBC3_RTC_MINUTES] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
f.write_all(&(gb_cart.rtc_regs[MBC3_RTC_HOURS] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
f.write_all(&(gb_cart.rtc_regs[MBC3_RTC_DAYS_L] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
f.write_all(&(gb_cart.rtc_regs[MBC3_RTC_DAYS_H] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
|
||||
f.write_all(&(gb_cart.rtc_regs_latch[MBC3_RTC_SECONDS] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
f.write_all(&(gb_cart.rtc_regs_latch[MBC3_RTC_MINUTES] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
f.write_all(&(gb_cart.rtc_regs_latch[MBC3_RTC_HOURS] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
f.write_all(&(gb_cart.rtc_regs_latch[MBC3_RTC_DAYS_L] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
f.write_all(&(gb_cart.rtc_regs_latch[MBC3_RTC_DAYS_H] as u32).to_le_bytes())
|
||||
.unwrap();
|
||||
|
||||
let max = ((i32::MAX as i64) << 32) | (i32::MAX as i64);
|
||||
f.write_all(&max.to_le_bytes()).unwrap();
|
||||
|
||||
f.flush().unwrap();
|
||||
}
|
||||
} else {
|
||||
eprintln!("Error saving TransferPak RAM to {ram_path}");
|
||||
}
|
||||
}
|
||||
|
||||
fn update_rtc_regs(cart: &mut device::controller::gbcart::GbCart, elapsed_time: i64) {
|
||||
let mut diff = elapsed_time - cart.last_elapsed_time;
|
||||
cart.last_elapsed_time = elapsed_time;
|
||||
|
||||
if diff > 0 {
|
||||
cart.rtc_regs[MBC3_RTC_SECONDS] += (diff % 60) as u8;
|
||||
if cart.rtc_regs[MBC3_RTC_SECONDS] >= 60 {
|
||||
cart.rtc_regs[MBC3_RTC_SECONDS] -= 60;
|
||||
cart.rtc_regs[MBC3_RTC_MINUTES] += 1;
|
||||
}
|
||||
diff /= 60;
|
||||
|
||||
cart.rtc_regs[MBC3_RTC_MINUTES] += (diff % 60) as u8;
|
||||
if cart.rtc_regs[MBC3_RTC_MINUTES] >= 60 {
|
||||
cart.rtc_regs[MBC3_RTC_MINUTES] -= 60;
|
||||
cart.rtc_regs[MBC3_RTC_HOURS] += 1;
|
||||
}
|
||||
diff /= 60;
|
||||
|
||||
cart.rtc_regs[MBC3_RTC_HOURS] += (diff % 24) as u8;
|
||||
if cart.rtc_regs[MBC3_RTC_HOURS] >= 24 {
|
||||
cart.rtc_regs[MBC3_RTC_HOURS] -= 24;
|
||||
cart.rtc_regs[MBC3_RTC_DAYS_L] += 1;
|
||||
}
|
||||
diff /= 24;
|
||||
|
||||
/* update days counter */
|
||||
let days = ((((cart.rtc_regs[MBC3_RTC_DAYS_H] & 0x01) as i64) << 8)
|
||||
| cart.rtc_regs[MBC3_RTC_DAYS_L] as i64)
|
||||
+ diff;
|
||||
|
||||
cart.rtc_regs[MBC3_RTC_DAYS_L] = (days & 0xff) as u8;
|
||||
cart.rtc_regs[MBC3_RTC_DAYS_H] &= !0x01;
|
||||
cart.rtc_regs[MBC3_RTC_DAYS_H] |= ((days >> 8) & 1) as u8;
|
||||
|
||||
/* set carry bit if days overflow */
|
||||
if days > 511 {
|
||||
cart.rtc_regs[MBC3_RTC_DAYS_H] |= 0x80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_mbc1(
|
||||
pif_ram: &[u8],
|
||||
cart: &mut device::controller::gbcart::GbCart,
|
||||
@@ -105,6 +241,7 @@ fn write_mbc3(
|
||||
address: u16,
|
||||
data: usize,
|
||||
size: usize,
|
||||
elapsed_time: i64,
|
||||
) {
|
||||
let value = pif_ram[data + size - 1];
|
||||
if address < 0x2000 {
|
||||
@@ -118,10 +255,13 @@ fn write_mbc3(
|
||||
} else if address < 0x6000 {
|
||||
cart.ram_bank = (value & 0xf) as u16;
|
||||
} else if address < 0x8000 {
|
||||
if !cart.set_latch && value != 0 {
|
||||
// not implemented
|
||||
if cart.cart_type == CartType::MBC3RamBattRtc {
|
||||
if !cart.latch && (value & 0x1) != 0 {
|
||||
update_rtc_regs(cart, elapsed_time);
|
||||
cart.rtc_regs_latch = cart.rtc_regs;
|
||||
}
|
||||
cart.latch = (value & 0x1) != 0;
|
||||
}
|
||||
cart.set_latch = value != 0;
|
||||
} else if (0xa000..0xc000).contains(&address) {
|
||||
if !cart.ram_enabled {
|
||||
return;
|
||||
@@ -130,29 +270,8 @@ fn write_mbc3(
|
||||
let banked_address = address - 0xA000 + (cart.ram_bank * 0x2000);
|
||||
cart.ram[banked_address as usize..banked_address as usize + size]
|
||||
.copy_from_slice(&pif_ram[data..data + size]);
|
||||
} else {
|
||||
match cart.ram_bank {
|
||||
0x8 => {
|
||||
cart.latch_second = value;
|
||||
}
|
||||
0x9 => {
|
||||
cart.latch_minute = value;
|
||||
}
|
||||
0xA => {
|
||||
cart.latch_hour = value;
|
||||
}
|
||||
0xB => {
|
||||
cart.latch_day &= 0xFF00;
|
||||
cart.latch_day |= value as u16;
|
||||
}
|
||||
0xC => {
|
||||
cart.latch_day &= 0x00FF;
|
||||
cart.latch_day |= (value as u16) << 8;
|
||||
}
|
||||
_ => {
|
||||
panic!("Unsupported ram bank {:x}", cart.ram_bank);
|
||||
}
|
||||
}
|
||||
} else if cart.cart_type == CartType::MBC3RamBattRtc {
|
||||
cart.rtc_regs[cart.ram_bank as usize - 0x8] = value;
|
||||
}
|
||||
} else {
|
||||
panic!("Unsupported write address {address:x}");
|
||||
@@ -161,10 +280,11 @@ fn write_mbc3(
|
||||
|
||||
fn read_mbc3(
|
||||
pif_ram: &mut [u8],
|
||||
cart: &device::controller::gbcart::GbCart,
|
||||
cart: &mut device::controller::gbcart::GbCart,
|
||||
address: u16,
|
||||
data: usize,
|
||||
size: usize,
|
||||
elapsed_time: i64,
|
||||
) {
|
||||
if address < 0x4000 {
|
||||
let banked_address = address & 0x3FFF;
|
||||
@@ -188,19 +308,16 @@ fn read_mbc3(
|
||||
.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,
|
||||
0x9 => cart.latch_minute,
|
||||
0xA => cart.latch_hour,
|
||||
0xB => cart.latch_day as u8,
|
||||
0xC => (cart.latch_day >> 8) as u8,
|
||||
_ => {
|
||||
panic!("Unsupported ram bank {:x}", cart.ram_bank);
|
||||
} else if cart.cart_type == CartType::MBC3RamBattRtc {
|
||||
if cart.latch {
|
||||
for i in 0..size {
|
||||
pif_ram[data + i] = cart.rtc_regs_latch[cart.ram_bank as usize - 0x8];
|
||||
}
|
||||
} else {
|
||||
update_rtc_regs(cart, elapsed_time);
|
||||
for i in 0..size {
|
||||
pif_ram[data + i] = cart.rtc_regs[cart.ram_bank as usize - 0x8];
|
||||
}
|
||||
};
|
||||
for i in 0..size {
|
||||
pif_ram[data + i] = latch;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -227,7 +344,7 @@ fn write_mbc5(
|
||||
} else if address < 0x6000 {
|
||||
cart.ram_bank = (value & 0xf) as u16;
|
||||
} else if address < 0xa000 {
|
||||
println!("Unknown MBC5 write address {address:x}");
|
||||
eprintln!("Unknown MBC5 write address {address:x}");
|
||||
} else if (0xa000..0xc000).contains(&address) {
|
||||
if !cart.ram_enabled {
|
||||
return;
|
||||
@@ -274,10 +391,11 @@ fn read_mbc5(
|
||||
|
||||
pub fn read(
|
||||
pif_ram: &mut [u8],
|
||||
cart: &device::controller::gbcart::GbCart,
|
||||
cart: &mut device::controller::gbcart::GbCart,
|
||||
address: u16,
|
||||
data: usize,
|
||||
size: usize,
|
||||
elapsed_time: i64,
|
||||
) {
|
||||
if !cart.enabled {
|
||||
for i in 0..size {
|
||||
@@ -287,8 +405,8 @@ pub fn read(
|
||||
}
|
||||
match cart.cart_type {
|
||||
CartType::MBC1RamBatt => read_mbc1(pif_ram, cart, address, data, size),
|
||||
CartType::MBC3RamBatt => read_mbc3(pif_ram, cart, address, data, size),
|
||||
CartType::MBC3RamBattRtc => read_mbc3(pif_ram, cart, address, data, size),
|
||||
CartType::MBC3RamBatt => read_mbc3(pif_ram, cart, address, data, size, elapsed_time),
|
||||
CartType::MBC3RamBattRtc => read_mbc3(pif_ram, cart, address, data, size, elapsed_time),
|
||||
CartType::MBC5RamBatt => read_mbc5(pif_ram, cart, address, data, size),
|
||||
_ => panic!("Unsupported cart type"),
|
||||
}
|
||||
@@ -300,14 +418,15 @@ pub fn write(
|
||||
address: u16,
|
||||
data: usize,
|
||||
size: usize,
|
||||
elapsed_time: i64,
|
||||
) {
|
||||
if !cart.enabled {
|
||||
return;
|
||||
}
|
||||
match cart.cart_type {
|
||||
CartType::MBC1RamBatt => write_mbc1(pif_ram, cart, address, data, size),
|
||||
CartType::MBC3RamBatt => write_mbc3(pif_ram, cart, address, data, size),
|
||||
CartType::MBC3RamBattRtc => write_mbc3(pif_ram, cart, address, data, size),
|
||||
CartType::MBC3RamBatt => write_mbc3(pif_ram, cart, address, data, size, elapsed_time),
|
||||
CartType::MBC3RamBattRtc => write_mbc3(pif_ram, cart, address, data, size, elapsed_time),
|
||||
CartType::MBC5RamBatt => write_mbc5(pif_ram, cart, address, data, size),
|
||||
_ => panic!("Unsupported cart type"),
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::ui;
|
||||
pub const MEMPAK_SIZE: usize = 0x8000;
|
||||
const MPK_PAGE_SIZE: usize = 256;
|
||||
|
||||
fn format_mempak(device: &mut device::Device) {
|
||||
pub fn format_mempak(device: &mut device::Device) {
|
||||
if device.ui.storage.saves.mempak.data.len() < MEMPAK_SIZE * 4 {
|
||||
device
|
||||
.ui
|
||||
@@ -43,6 +43,11 @@ fn format_mempak(device: &mut device::Device) {
|
||||
];
|
||||
for i in 0..4 {
|
||||
let offset = i * MEMPAK_SIZE;
|
||||
if device.ui.storage.saves.mempak.data[offset..offset + MEMPAK_SIZE] != [0; MEMPAK_SIZE]
|
||||
{
|
||||
// don't overwrite existing data
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Fill Page 0 with pre-initialized content */
|
||||
device.ui.storage.saves.mempak.data[offset..offset + MPK_PAGE_SIZE]
|
||||
@@ -77,8 +82,6 @@ fn format_mempak(device: &mut device::Device) {
|
||||
|
||||
pub fn read(device: &mut device::Device, channel: usize, address: u16, data: usize, size: usize) {
|
||||
if (address as usize) < MEMPAK_SIZE {
|
||||
format_mempak(device);
|
||||
|
||||
let offset = (channel * MEMPAK_SIZE) + address as usize;
|
||||
device.pif.ram[data..data + size]
|
||||
.copy_from_slice(&device.ui.storage.saves.mempak.data[offset..offset + size])
|
||||
@@ -91,8 +94,6 @@ pub fn read(device: &mut device::Device, channel: usize, address: u16, data: usi
|
||||
|
||||
pub fn write(device: &mut device::Device, channel: usize, address: u16, data: usize, size: usize) {
|
||||
if (address as usize) < MEMPAK_SIZE {
|
||||
format_mempak(device);
|
||||
|
||||
let offset = (channel * MEMPAK_SIZE) + address as usize;
|
||||
device.ui.storage.saves.mempak.data[offset..offset + size]
|
||||
.copy_from_slice(&device.pif.ram[data..data + size]);
|
||||
|
||||
@@ -54,14 +54,15 @@ pub fn read(device: &mut device::Device, channel: usize, address: u16, data: usi
|
||||
0xC..=0xF => {
|
||||
device::controller::gbcart::read(
|
||||
&mut device.pif.ram,
|
||||
&pak.cart,
|
||||
&mut pak.cart,
|
||||
0x4000 * pak.bank + (address & 0x7fff) - 0x4000,
|
||||
data,
|
||||
size,
|
||||
device.vi.elapsed_time as i64,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
println!("unknown transfer pak read {:x}", address >> 12);
|
||||
eprintln!("unknown transfer pak read {:x}", address >> 12);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,7 +85,7 @@ pub fn write(device: &mut device::Device, channel: usize, address: u16, data: us
|
||||
}
|
||||
pak.enabled = true;
|
||||
}
|
||||
_ => println!("unknown transfer pak value"),
|
||||
_ => eprintln!("unknown transfer pak value"),
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -124,10 +125,11 @@ pub fn write(device: &mut device::Device, channel: usize, address: u16, data: us
|
||||
0x4000 * pak.bank + (address & 0x7fff) - 0x4000,
|
||||
data,
|
||||
size,
|
||||
device.vi.elapsed_time as i64,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
println!("unknown transfer pak write {:x}", address >> 12);
|
||||
eprintln!("unknown transfer pak write {:x}", address >> 12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::device;
|
||||
#[cfg(feature = "gui")]
|
||||
#[cfg(all(feature = "gui", not(target_os = "android")))]
|
||||
use crate::ui;
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -181,9 +181,9 @@ pub fn process(device: &mut device::Device, channel: usize) {
|
||||
device.pif.ram[device.pif.channels[channel].rx_buf.unwrap()] = 0;
|
||||
}
|
||||
JCMD_VRU_READ => {
|
||||
#[cfg(feature = "gui")]
|
||||
#[cfg(all(feature = "gui", not(target_os = "android")))]
|
||||
let index = ui::vru::prompt_for_match(&device.vru.words, device.vi.frame_time);
|
||||
#[cfg(not(feature = "gui"))]
|
||||
#[cfg(any(not(feature = "gui"), target_os = "android"))]
|
||||
let index = 0x7FFF;
|
||||
let num_results = if index == 0x7FFF { 0 } else { 1 };
|
||||
let data: HashMap<usize, u16> = HashMap::from([
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
use crate::{device, savestates};
|
||||
|
||||
pub const COP0_INDEX_REG: u32 = 0;
|
||||
const COP0_RANDOM_REG: u32 = 1;
|
||||
pub const COP0_ENTRYLO0_REG: u32 = 2;
|
||||
pub const COP0_ENTRYLO1_REG: u32 = 3;
|
||||
pub const COP0_CONTEXT_REG: u32 = 4;
|
||||
pub const COP0_PAGEMASK_REG: u32 = 5;
|
||||
const COP0_WIRED_REG: u32 = 6;
|
||||
//const COP0_UNUSED_7: u32 = 7;
|
||||
pub const COP0_BADVADDR_REG: u32 = 8;
|
||||
pub const COP0_COUNT_REG: u32 = 9;
|
||||
pub const COP0_ENTRYHI_REG: u32 = 10;
|
||||
const COP0_COMPARE_REG: u32 = 11;
|
||||
pub const COP0_STATUS_REG: u32 = 12;
|
||||
pub const COP0_CAUSE_REG: u32 = 13;
|
||||
pub const COP0_EPC_REG: u32 = 14;
|
||||
const COP0_PREVID_REG: u32 = 15;
|
||||
const COP0_CONFIG_REG: u32 = 16;
|
||||
pub const COP0_LLADDR_REG: u32 = 17;
|
||||
//const COP0_WATCHLO_REG: u32 = 18;
|
||||
//const COP0_WATCHHI_REG: u32 = 19;
|
||||
pub const COP0_XCONTEXT_REG: u32 = 20;
|
||||
//const COP0_UNUSED_21: u32 = 21;
|
||||
//const COP0_UNUSED_22: u32 = 22;
|
||||
//const COP0_UNUSED_23: u32 = 23;
|
||||
//const COP0_UNUSED_24: u32 = 24;
|
||||
//const COP0_UNUSED_25: u32 = 25;
|
||||
//const COP0_PARITYERR_REG: u32 = 26;
|
||||
//const COP0_CACHEERR_REG: u32 = 27;
|
||||
pub const COP0_TAGLO_REG: u32 = 28;
|
||||
//const COP0_TAGHI_REG: u32 = 29;
|
||||
pub const COP0_ERROREPC_REG: u32 = 30;
|
||||
//const COP0_UNUSED_31: u32 = 31;
|
||||
pub const COP0_REGS_COUNT: u32 = 32;
|
||||
pub const COP0_INDEX_REG: usize = 0;
|
||||
const COP0_RANDOM_REG: usize = 1;
|
||||
pub const COP0_ENTRYLO0_REG: usize = 2;
|
||||
pub const COP0_ENTRYLO1_REG: usize = 3;
|
||||
pub const COP0_CONTEXT_REG: usize = 4;
|
||||
pub const COP0_PAGEMASK_REG: usize = 5;
|
||||
const COP0_WIRED_REG: usize = 6;
|
||||
//const COP0_UNUSED_7: usize = 7;
|
||||
pub const COP0_BADVADDR_REG: usize = 8;
|
||||
pub const COP0_COUNT_REG: usize = 9;
|
||||
pub const COP0_ENTRYHI_REG: usize = 10;
|
||||
const COP0_COMPARE_REG: usize = 11;
|
||||
pub const COP0_STATUS_REG: usize = 12;
|
||||
pub const COP0_CAUSE_REG: usize = 13;
|
||||
pub const COP0_EPC_REG: usize = 14;
|
||||
const COP0_PREVID_REG: usize = 15;
|
||||
const COP0_CONFIG_REG: usize = 16;
|
||||
pub const COP0_LLADDR_REG: usize = 17;
|
||||
//const COP0_WATCHLO_REG: usize = 18;
|
||||
//const COP0_WATCHHI_REG: usize = 19;
|
||||
pub const COP0_XCONTEXT_REG: usize = 20;
|
||||
//const COP0_UNUSED_21: usize = 21;
|
||||
//const COP0_UNUSED_22: usize = 22;
|
||||
//const COP0_UNUSED_23: usize = 23;
|
||||
//const COP0_UNUSED_24: usize = 24;
|
||||
//const COP0_UNUSED_25: usize = 25;
|
||||
//const COP0_PARITYERR_REG: usize = 26;
|
||||
//const COP0_CACHEERR_REG: usize = 27;
|
||||
pub const COP0_TAGLO_REG: usize = 28;
|
||||
//const COP0_TAGHI_REG: usize = 29;
|
||||
pub const COP0_ERROREPC_REG: usize = 30;
|
||||
//const COP0_UNUSED_31: usize = 31;
|
||||
pub const COP0_REGS_COUNT: usize = 32;
|
||||
|
||||
pub const COP0_STATUS_IE: u64 = 1 << 0;
|
||||
pub const COP0_STATUS_EXL: u64 = 1 << 1;
|
||||
@@ -94,8 +94,8 @@ const COP0_TAGLO_REG_MASK: u64 = 0b00001111111111111111111111000000;
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Cop0 {
|
||||
pub reg_latch: u64,
|
||||
pub regs: [u64; COP0_REGS_COUNT as usize],
|
||||
pub reg_write_masks: [u64; COP0_REGS_COUNT as usize],
|
||||
pub regs: [u64; COP0_REGS_COUNT],
|
||||
pub reg_write_masks: [u64; COP0_REGS_COUNT],
|
||||
#[serde(skip, default = "savestates::default_instructions")]
|
||||
pub instrs: [fn(&mut device::Device, u32); 32],
|
||||
#[serde(skip, default = "savestates::default_instructions")]
|
||||
@@ -136,11 +136,11 @@ fn dmtc0(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn tlbr(device: &mut device::Device, _opcode: u32) {
|
||||
device::tlb::read(device, device.cpu.cop0.regs[COP0_INDEX_REG as usize])
|
||||
device::tlb::read(device, device.cpu.cop0.regs[COP0_INDEX_REG])
|
||||
}
|
||||
|
||||
fn tlbwi(device: &mut device::Device, _opcode: u32) {
|
||||
device::tlb::write(device, device.cpu.cop0.regs[COP0_INDEX_REG as usize])
|
||||
device::tlb::write(device, device.cpu.cop0.regs[COP0_INDEX_REG])
|
||||
}
|
||||
|
||||
fn tlbwr(device: &mut device::Device, _opcode: u32) {
|
||||
@@ -153,12 +153,12 @@ fn tlbp(device: &mut device::Device, _opcode: u32) {
|
||||
}
|
||||
|
||||
fn eret(device: &mut device::Device, _opcode: u32) {
|
||||
if device.cpu.cop0.regs[COP0_STATUS_REG as usize] & COP0_STATUS_ERL != 0 {
|
||||
device.cpu.pc = device.cpu.cop0.regs[COP0_ERROREPC_REG as usize];
|
||||
device.cpu.cop0.regs[COP0_STATUS_REG as usize] &= !COP0_STATUS_ERL
|
||||
if device.cpu.cop0.regs[COP0_STATUS_REG] & COP0_STATUS_ERL != 0 {
|
||||
device.cpu.pc = device.cpu.cop0.regs[COP0_ERROREPC_REG];
|
||||
device.cpu.cop0.regs[COP0_STATUS_REG] &= !COP0_STATUS_ERL
|
||||
} else {
|
||||
device.cpu.pc = device.cpu.cop0.regs[COP0_EPC_REG as usize];
|
||||
device.cpu.cop0.regs[COP0_STATUS_REG as usize] &= !COP0_STATUS_EXL
|
||||
device.cpu.pc = device.cpu.cop0.regs[COP0_EPC_REG];
|
||||
device.cpu.cop0.regs[COP0_STATUS_REG] &= !COP0_STATUS_EXL
|
||||
}
|
||||
device.cpu.branch_state.state = device::cpu::State::Exception;
|
||||
device.cpu.llbit = false;
|
||||
@@ -184,7 +184,7 @@ pub fn reserved(device: &mut device::Device, _opcode: u32) {
|
||||
pub fn emux_xdetect(_device: &mut device::Device, _opcode: u32) {}
|
||||
|
||||
fn get_control_registers(device: &device::Device, index: u32) -> u64 {
|
||||
match index {
|
||||
match index as usize {
|
||||
COP0_COUNT_REG => device.cpu.cop0.regs[index as usize] >> 1,
|
||||
COP0_RANDOM_REG => set_random_register(device),
|
||||
7 | 21 | 22 | 23 | 24 | 25 | 31 => device.cpu.cop0.reg_latch,
|
||||
@@ -194,22 +194,18 @@ fn get_control_registers(device: &device::Device, index: u32) -> u64 {
|
||||
|
||||
fn set_control_registers(device: &mut device::Device, index: u32, mut data: u64) {
|
||||
device.cpu.cop0.reg_latch = data;
|
||||
match index {
|
||||
match index as usize {
|
||||
COP0_COUNT_REG => {
|
||||
data &= 0xFFFFFFFF;
|
||||
data <<= 1;
|
||||
device::events::translate_events(
|
||||
device,
|
||||
device.cpu.cop0.regs[COP0_COUNT_REG as usize],
|
||||
data,
|
||||
);
|
||||
device.cpu.cop0.regs[COP0_COUNT_REG as usize] = data;
|
||||
device::events::translate_events(device, device.cpu.cop0.regs[COP0_COUNT_REG], data);
|
||||
device.cpu.cop0.regs[COP0_COUNT_REG] = data;
|
||||
return;
|
||||
}
|
||||
COP0_WIRED_REG => device.cpu.cop0.regs[COP0_RANDOM_REG as usize] = 31,
|
||||
COP0_WIRED_REG => device.cpu.cop0.regs[COP0_RANDOM_REG] = 31,
|
||||
COP0_COMPARE_REG => {
|
||||
data &= 0xFFFFFFFF;
|
||||
let current_count = (device.cpu.cop0.regs[COP0_COUNT_REG as usize] >> 1) & 0xFFFFFFFF;
|
||||
let current_count = (device.cpu.cop0.regs[COP0_COUNT_REG] >> 1) & 0xFFFFFFFF;
|
||||
let mut compare_event_diff = (data as u32).wrapping_sub(current_count as u32);
|
||||
|
||||
if compare_event_diff == 0 {
|
||||
@@ -221,7 +217,7 @@ fn set_control_registers(device: &mut device::Device, index: u32, mut data: u64)
|
||||
device::events::EVENT_TYPE_COMPARE,
|
||||
(compare_event_diff as u64) << 1,
|
||||
);
|
||||
device.cpu.cop0.regs[COP0_CAUSE_REG as usize] &= !COP0_CAUSE_IP7;
|
||||
device.cpu.cop0.regs[COP0_CAUSE_REG] &= !COP0_CAUSE_IP7;
|
||||
}
|
||||
COP0_STATUS_REG
|
||||
if data & COP0_STATUS_FR != device.cpu.cop0.regs[index as usize] & COP0_STATUS_FR =>
|
||||
@@ -239,9 +235,8 @@ fn set_control_registers(device: &mut device::Device, index: u32, mut data: u64)
|
||||
}
|
||||
|
||||
pub fn compare_event(device: &mut device::Device) {
|
||||
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.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] |= device::cop0::COP0_CAUSE_IP7;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &= !device::cop0::COP0_CAUSE_EXCCODE_MASK;
|
||||
|
||||
device::events::create_event_at(
|
||||
device,
|
||||
@@ -252,17 +247,17 @@ pub fn compare_event(device: &mut device::Device) {
|
||||
}
|
||||
|
||||
fn set_random_register(device: &device::Device) -> u64 {
|
||||
if device.cpu.cop0.regs[COP0_WIRED_REG as usize] > 31 {
|
||||
(u64::MAX - device.cpu.cop0.regs[COP0_COUNT_REG as usize]) & 0x3F
|
||||
if device.cpu.cop0.regs[COP0_WIRED_REG] > 31 {
|
||||
(u64::MAX - device.cpu.cop0.regs[COP0_COUNT_REG]) & 0x3F
|
||||
} else {
|
||||
(u64::MAX - device.cpu.cop0.regs[COP0_COUNT_REG as usize])
|
||||
% (32 - device.cpu.cop0.regs[COP0_WIRED_REG as usize])
|
||||
+ device.cpu.cop0.regs[COP0_WIRED_REG as usize]
|
||||
(u64::MAX - device.cpu.cop0.regs[COP0_COUNT_REG])
|
||||
% (32 - device.cpu.cop0.regs[COP0_WIRED_REG])
|
||||
+ device.cpu.cop0.regs[COP0_WIRED_REG]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_cycles(device: &mut device::Device, cycles: u64) {
|
||||
device.cpu.cop0.regs[COP0_COUNT_REG as usize] += cycles // COUNT_REG is shifted right 1 bit when read by MFC0
|
||||
device.cpu.cop0.regs[COP0_COUNT_REG] += cycles // COUNT_REG is shifted right 1 bit when read by MFC0
|
||||
}
|
||||
|
||||
pub fn map_instructions(device: &mut device::Device) {
|
||||
@@ -376,14 +371,14 @@ pub fn init(device: &mut device::Device) {
|
||||
map_instructions(device);
|
||||
|
||||
// taken from VR4300 manual
|
||||
device.cpu.cop0.regs[COP0_RANDOM_REG as usize] = 0b00000000000000000000000000011111;
|
||||
device.cpu.cop0.regs[COP0_CONFIG_REG as usize] = 0b01110000000001101110010001100000;
|
||||
device.cpu.cop0.regs[COP0_STATUS_REG as usize] = 0b00000000010000000000000000000100;
|
||||
device.cpu.cop0.regs[COP0_PREVID_REG as usize] = 0b00000000000000000000101100100010;
|
||||
device.cpu.cop0.regs[COP0_EPC_REG as usize] = 0b11111111111111111111111111111111;
|
||||
device.cpu.cop0.regs[COP0_ERROREPC_REG as usize] = 0b11111111111111111111111111111111;
|
||||
device.cpu.cop0.regs[COP0_BADVADDR_REG as usize] = 0xFFFFFFFF;
|
||||
device.cpu.cop0.regs[COP0_CONTEXT_REG as usize] = 0x7FFFF0;
|
||||
device.cpu.cop0.regs[COP0_RANDOM_REG] = 0b00000000000000000000000000011111;
|
||||
device.cpu.cop0.regs[COP0_CONFIG_REG] = 0b01110000000001101110010001100000;
|
||||
device.cpu.cop0.regs[COP0_STATUS_REG] = 0b00000000010000000000000000000100;
|
||||
device.cpu.cop0.regs[COP0_PREVID_REG] = 0b00000000000000000000101100100010;
|
||||
device.cpu.cop0.regs[COP0_EPC_REG] = 0b11111111111111111111111111111111;
|
||||
device.cpu.cop0.regs[COP0_ERROREPC_REG] = 0b11111111111111111111111111111111;
|
||||
device.cpu.cop0.regs[COP0_BADVADDR_REG] = 0xFFFFFFFF;
|
||||
device.cpu.cop0.regs[COP0_CONTEXT_REG] = 0x7FFFF0;
|
||||
|
||||
device::events::create_event(device, device::events::EVENT_TYPE_COMPARE, u32::MAX as u64)
|
||||
}
|
||||
|
||||
@@ -47,9 +47,7 @@ pub struct Cop1 {
|
||||
}
|
||||
|
||||
pub fn lwc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -79,9 +77,7 @@ pub fn lwc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
pub fn ldc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -118,9 +114,7 @@ pub fn ldc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
pub fn swc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -145,9 +139,7 @@ pub fn swc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
pub fn sdc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -181,9 +173,7 @@ pub fn sdc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn mfc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -193,9 +183,7 @@ fn mfc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn dmfc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -205,9 +193,7 @@ fn dmfc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn cfc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -217,9 +203,7 @@ fn cfc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn dcfc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -229,9 +213,7 @@ fn dcfc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn mtc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -247,9 +229,7 @@ fn mtc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn dmtc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -260,9 +240,7 @@ fn dmtc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn ctc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -274,9 +252,7 @@ fn ctc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn dctc1(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -286,9 +262,7 @@ fn dctc1(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn execute_cop1_b(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -296,9 +270,7 @@ fn execute_cop1_b(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn execute_cop1_s(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -306,9 +278,7 @@ fn execute_cop1_s(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn execute_cop1_d(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -316,9 +286,7 @@ fn execute_cop1_d(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn execute_cop1_l(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -326,9 +294,7 @@ fn execute_cop1_l(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn execute_cop1_w(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -340,9 +306,7 @@ fn unusable(device: &mut device::Device, _opcode: u32) {
|
||||
}
|
||||
|
||||
pub fn reserved(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -406,9 +370,7 @@ fn set_control_registers_fpu(device: &mut device::Device, index: u32, data: u32)
|
||||
|
||||
pub fn set_fpr_single(device: &mut device::Device, index: usize, value: f32, clear_high: bool) {
|
||||
let bytes = value.to_ne_bytes();
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_FR
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_FR == 0 {
|
||||
device.cpu.cop1.fgr32[index] = bytes;
|
||||
} else {
|
||||
let bytes_lo = bytes;
|
||||
@@ -422,9 +384,7 @@ pub fn set_fpr_single(device: &mut device::Device, index: usize, value: f32, cle
|
||||
}
|
||||
|
||||
pub fn get_fpr_single(device: &device::Device, index: usize) -> f32 {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_FR
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_FR == 0 {
|
||||
f32::from_ne_bytes(device.cpu.cop1.fgr32[index])
|
||||
} else {
|
||||
f32::from_ne_bytes(device.cpu.cop1.fgr64[index][0..4].try_into().unwrap())
|
||||
@@ -433,9 +393,7 @@ pub fn get_fpr_single(device: &device::Device, index: usize) -> f32 {
|
||||
|
||||
pub fn set_fpr_double(device: &mut device::Device, index: usize, value: f64) {
|
||||
let bytes = value.to_ne_bytes();
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_FR
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_FR == 0 {
|
||||
device.cpu.cop1.fgr32[index & !1] = bytes[0..4].try_into().unwrap();
|
||||
device.cpu.cop1.fgr32[(index & !1) + 1] = bytes[4..8].try_into().unwrap();
|
||||
} else {
|
||||
@@ -444,9 +402,7 @@ pub fn set_fpr_double(device: &mut device::Device, index: usize, value: f64) {
|
||||
}
|
||||
|
||||
pub fn get_fpr_double(device: &device::Device, index: usize) -> f64 {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_FR
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_FR == 0 {
|
||||
let bytes_lo = device.cpu.cop1.fgr32[index & !1];
|
||||
let bytes_hi = device.cpu.cop1.fgr32[(index & !1) + 1];
|
||||
f64::from_ne_bytes([bytes_lo, bytes_hi].concat().try_into().unwrap())
|
||||
|
||||
@@ -8,9 +8,7 @@ pub struct Cop2 {
|
||||
}
|
||||
|
||||
fn mfc2(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize] =
|
||||
@@ -18,18 +16,14 @@ fn mfc2(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn dmfc2(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize] = device.cpu.cop2.reg_latch
|
||||
}
|
||||
|
||||
fn cfc2(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize] =
|
||||
@@ -37,27 +31,21 @@ fn cfc2(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
fn mtc2(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
device.cpu.cop2.reg_latch = device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize]
|
||||
}
|
||||
|
||||
fn dmtc2(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
device.cpu.cop2.reg_latch = device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize]
|
||||
}
|
||||
|
||||
fn ctc2(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
@@ -65,9 +53,7 @@ fn ctc2(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
|
||||
pub fn reserved(device: &mut device::Device, opcode: u32) {
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
|
||||
return unusable(device, opcode);
|
||||
}
|
||||
|
||||
|
||||
@@ -300,8 +300,7 @@ pub fn run(device: &mut device::Device) {
|
||||
State::Exception => device.cpu.branch_state.state = State::Step,
|
||||
}
|
||||
device::cop0::add_cycles(device, 1);
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize] > device.cpu.next_event_count
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG] > device.cpu.next_event_count {
|
||||
device::events::trigger_event(device)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ pub fn check_relative_idle_loop(device: &mut device::Device, opcode: u32) {
|
||||
device::memory::AccessSize::Word,
|
||||
) == 0
|
||||
{
|
||||
device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize] = device.cpu.next_event_count
|
||||
device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG] = device.cpu.next_event_count
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ fn check_absolute_idle_loop(device: &mut device::Device, target: u64) {
|
||||
device::memory::AccessSize::Word,
|
||||
) == 0
|
||||
{
|
||||
device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize] = device.cpu.next_event_count
|
||||
device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG] = device.cpu.next_event_count
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,17 +647,17 @@ pub fn cache(device: &mut device::Device, opcode: u32) {
|
||||
valid = 1
|
||||
}
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
|
||||
valid << 7,
|
||||
0x80,
|
||||
);
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
|
||||
0,
|
||||
0x40,
|
||||
);
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
|
||||
(device.memory.icache[icache_line].tag >> 4) as u64,
|
||||
0xFFFFF00,
|
||||
)
|
||||
@@ -665,10 +665,9 @@ pub fn cache(device: &mut device::Device, opcode: u32) {
|
||||
0x08 => {
|
||||
//icache store tag
|
||||
device.memory.icache[icache_line].valid =
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize] & 0x80) >> 7 != 0;
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG] & 0x80) >> 7 != 0;
|
||||
device.memory.icache[icache_line].tag =
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize] & 0xFFFFF00) << 4)
|
||||
as u32
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG] & 0xFFFFF00) << 4) as u32
|
||||
}
|
||||
0x10 => {
|
||||
//icache hit invalidate
|
||||
@@ -704,17 +703,17 @@ pub fn cache(device: &mut device::Device, opcode: u32) {
|
||||
dirty = 1
|
||||
}
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
|
||||
valid << 7,
|
||||
0x80,
|
||||
);
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
|
||||
dirty << 6,
|
||||
0x40,
|
||||
);
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
|
||||
(device.memory.dcache[dcache_line].tag >> 4) as u64,
|
||||
0xFFFFF00,
|
||||
)
|
||||
@@ -722,12 +721,11 @@ pub fn cache(device: &mut device::Device, opcode: u32) {
|
||||
0x09 => {
|
||||
//dcache index store tag
|
||||
device.memory.dcache[dcache_line].valid =
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize] & 0x80) >> 7 != 0;
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG] & 0x80) >> 7 != 0;
|
||||
device.memory.dcache[dcache_line].dirty =
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize] & 0x40) >> 6 != 0;
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG] & 0x40) >> 6 != 0;
|
||||
device.memory.dcache[dcache_line].tag =
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize] & 0xFFFFF00) << 4)
|
||||
as u32
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG] & 0xFFFFF00) << 4) as u32
|
||||
}
|
||||
0x0D => {
|
||||
//dcache create dirty exclusive
|
||||
@@ -765,7 +763,7 @@ pub fn cache(device: &mut device::Device, opcode: u32) {
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
panic!("unknown cache code {:#01x}", rt(opcode))
|
||||
panic!("unknown cache code {:x}", rt(opcode))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -787,7 +785,7 @@ pub fn ll(device: &mut device::Device, opcode: u32) {
|
||||
cached,
|
||||
) as i32);
|
||||
device.cpu.llbit = true;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_LLADDR_REG as usize] = phys_address >> 4
|
||||
device.cpu.cop0.regs[device::cop0::COP0_LLADDR_REG] = phys_address >> 4
|
||||
}
|
||||
|
||||
pub fn lld(device: &mut device::Device, opcode: u32) {
|
||||
@@ -816,7 +814,7 @@ pub fn lld(device: &mut device::Device, opcode: u32) {
|
||||
device.cpu.gpr[rt(opcode) as usize] = ((w[0] as u64) << 32) | (w[1]) as u64;
|
||||
|
||||
device.cpu.llbit = true;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_LLADDR_REG as usize] = phys_address >> 4
|
||||
device.cpu.cop0.regs[device::cop0::COP0_LLADDR_REG] = phys_address >> 4
|
||||
}
|
||||
|
||||
pub fn ld(device: &mut device::Device, opcode: u32) {
|
||||
|
||||
@@ -26,7 +26,7 @@ pub struct Event {
|
||||
pub fn create_event(device: &mut device::Device, name: usize, when: u64) {
|
||||
device.cpu.events[name] = Event {
|
||||
enabled: true,
|
||||
count: device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize] + when,
|
||||
count: device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG] + when,
|
||||
};
|
||||
set_next_event(device);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ 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]
|
||||
if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG]
|
||||
& (device::cop0::COP0_STATUS_IE
|
||||
| device::cop0::COP0_STATUS_EXL
|
||||
| device::cop0::COP0_STATUS_ERL))
|
||||
@@ -12,17 +12,14 @@ pub fn check_pending_interrupts(device: &mut device::Device) {
|
||||
return;
|
||||
}
|
||||
|
||||
if device.mi.regs[device::mi::MI_INTR_REG as usize]
|
||||
& 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;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
|
||||
if device.mi.regs[device::mi::MI_INTR_REG] & device.mi.regs[device::mi::MI_INTR_MASK_REG] != 0 {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] |= device::cop0::COP0_CAUSE_IP2;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &=
|
||||
!device::cop0::COP0_CAUSE_EXCCODE_MASK;
|
||||
}
|
||||
|
||||
if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize]
|
||||
& device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize]
|
||||
if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG]
|
||||
& device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG]
|
||||
& device::cop0::COP0_CAUSE_IP_MASK)
|
||||
== 0
|
||||
{
|
||||
@@ -42,43 +39,37 @@ pub fn interrupt_exception(device: &mut device::Device) {
|
||||
}
|
||||
|
||||
pub fn floating_point_exception(device: &mut device::Device) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_FPE;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_FPE;
|
||||
|
||||
exception_general(device, 0x180)
|
||||
}
|
||||
|
||||
pub fn trap_exception(device: &mut device::Device) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_TR;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_TR;
|
||||
|
||||
exception_general(device, 0x180)
|
||||
}
|
||||
|
||||
pub fn syscall_exception(device: &mut device::Device) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_SYS;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_SYS;
|
||||
|
||||
exception_general(device, 0x180)
|
||||
}
|
||||
|
||||
pub fn break_exception(device: &mut device::Device) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_BP;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_BP;
|
||||
|
||||
exception_general(device, 0x180)
|
||||
}
|
||||
|
||||
pub fn reserved_exception(device: &mut device::Device, cop: u64) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_RI | cop;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_RI | cop;
|
||||
|
||||
exception_general(device, 0x180)
|
||||
}
|
||||
|
||||
pub fn cop_unusable_exception(device: &mut device::Device, cop: u64) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_CPU | cop;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_CPU | cop;
|
||||
|
||||
exception_general(device, 0x180)
|
||||
}
|
||||
@@ -89,31 +80,29 @@ pub fn tlb_miss_exception(
|
||||
access_type: device::memory::AccessType,
|
||||
) {
|
||||
if access_type == device::memory::AccessType::Read {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_TLBL
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_TLBL
|
||||
} else {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_TLBS
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_TLBS
|
||||
}
|
||||
|
||||
device.cpu.cop0.regs[device::cop0::COP0_BADVADDR_REG as usize] = address;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_BADVADDR_REG] = address;
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_CONTEXT_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_CONTEXT_REG],
|
||||
address >> 9,
|
||||
device::cop0::COP0_CONTEXT_BADVPN2_MASK,
|
||||
);
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_XCONTEXT_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_XCONTEXT_REG],
|
||||
address >> 9,
|
||||
device::cop0::COP0_XCONTEXT_BADVPN2_MASK,
|
||||
);
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_XCONTEXT_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_XCONTEXT_REG],
|
||||
address >> 31,
|
||||
device::cop0::COP0_XCONTEXT_REGION_MASK,
|
||||
);
|
||||
device::memory::masked_write_64(
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize],
|
||||
&mut device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG],
|
||||
address,
|
||||
0xFFFFE000,
|
||||
);
|
||||
@@ -124,7 +113,7 @@ pub fn tlb_miss_exception(
|
||||
if address & !3 >= i.start_even && address & !3 <= i.end_even {
|
||||
valid = i.v_even != 0;
|
||||
if valid && access_type == device::memory::AccessType::Write && i.d_even == 0 {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_MOD;
|
||||
valid = false;
|
||||
}
|
||||
@@ -133,7 +122,7 @@ pub fn tlb_miss_exception(
|
||||
if address & !3 >= i.start_odd && address & !3 <= i.end_odd {
|
||||
valid = i.v_odd != 0;
|
||||
if valid && access_type == device::memory::AccessType::Write && i.d_odd == 0 {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] =
|
||||
device::cop0::COP0_CAUSE_EXCCODE_MOD;
|
||||
valid = false;
|
||||
}
|
||||
@@ -141,8 +130,7 @@ pub fn tlb_miss_exception(
|
||||
}
|
||||
}
|
||||
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_EXL
|
||||
== 0
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_EXL == 0
|
||||
&& valid
|
||||
{
|
||||
vector_offset = 0;
|
||||
@@ -152,17 +140,17 @@ pub fn tlb_miss_exception(
|
||||
}
|
||||
|
||||
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_CAUSE_REG] &= !device::cop0::COP0_CAUSE_IP4;
|
||||
|
||||
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] |= device::cop0::COP0_STATUS_ERL
|
||||
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] |= 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_STATUS_REG] &= !device::cop0::COP0_STATUS_TS;
|
||||
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ERROREPC_REG as usize] = device.cpu.pc;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ERROREPC_REG] = 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.rsp.regs2[device::rsp_interface::SP_PC_REG] = 0;
|
||||
|
||||
device::pif::reset_pif(device, true);
|
||||
|
||||
@@ -170,25 +158,19 @@ pub fn reset_event(device: &mut device::Device) {
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
device.cpu.cop0.regs[device::cop0::COP0_EPC_REG as usize] = device.cpu.pc;
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_EXL == 0 {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_EPC_REG] = device.cpu.pc;
|
||||
if device::cpu::in_delay_slot(device) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] |=
|
||||
device::cop0::COP0_CAUSE_BD;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_EPC_REG as usize] -= 4;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] |= device::cop0::COP0_CAUSE_BD;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_EPC_REG] -= 4;
|
||||
} else {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
|
||||
!device::cop0::COP0_CAUSE_BD;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &= !device::cop0::COP0_CAUSE_BD;
|
||||
}
|
||||
}
|
||||
|
||||
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] |= device::cop0::COP0_STATUS_EXL;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] |= device::cop0::COP0_STATUS_EXL;
|
||||
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_BEV
|
||||
== 0
|
||||
{
|
||||
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_BEV == 0 {
|
||||
device.cpu.pc = device::cpu_instructions::se32((0x80000000 + vector_offset) as i32);
|
||||
} else {
|
||||
device.cpu.pc = device::cpu_instructions::se32((0xBFC00200 + vector_offset) as i32);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::device;
|
||||
|
||||
pub const MI_INIT_MODE_REG: u32 = 0;
|
||||
const MI_VERSION_REG: u32 = 1;
|
||||
pub const MI_INTR_REG: u32 = 2;
|
||||
pub const MI_INTR_MASK_REG: u32 = 3;
|
||||
pub const MI_REGS_COUNT: u32 = 4;
|
||||
pub const MI_INIT_MODE_REG: usize = 0;
|
||||
const MI_VERSION_REG: usize = 1;
|
||||
pub const MI_INTR_REG: usize = 2;
|
||||
pub const MI_INTR_MASK_REG: usize = 3;
|
||||
pub const MI_REGS_COUNT: usize = 4;
|
||||
|
||||
/* read */
|
||||
pub const MI_INTR_SP: u32 = 1 << 0;
|
||||
@@ -46,7 +46,7 @@ pub const MI_INIT_LENGTH_MASK: u32 = 0b1111111;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Mi {
|
||||
pub regs: [u32; MI_REGS_COUNT as usize],
|
||||
pub regs: [u32; MI_REGS_COUNT],
|
||||
}
|
||||
|
||||
pub fn read_regs(
|
||||
@@ -60,46 +60,45 @@ pub fn read_regs(
|
||||
|
||||
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
MI_INIT_MODE_REG => update_init_mode(device, value),
|
||||
MI_INTR_MASK_REG => update_intr_mask(device, value),
|
||||
_ => device::memory::masked_write_32(&mut device.mi.regs[reg as usize], value, mask),
|
||||
}
|
||||
|
||||
if device.mi.regs[MI_INTR_REG as usize] & device.mi.regs[MI_INTR_MASK_REG as usize] == 0 {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
|
||||
!device::cop0::COP0_CAUSE_IP2;
|
||||
if device.mi.regs[MI_INTR_REG] & device.mi.regs[MI_INTR_MASK_REG] == 0 {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &= !device::cop0::COP0_CAUSE_IP2;
|
||||
}
|
||||
device::exceptions::check_pending_interrupts(device)
|
||||
}
|
||||
|
||||
fn update_init_mode(device: &mut device::Device, w: u32) {
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] &= !MI_INIT_LENGTH_MASK;
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] |= w & MI_INIT_LENGTH_MASK;
|
||||
device.mi.regs[MI_INIT_MODE_REG] &= !MI_INIT_LENGTH_MASK;
|
||||
device.mi.regs[MI_INIT_MODE_REG] |= w & MI_INIT_LENGTH_MASK;
|
||||
|
||||
if w & MI_CLR_INIT != 0 {
|
||||
for i in 0..(0x3F00000 >> 16) {
|
||||
device.memory.memory_map_write[i] = device::rdram::write_mem;
|
||||
}
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] &= !MI_INIT_MODE
|
||||
device.mi.regs[MI_INIT_MODE_REG] &= !MI_INIT_MODE
|
||||
}
|
||||
if w & MI_SET_INIT != 0 {
|
||||
for i in 0..(0x3F00000 >> 16) {
|
||||
device.memory.memory_map_write[i] = device::rdram::write_mem_repeat;
|
||||
}
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] |= MI_INIT_MODE
|
||||
device.mi.regs[MI_INIT_MODE_REG] |= MI_INIT_MODE
|
||||
}
|
||||
if w & MI_CLR_EBUS != 0 {
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] &= !MI_EBUS_MODE
|
||||
device.mi.regs[MI_INIT_MODE_REG] &= !MI_EBUS_MODE
|
||||
}
|
||||
if w & MI_SET_EBUS != 0 {
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] |= MI_EBUS_MODE
|
||||
device.mi.regs[MI_INIT_MODE_REG] |= MI_EBUS_MODE
|
||||
}
|
||||
if w & MI_CLR_RDRAM != 0 {
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] &= !MI_RDRAM_MODE
|
||||
device.mi.regs[MI_INIT_MODE_REG] &= !MI_RDRAM_MODE
|
||||
}
|
||||
if w & MI_SET_RDRAM != 0 {
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] |= MI_RDRAM_MODE
|
||||
device.mi.regs[MI_INIT_MODE_REG] |= MI_RDRAM_MODE
|
||||
}
|
||||
|
||||
if w & MI_CLR_DP_INTR != 0 {
|
||||
@@ -109,57 +108,56 @@ fn update_init_mode(device: &mut device::Device, w: u32) {
|
||||
|
||||
fn update_intr_mask(device: &mut device::Device, w: u32) {
|
||||
if w & MI_CLR_SP != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_SP
|
||||
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_SP
|
||||
}
|
||||
if w & MI_SET_SP != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_SP
|
||||
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_SP
|
||||
}
|
||||
if w & MI_CLR_SI != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_SI
|
||||
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_SI
|
||||
}
|
||||
if w & MI_SET_SI != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_SI
|
||||
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_SI
|
||||
}
|
||||
if w & MI_CLR_AI != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_AI
|
||||
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_AI
|
||||
}
|
||||
if w & MI_SET_AI != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_AI
|
||||
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_AI
|
||||
}
|
||||
if w & MI_CLR_VI != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_VI
|
||||
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_VI
|
||||
}
|
||||
if w & MI_SET_VI != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_VI
|
||||
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_VI
|
||||
}
|
||||
if w & MI_CLR_PI != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_PI
|
||||
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_PI
|
||||
}
|
||||
if w & MI_SET_PI != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_PI
|
||||
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_PI
|
||||
}
|
||||
if w & MI_CLR_DP != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_DP
|
||||
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_DP
|
||||
}
|
||||
if w & MI_SET_DP != 0 {
|
||||
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_DP
|
||||
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_DP
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_rcp_interrupt(device: &mut device::Device, interrupt: u32) {
|
||||
device.mi.regs[MI_INTR_REG as usize] &= !interrupt;
|
||||
device.mi.regs[MI_INTR_REG] &= !interrupt;
|
||||
|
||||
if device.mi.regs[MI_INTR_REG as usize] & device.mi.regs[MI_INTR_MASK_REG as usize] == 0 {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
|
||||
!device::cop0::COP0_CAUSE_IP2;
|
||||
if device.mi.regs[MI_INTR_REG] & device.mi.regs[MI_INTR_MASK_REG] == 0 {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &= !device::cop0::COP0_CAUSE_IP2;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_rcp_interrupt(device: &mut device::Device, interrupt: u32) {
|
||||
device.mi.regs[MI_INTR_REG as usize] |= interrupt;
|
||||
device.mi.regs[MI_INTR_REG] |= interrupt;
|
||||
device::exceptions::check_pending_interrupts(device)
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
device.mi.regs[MI_VERSION_REG as usize] = 0x02020102
|
||||
device.mi.regs[MI_VERSION_REG] = 0x02020102
|
||||
}
|
||||
|
||||
@@ -2,21 +2,21 @@ use crate::device;
|
||||
use crate::ui;
|
||||
use rand::Rng;
|
||||
|
||||
const PI_DRAM_ADDR_REG: u32 = 0;
|
||||
const PI_CART_ADDR_REG: u32 = 1;
|
||||
const PI_RD_LEN_REG: u32 = 2;
|
||||
const PI_WR_LEN_REG: u32 = 3;
|
||||
pub const PI_STATUS_REG: u32 = 4;
|
||||
const PI_BSD_DOM1_LAT_REG: u32 = 5;
|
||||
const PI_BSD_DOM1_PWD_REG: u32 = 6;
|
||||
const PI_BSD_DOM1_PGS_REG: u32 = 7;
|
||||
const PI_BSD_DOM1_RLS_REG: u32 = 8;
|
||||
const PI_BSD_DOM2_LAT_REG: u32 = 9;
|
||||
const PI_BSD_DOM2_PWD_REG: u32 = 10;
|
||||
const PI_BSD_DOM2_PGS_REG: u32 = 11;
|
||||
const PI_BSD_DOM2_RLS_REG: u32 = 12;
|
||||
//const UNKNOWN_REG: u32 = 13; //LibDragon
|
||||
pub const PI_REGS_COUNT: u32 = 14;
|
||||
const PI_DRAM_ADDR_REG: usize = 0;
|
||||
const PI_CART_ADDR_REG: usize = 1;
|
||||
const PI_RD_LEN_REG: usize = 2;
|
||||
const PI_WR_LEN_REG: usize = 3;
|
||||
pub const PI_STATUS_REG: usize = 4;
|
||||
const PI_BSD_DOM1_LAT_REG: usize = 5;
|
||||
const PI_BSD_DOM1_PWD_REG: usize = 6;
|
||||
const PI_BSD_DOM1_PGS_REG: usize = 7;
|
||||
const PI_BSD_DOM1_RLS_REG: usize = 8;
|
||||
const PI_BSD_DOM2_LAT_REG: usize = 9;
|
||||
const PI_BSD_DOM2_PWD_REG: usize = 10;
|
||||
const PI_BSD_DOM2_PGS_REG: usize = 11;
|
||||
const PI_BSD_DOM2_RLS_REG: usize = 12;
|
||||
//const UNKNOWN_REG: usize = 13; //LibDragon
|
||||
pub const PI_REGS_COUNT: usize = 14;
|
||||
|
||||
/* PI_STATUS - read */
|
||||
const PI_STATUS_DMA_BUSY: u32 = 1 << 0;
|
||||
@@ -30,7 +30,7 @@ const PI_STATUS_CLR_INTR: u32 = 1 << 1;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Pi {
|
||||
pub regs: [u32; PI_REGS_COUNT as usize],
|
||||
pub regs: [u32; PI_REGS_COUNT],
|
||||
}
|
||||
|
||||
struct PiHandler {
|
||||
@@ -45,7 +45,7 @@ pub fn read_regs(
|
||||
) -> u32 {
|
||||
device::cop0::add_cycles(device, 20);
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
PI_WR_LEN_REG | PI_RD_LEN_REG => 0x7F,
|
||||
PI_CART_ADDR_REG => device.pi.regs[reg as usize] & 0xFFFFFFFE,
|
||||
PI_DRAM_ADDR_REG => device.pi.regs[reg as usize] & 0xFFFFFE,
|
||||
@@ -54,11 +54,11 @@ pub fn read_regs(
|
||||
}
|
||||
|
||||
fn dma_read(device: &mut device::Device) {
|
||||
let handler = get_handler(device.pi.regs[PI_CART_ADDR_REG as usize]);
|
||||
let handler = get_handler(device.pi.regs[PI_CART_ADDR_REG]);
|
||||
|
||||
let cart_addr = device.pi.regs[PI_CART_ADDR_REG as usize] & !1;
|
||||
let dram_addr = device.pi.regs[PI_DRAM_ADDR_REG as usize] & 0xFFFFFE;
|
||||
let mut length = (device.pi.regs[PI_RD_LEN_REG as usize] & 0xFFFFFF) + 1;
|
||||
let cart_addr = device.pi.regs[PI_CART_ADDR_REG] & !1;
|
||||
let dram_addr = device.pi.regs[PI_DRAM_ADDR_REG] & 0xFFFFFE;
|
||||
let mut length = (device.pi.regs[PI_RD_LEN_REG] & 0xFFFFFF) + 1;
|
||||
|
||||
/* PI seems to treat the first 128 bytes differently, see https://n64brew.dev/wiki/Peripheral_Interface#Unaligned_DMA_transfer */
|
||||
if length >= 0x7f && (length & 1) != 0 {
|
||||
@@ -70,20 +70,18 @@ fn dma_read(device: &mut device::Device) {
|
||||
device::events::create_event(device, device::events::EVENT_TYPE_PI, cycles);
|
||||
|
||||
/* Update PI_DRAM_ADDR_REG and PI_CART_ADDR_REG */
|
||||
device.pi.regs[PI_DRAM_ADDR_REG as usize] =
|
||||
(device.pi.regs[PI_DRAM_ADDR_REG as usize] + length + 7) & !7;
|
||||
device.pi.regs[PI_CART_ADDR_REG as usize] =
|
||||
(device.pi.regs[PI_CART_ADDR_REG as usize] + length + 1) & !1;
|
||||
device.pi.regs[PI_DRAM_ADDR_REG] = (device.pi.regs[PI_DRAM_ADDR_REG] + length + 7) & !7;
|
||||
device.pi.regs[PI_CART_ADDR_REG] = (device.pi.regs[PI_CART_ADDR_REG] + length + 1) & !1;
|
||||
|
||||
device.pi.regs[PI_STATUS_REG as usize] |= PI_STATUS_DMA_BUSY
|
||||
device.pi.regs[PI_STATUS_REG] |= PI_STATUS_DMA_BUSY
|
||||
}
|
||||
|
||||
fn dma_write(device: &mut device::Device) {
|
||||
let handler = get_handler(device.pi.regs[PI_CART_ADDR_REG as usize]);
|
||||
let handler = get_handler(device.pi.regs[PI_CART_ADDR_REG]);
|
||||
|
||||
let cart_addr = device.pi.regs[PI_CART_ADDR_REG as usize] & !1;
|
||||
let dram_addr = device.pi.regs[PI_DRAM_ADDR_REG as usize] & 0xFFFFFE;
|
||||
let mut length = (device.pi.regs[PI_WR_LEN_REG as usize] & 0xFFFFFF) + 1;
|
||||
let cart_addr = device.pi.regs[PI_CART_ADDR_REG] & !1;
|
||||
let dram_addr = device.pi.regs[PI_DRAM_ADDR_REG] & 0xFFFFFE;
|
||||
let mut length = (device.pi.regs[PI_WR_LEN_REG] & 0xFFFFFF) + 1;
|
||||
|
||||
/* PI seems to treat the first 128 bytes differently, see https://n64brew.dev/wiki/Peripheral_Interface#Unaligned_DMA_transfer */
|
||||
if length >= 0x7f && (length & 1) != 0 {
|
||||
@@ -101,12 +99,10 @@ fn dma_write(device: &mut device::Device) {
|
||||
device::events::create_event(device, device::events::EVENT_TYPE_PI, cycles);
|
||||
|
||||
/* Update PI_DRAM_ADDR_REG and PI_CART_ADDR_REG */
|
||||
device.pi.regs[PI_DRAM_ADDR_REG as usize] =
|
||||
(device.pi.regs[PI_DRAM_ADDR_REG as usize] + length + 7) & !7;
|
||||
device.pi.regs[PI_CART_ADDR_REG as usize] =
|
||||
(device.pi.regs[PI_CART_ADDR_REG as usize] + length + 1) & !1;
|
||||
device.pi.regs[PI_DRAM_ADDR_REG] = (device.pi.regs[PI_DRAM_ADDR_REG] + length + 7) & !7;
|
||||
device.pi.regs[PI_CART_ADDR_REG] = (device.pi.regs[PI_CART_ADDR_REG] + length + 1) & !1;
|
||||
|
||||
device.pi.regs[PI_STATUS_REG as usize] |= PI_STATUS_DMA_BUSY
|
||||
device.pi.regs[PI_STATUS_REG] |= PI_STATUS_DMA_BUSY
|
||||
}
|
||||
|
||||
fn get_handler(address: u32) -> PiHandler {
|
||||
@@ -138,7 +134,7 @@ fn get_handler(address: u32) -> PiHandler {
|
||||
|
||||
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
PI_RD_LEN_REG => {
|
||||
device::memory::masked_write_32(&mut device.pi.regs[reg as usize], value, mask);
|
||||
device::pi::dma_read(device)
|
||||
@@ -153,7 +149,7 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
device::mi::clear_rcp_interrupt(device, device::mi::MI_INTR_PI);
|
||||
}
|
||||
if value & mask & PI_STATUS_RESET != 0 {
|
||||
device.pi.regs[PI_STATUS_REG as usize] = 0
|
||||
device.pi.regs[PI_STATUS_REG] = 0
|
||||
}
|
||||
}
|
||||
PI_BSD_DOM1_LAT_REG | PI_BSD_DOM1_PWD_REG | PI_BSD_DOM2_LAT_REG | PI_BSD_DOM2_PWD_REG => {
|
||||
@@ -179,15 +175,15 @@ pub fn calculate_cycles(device: &mut device::Device, domain: i32, length: u32) -
|
||||
let page_size_base: f64 = 2.0;
|
||||
|
||||
if domain == 1 {
|
||||
latency = (device.pi.regs[PI_BSD_DOM1_LAT_REG as usize] + 1) as f64;
|
||||
pulse_width = (device.pi.regs[PI_BSD_DOM1_PWD_REG as usize] + 1) as f64;
|
||||
release = (device.pi.regs[PI_BSD_DOM1_RLS_REG as usize] + 1) as f64;
|
||||
page_size = page_size_base.powf((device.pi.regs[PI_BSD_DOM1_PGS_REG as usize] + 2) as f64);
|
||||
latency = (device.pi.regs[PI_BSD_DOM1_LAT_REG] + 1) as f64;
|
||||
pulse_width = (device.pi.regs[PI_BSD_DOM1_PWD_REG] + 1) as f64;
|
||||
release = (device.pi.regs[PI_BSD_DOM1_RLS_REG] + 1) as f64;
|
||||
page_size = page_size_base.powf((device.pi.regs[PI_BSD_DOM1_PGS_REG] + 2) as f64);
|
||||
} else if domain == 2 {
|
||||
latency = (device.pi.regs[PI_BSD_DOM2_LAT_REG as usize] + 1) as f64;
|
||||
pulse_width = (device.pi.regs[PI_BSD_DOM2_PWD_REG as usize] + 1) as f64;
|
||||
release = (device.pi.regs[PI_BSD_DOM2_RLS_REG as usize] + 1) as f64;
|
||||
page_size = page_size_base.powf((device.pi.regs[PI_BSD_DOM2_PGS_REG as usize] + 2) as f64);
|
||||
latency = (device.pi.regs[PI_BSD_DOM2_LAT_REG] + 1) as f64;
|
||||
pulse_width = (device.pi.regs[PI_BSD_DOM2_PWD_REG] + 1) as f64;
|
||||
release = (device.pi.regs[PI_BSD_DOM2_RLS_REG] + 1) as f64;
|
||||
page_size = page_size_base.powf((device.pi.regs[PI_BSD_DOM2_PGS_REG] + 2) as f64);
|
||||
} else {
|
||||
panic!("unknown pi dma")
|
||||
}
|
||||
@@ -201,8 +197,8 @@ pub fn calculate_cycles(device: &mut device::Device, domain: i32, length: u32) -
|
||||
}
|
||||
|
||||
pub fn dma_event(device: &mut device::Device) {
|
||||
device.pi.regs[PI_STATUS_REG as usize] &= !(PI_STATUS_DMA_BUSY | PI_STATUS_IO_BUSY);
|
||||
device.pi.regs[PI_STATUS_REG as usize] |= PI_STATUS_INTERRUPT;
|
||||
device.pi.regs[PI_STATUS_REG] &= !(PI_STATUS_DMA_BUSY | PI_STATUS_IO_BUSY);
|
||||
device.pi.regs[PI_STATUS_REG] |= PI_STATUS_INTERRUPT;
|
||||
|
||||
device::mi::set_rcp_interrupt(device, device::mi::MI_INTR_PI)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
|
||||
|
||||
device.si.dma_dir = device::si::DmaDir::Write;
|
||||
device::events::create_event(device, device::events::EVENT_TYPE_SI, 3200); //based on https://github.com/rasky/n64-systembench
|
||||
device.si.regs[device::si::SI_STATUS_REG as usize] |=
|
||||
device.si.regs[device::si::SI_STATUS_REG] |=
|
||||
device::si::SI_STATUS_DMA_BUSY | device::si::SI_STATUS_IO_BUSY
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
use crate::{device, savestates, ui};
|
||||
|
||||
pub const DPC_START_REG: u32 = 0;
|
||||
pub const DPC_END_REG: u32 = 1;
|
||||
pub const DPC_CURRENT_REG: u32 = 2;
|
||||
pub const DPC_STATUS_REG: u32 = 3;
|
||||
const DPC_CLOCK_REG: u32 = 4;
|
||||
const DPC_BUFBUSY_REG: u32 = 5;
|
||||
const DPC_PIPEBUSY_REG: u32 = 6;
|
||||
const DPC_TMEM_REG: u32 = 7;
|
||||
pub const DPC_REGS_COUNT: u32 = 8;
|
||||
pub const DPC_START_REG: usize = 0;
|
||||
pub const DPC_END_REG: usize = 1;
|
||||
pub const DPC_CURRENT_REG: usize = 2;
|
||||
pub const DPC_STATUS_REG: usize = 3;
|
||||
const DPC_CLOCK_REG: usize = 4;
|
||||
const DPC_BUFBUSY_REG: usize = 5;
|
||||
const DPC_PIPEBUSY_REG: usize = 6;
|
||||
const DPC_TMEM_REG: usize = 7;
|
||||
pub const DPC_REGS_COUNT: usize = 8;
|
||||
|
||||
//const DPS_TBIST_REG: u32 = 0;
|
||||
//const DPS_TEST_MODE_REG: u32 = 1;
|
||||
//const DPS_BUFTEST_ADDR_REG: u32 = 2;
|
||||
//const DPS_BUFTEST_DATA_REG: u32 = 3;
|
||||
pub const DPS_REGS_COUNT: u32 = 4;
|
||||
//const DPS_TBIST_REG: usize = 0;
|
||||
//const DPS_TEST_MODE_REG: usize = 1;
|
||||
//const DPS_BUFTEST_ADDR_REG: usize = 2;
|
||||
//const DPS_BUFTEST_DATA_REG: usize = 3;
|
||||
pub const DPS_REGS_COUNT: usize = 4;
|
||||
|
||||
/* DPC status - read */
|
||||
const DPC_STATUS_XBUS_DMEM_DMA: u32 = 1 << 0;
|
||||
@@ -42,8 +42,8 @@ const DPC_CLR_CLOCK_CTR: u32 = 1 << 9;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Rdp {
|
||||
pub regs_dpc: [u32; DPC_REGS_COUNT as usize],
|
||||
pub regs_dps: [u32; DPS_REGS_COUNT as usize],
|
||||
pub regs_dpc: [u32; DPC_REGS_COUNT],
|
||||
pub regs_dps: [u32; DPS_REGS_COUNT],
|
||||
pub wait_frozen: bool,
|
||||
pub last_status_value: u32,
|
||||
}
|
||||
@@ -57,7 +57,7 @@ pub fn read_regs_dpc(
|
||||
device::cop0::add_cycles(device, 20);
|
||||
}
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
DPC_STATUS_REG => {
|
||||
let value =
|
||||
device.rdp.regs_dpc[reg as usize] & (DPC_STATUS_PIPE_BUSY | DPC_STATUS_CMD_BUSY);
|
||||
@@ -80,18 +80,18 @@ pub fn read_regs_dpc(
|
||||
|
||||
pub fn write_regs_dpc(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
DPC_CURRENT_REG | DPC_CLOCK_REG | DPC_BUFBUSY_REG | DPC_PIPEBUSY_REG | DPC_TMEM_REG => {}
|
||||
DPC_STATUS_REG => update_dpc_status(device, value & mask),
|
||||
DPC_START_REG => {
|
||||
if (device.rdp.regs_dpc[DPC_STATUS_REG as usize] & DPC_STATUS_START_VALID) == 0 {
|
||||
if (device.rdp.regs_dpc[DPC_STATUS_REG] & DPC_STATUS_START_VALID) == 0 {
|
||||
device::memory::masked_write_32(
|
||||
&mut device.rdp.regs_dpc[reg as usize],
|
||||
value & 0xFFFFF8,
|
||||
mask,
|
||||
)
|
||||
}
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |= DPC_STATUS_START_VALID
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] |= DPC_STATUS_START_VALID
|
||||
}
|
||||
DPC_END_REG => {
|
||||
device::memory::masked_write_32(
|
||||
@@ -99,12 +99,11 @@ pub fn write_regs_dpc(device: &mut device::Device, address: u64, value: u32, mas
|
||||
value & 0xFFFFF8,
|
||||
mask,
|
||||
);
|
||||
if (device.rdp.regs_dpc[DPC_STATUS_REG as usize] & DPC_STATUS_START_VALID) != 0 {
|
||||
device.rdp.regs_dpc[DPC_CURRENT_REG as usize] =
|
||||
device.rdp.regs_dpc[DPC_START_REG as usize];
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_START_VALID
|
||||
if (device.rdp.regs_dpc[DPC_STATUS_REG] & DPC_STATUS_START_VALID) != 0 {
|
||||
device.rdp.regs_dpc[DPC_CURRENT_REG] = device.rdp.regs_dpc[DPC_START_REG];
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_START_VALID
|
||||
}
|
||||
if device.rdp.regs_dpc[DPC_STATUS_REG as usize] & DPC_STATUS_FREEZE == 0 {
|
||||
if device.rdp.regs_dpc[DPC_STATUS_REG] & DPC_STATUS_FREEZE == 0 {
|
||||
run_rdp(device)
|
||||
} else {
|
||||
device.rdp.wait_frozen = true;
|
||||
@@ -116,9 +115,9 @@ pub fn write_regs_dpc(device: &mut device::Device, address: u64, value: u32, mas
|
||||
|
||||
fn run_rdp(device: &mut device::Device) {
|
||||
let timer = ui::video::process_rdp_list();
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |=
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] |=
|
||||
DPC_STATUS_START_GCLK | DPC_STATUS_PIPE_BUSY | DPC_STATUS_CMD_BUSY;
|
||||
device.rdp.regs_dpc[DPC_PIPEBUSY_REG as usize] = 0xFFFFFF;
|
||||
device.rdp.regs_dpc[DPC_PIPEBUSY_REG] = 0xFFFFFF;
|
||||
|
||||
if timer != 0 {
|
||||
if device.save_state {
|
||||
@@ -154,59 +153,59 @@ pub fn write_regs_dps(device: &mut device::Device, address: u64, value: u32, mas
|
||||
fn update_dpc_status(device: &mut device::Device, w: u32) {
|
||||
/* clear / set xbus_dmem_dma */
|
||||
if w & DPC_CLR_XBUS_DMEM_DMA != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_XBUS_DMEM_DMA
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_XBUS_DMEM_DMA
|
||||
}
|
||||
if w & DPC_SET_XBUS_DMEM_DMA != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |= DPC_STATUS_XBUS_DMEM_DMA
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] |= DPC_STATUS_XBUS_DMEM_DMA
|
||||
}
|
||||
|
||||
/* clear / set freeze */
|
||||
if w & DPC_CLR_FREEZE != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_FREEZE;
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_FREEZE;
|
||||
if device.rdp.wait_frozen {
|
||||
run_rdp(device);
|
||||
device.rdp.wait_frozen = false;
|
||||
}
|
||||
}
|
||||
if w & DPC_SET_FREEZE != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |= DPC_STATUS_FREEZE;
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] |= DPC_STATUS_FREEZE;
|
||||
}
|
||||
|
||||
/* clear / set flush */
|
||||
if w & DPC_CLR_FLUSH != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_FLUSH
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_FLUSH
|
||||
}
|
||||
if w & DPC_SET_FLUSH != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |= DPC_STATUS_FLUSH
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] |= DPC_STATUS_FLUSH
|
||||
}
|
||||
|
||||
if w & DPC_CLR_TMEM_CTR != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_TMEM_BUSY;
|
||||
device.rdp.regs_dpc[DPC_TMEM_REG as usize] = 0
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_TMEM_BUSY;
|
||||
device.rdp.regs_dpc[DPC_TMEM_REG] = 0
|
||||
}
|
||||
if w & DPC_CLR_PIPE_CTR != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_PIPE_BUSY;
|
||||
device.rdp.regs_dpc[DPC_PIPEBUSY_REG as usize] = 0
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_PIPE_BUSY;
|
||||
device.rdp.regs_dpc[DPC_PIPEBUSY_REG] = 0
|
||||
}
|
||||
if w & DPC_CLR_CMD_CTR != 0 {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_CMD_BUSY;
|
||||
device.rdp.regs_dpc[DPC_BUFBUSY_REG as usize] = 0
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_CMD_BUSY;
|
||||
device.rdp.regs_dpc[DPC_BUFBUSY_REG] = 0
|
||||
}
|
||||
|
||||
/* clear clock counter */
|
||||
if w & DPC_CLR_CLOCK_CTR != 0 {
|
||||
device.rdp.regs_dpc[DPC_CLOCK_REG as usize] = 0
|
||||
device.rdp.regs_dpc[DPC_CLOCK_REG] = 0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |=
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] |=
|
||||
DPC_STATUS_START_GCLK | DPC_STATUS_PIPE_BUSY | DPC_STATUS_CBUF_READY;
|
||||
device.rdp.regs_dpc[DPC_PIPEBUSY_REG as usize] = 0xFFFFFF;
|
||||
device.rdp.regs_dpc[DPC_PIPEBUSY_REG] = 0xFFFFFF;
|
||||
}
|
||||
|
||||
pub fn rdp_interrupt_event(device: &mut device::Device) {
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &=
|
||||
device.rdp.regs_dpc[DPC_STATUS_REG] &=
|
||||
!(DPC_STATUS_START_GCLK | DPC_STATUS_PIPE_BUSY | DPC_STATUS_CMD_BUSY);
|
||||
|
||||
device::mi::set_rcp_interrupt(device, device::mi::MI_INTR_DP)
|
||||
|
||||
@@ -3,17 +3,17 @@ use crate::retroachievements;
|
||||
use crate::ui;
|
||||
use std::alloc::{Layout, alloc_zeroed};
|
||||
|
||||
//const RDRAM_CONFIG_REG: u32 = 0;
|
||||
//const RDRAM_DEVICE_ID_REG: u32 = 1;
|
||||
//const RDRAM_DELAY_REG: u32 = 2;
|
||||
const RDRAM_MODE_REG: u32 = 3;
|
||||
//const RDRAM_REF_INTERVAL_REG: u32 = 4;
|
||||
//const RDRAM_REF_ROW_REG: u32 = 5;
|
||||
//const RDRAM_RAS_INTERVAL_REG: u32 = 6;
|
||||
//const RDRAM_MIN_INTERVAL_REG: u32 = 7;
|
||||
//const RDRAM_ADDR_SELECT_REG: u32 = 8;
|
||||
//const RDRAM_DEVICE_MANUF_REG: u32 = 9;
|
||||
pub const RDRAM_REGS_COUNT: u32 = 10;
|
||||
//const RDRAM_CONFIG_REG: usize = 0;
|
||||
//const RDRAM_DEVICE_ID_REG: usize = 1;
|
||||
//const RDRAM_DELAY_REG: usize = 2;
|
||||
const RDRAM_MODE_REG: usize = 3;
|
||||
//const RDRAM_REF_INTERVAL_REG: usize = 4;
|
||||
//const RDRAM_REF_ROW_REG: usize = 5;
|
||||
//const RDRAM_RAS_INTERVAL_REG: usize = 6;
|
||||
//const RDRAM_MIN_INTERVAL_REG: usize = 7;
|
||||
//const RDRAM_ADDR_SELECT_REG: usize = 8;
|
||||
//const RDRAM_DEVICE_MANUF_REG: usize = 9;
|
||||
pub const RDRAM_REGS_COUNT: usize = 10;
|
||||
|
||||
pub const RDRAM_MASK: usize = 0xFFFFFF;
|
||||
|
||||
@@ -21,7 +21,7 @@ pub const RDRAM_MASK: usize = 0xFFFFFF;
|
||||
pub struct Rdram {
|
||||
pub mem: Vec<u8>,
|
||||
pub size: u32,
|
||||
pub regs: [[u32; RDRAM_REGS_COUNT as usize]; 4],
|
||||
pub regs: [[u32; RDRAM_REGS_COUNT]; 4],
|
||||
}
|
||||
|
||||
pub fn read_mem_fast(
|
||||
@@ -91,9 +91,8 @@ pub fn write_mem_repeat(device: &mut device::Device, address: u64, value: u32, m
|
||||
panic!("RDRAM write_mem_repeat called with mask {:#x}", mask);
|
||||
}
|
||||
|
||||
let repeat_length = (device.mi.regs[device::mi::MI_INIT_MODE_REG as usize]
|
||||
& device::mi::MI_INIT_LENGTH_MASK)
|
||||
+ 1;
|
||||
let repeat_length =
|
||||
(device.mi.regs[device::mi::MI_INIT_MODE_REG] & device::mi::MI_INIT_LENGTH_MASK) + 1;
|
||||
|
||||
if !repeat_length.is_multiple_of(4) {
|
||||
panic!(
|
||||
@@ -113,7 +112,7 @@ pub fn write_mem_repeat(device: &mut device::Device, address: u64, value: u32, m
|
||||
.copy_from_slice(&value.to_ne_bytes());
|
||||
}
|
||||
|
||||
device.mi.regs[device::mi::MI_INIT_MODE_REG as usize] &= !device::mi::MI_INIT_MODE;
|
||||
device.mi.regs[device::mi::MI_INIT_MODE_REG] &= !device::mi::MI_INIT_MODE;
|
||||
for i in 0..(0x3F00000 >> 16) {
|
||||
device.memory.memory_map_write[i] = device::rdram::write_mem;
|
||||
}
|
||||
@@ -127,7 +126,7 @@ pub fn read_regs(
|
||||
device::cop0::add_cycles(device, 20);
|
||||
let chip_id = (address >> 13) & 3;
|
||||
let reg = (address & 0x3FF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
RDRAM_MODE_REG => device.pi.regs[reg as usize] ^ 0xc0c0c0c0,
|
||||
0x80 => 0x00000000, //Row, needed for libdragon
|
||||
_ => device.rdram.regs[chip_id as usize][reg as usize],
|
||||
@@ -152,7 +151,7 @@ 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);
|
||||
retroachievements::set_rdram(device.rdram.mem.as_ptr(), device.rdram.size as usize);
|
||||
|
||||
// hack, skip RDRAM initialization
|
||||
device
|
||||
@@ -169,8 +168,8 @@ pub fn init(device: &mut device::Device) {
|
||||
.unwrap_or(&mut [0; 4])
|
||||
.copy_from_slice(&device.rdram.size.to_ne_bytes());
|
||||
|
||||
device.ri.regs[device::ri::RI_MODE_REG as usize] = 0x0e;
|
||||
device.ri.regs[device::ri::RI_CONFIG_REG as usize] = 0x40;
|
||||
device.ri.regs[device::ri::RI_MODE_REG] = 0x0e;
|
||||
device.ri.regs[device::ri::RI_CONFIG_REG] = 0x40;
|
||||
}
|
||||
|
||||
pub fn rdram_calculate_cycles(length: u64) -> u64 {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use crate::device;
|
||||
|
||||
pub const RI_MODE_REG: u32 = 0;
|
||||
pub const RI_CONFIG_REG: u32 = 1;
|
||||
//const RI_CURRENT_LOAD_REG: u32 = 2;
|
||||
pub const RI_SELECT_REG: u32 = 3;
|
||||
pub const RI_REFRESH_REG: u32 = 4;
|
||||
//const RI_LATENCY_REG: u32 = 5;
|
||||
//const RI_ERROR_REG: u32 = 6;
|
||||
//const RI_WERROR_REG: u32 = 7;
|
||||
pub const RI_REGS_COUNT: u32 = 8;
|
||||
pub const RI_MODE_REG: usize = 0;
|
||||
pub const RI_CONFIG_REG: usize = 1;
|
||||
//const RI_CURRENT_LOAD_REG: usize = 2;
|
||||
pub const RI_SELECT_REG: usize = 3;
|
||||
pub const RI_REFRESH_REG: usize = 4;
|
||||
//const RI_LATENCY_REG: usize = 5;
|
||||
//const RI_ERROR_REG: usize = 6;
|
||||
//const RI_WERROR_REG: usize = 7;
|
||||
pub const RI_REGS_COUNT: usize = 8;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Ri {
|
||||
pub regs: [u32; RI_REGS_COUNT as usize],
|
||||
pub regs: [u32; RI_REGS_COUNT],
|
||||
pub ram_init: bool,
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ pub fn read_regs(
|
||||
) -> u32 {
|
||||
device::cop0::add_cycles(device, 20);
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
RI_SELECT_REG => {
|
||||
if !device.ri.ram_init {
|
||||
device::cop0::add_cycles(device, device.cpu.clock_rate / 2); // hack, simulate RDRAM initialization
|
||||
@@ -38,7 +38,7 @@ pub fn read_regs(
|
||||
|
||||
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
if reg as u32 == RI_SELECT_REG {
|
||||
if reg as usize == RI_SELECT_REG {
|
||||
device.ri.ram_init = false;
|
||||
}
|
||||
device::memory::masked_write_32(&mut device.ri.regs[reg as usize], value, mask);
|
||||
|
||||
@@ -132,48 +132,47 @@ pub fn run(device: &mut device::Device) -> u64 {
|
||||
device.rsp.cpu.gpr[0] = 0; // gpr 0 is read only
|
||||
|
||||
let instruction = device.rsp.cpu.instructions
|
||||
[(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] / 4) as usize];
|
||||
[(device.rsp.regs2[device::rsp_interface::SP_PC_REG] / 4) as usize];
|
||||
(instruction.func)(device, instruction.opcode);
|
||||
|
||||
match device.rsp.cpu.branch_state.state {
|
||||
device::cpu::State::Step => {
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] += 4;
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG] += 4;
|
||||
if device.rsp.cpu.broken {
|
||||
break;
|
||||
}
|
||||
}
|
||||
device::cpu::State::Take => {
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] += 4;
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG] += 4;
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::DelaySlotTaken
|
||||
}
|
||||
device::cpu::State::NotTaken => {
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] += 4;
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG] += 4;
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::DelaySlotNotTaken
|
||||
}
|
||||
device::cpu::State::DelaySlotTaken => {
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] =
|
||||
device.rsp.cpu.branch_state.pc;
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG] = device.rsp.cpu.branch_state.pc;
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Step;
|
||||
if device.rsp.cpu.broken {
|
||||
break;
|
||||
}
|
||||
}
|
||||
device::cpu::State::DelaySlotNotTaken => {
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] += 4;
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG] += 4;
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Step;
|
||||
if device.rsp.cpu.broken {
|
||||
break;
|
||||
}
|
||||
}
|
||||
device::cpu::State::Discard => {
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] += 8;
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG] += 8;
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Step
|
||||
}
|
||||
device::cpu::State::Exception => {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Step
|
||||
}
|
||||
}
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] &= 0xFFC;
|
||||
device.rsp.regs2[device::rsp_interface::SP_PC_REG] &= 0xFFC;
|
||||
|
||||
if device.rsp.cpu.instruction_type == device.rsp.cpu.last_instruction_type {
|
||||
device.rsp.cpu.cycle_counter += 1;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
|
||||
const SP_MEM_ADDR_REG: u32 = 0;
|
||||
const SP_DRAM_ADDR_REG: u32 = 1;
|
||||
const SP_RD_LEN_REG: u32 = 2;
|
||||
const SP_WR_LEN_REG: u32 = 3;
|
||||
pub const SP_STATUS_REG: u32 = 4;
|
||||
const SP_DMA_FULL_REG: u32 = 5;
|
||||
const SP_DMA_BUSY_REG: u32 = 6;
|
||||
const SP_SEMAPHORE_REG: u32 = 7;
|
||||
pub const SP_REGS_COUNT: u32 = 8;
|
||||
const SP_MEM_ADDR_REG: usize = 0;
|
||||
const SP_DRAM_ADDR_REG: usize = 1;
|
||||
const SP_RD_LEN_REG: usize = 2;
|
||||
const SP_WR_LEN_REG: usize = 3;
|
||||
pub const SP_STATUS_REG: usize = 4;
|
||||
const SP_DMA_FULL_REG: usize = 5;
|
||||
const SP_DMA_BUSY_REG: usize = 6;
|
||||
const SP_SEMAPHORE_REG: usize = 7;
|
||||
pub const SP_REGS_COUNT: usize = 8;
|
||||
|
||||
pub const SP_PC_REG: u32 = 0;
|
||||
//const SP_IBIST_REG: u32 = 1;
|
||||
pub const SP_REGS2_COUNT: u32 = 2;
|
||||
pub const SP_PC_REG: usize = 0;
|
||||
//const SP_IBIST_REG: usize = 1;
|
||||
pub const SP_REGS2_COUNT: usize = 2;
|
||||
|
||||
/* SP_STATUS - read */
|
||||
pub const SP_STATUS_HALT: u32 = 1 << 0;
|
||||
@@ -79,8 +79,8 @@ pub struct RspDma {
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Rsp {
|
||||
pub cpu: device::rsp_cpu::Cpu,
|
||||
pub regs: [u32; SP_REGS_COUNT as usize],
|
||||
pub regs2: [u32; SP_REGS2_COUNT as usize],
|
||||
pub regs: [u32; SP_REGS_COUNT],
|
||||
pub regs2: [u32; SP_REGS2_COUNT],
|
||||
#[serde(with = "serde_big_array::BigArray")]
|
||||
pub mem: [u8; 0x2000],
|
||||
pub fifo: [RspDma; 2],
|
||||
@@ -204,10 +204,10 @@ fn do_dma(device: &mut device::Device, dma: RspDma) {
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
device.rsp.regs[SP_MEM_ADDR_REG as usize] = (mem_addr & 0xfff) + (dma.memaddr & 0x1000);
|
||||
device.rsp.regs[SP_DRAM_ADDR_REG as usize] = dram_addr;
|
||||
device.rsp.regs[SP_RD_LEN_REG as usize] = 0xff8;
|
||||
device.rsp.regs[SP_WR_LEN_REG as usize] = 0xff8;
|
||||
device.rsp.regs[SP_MEM_ADDR_REG] = (mem_addr & 0xfff) + (dma.memaddr & 0x1000);
|
||||
device.rsp.regs[SP_DRAM_ADDR_REG] = dram_addr;
|
||||
device.rsp.regs[SP_RD_LEN_REG] = 0xff8;
|
||||
device.rsp.regs[SP_WR_LEN_REG] = 0xff8;
|
||||
|
||||
device::events::create_event(
|
||||
device,
|
||||
@@ -217,52 +217,52 @@ fn do_dma(device: &mut device::Device, dma: RspDma) {
|
||||
}
|
||||
|
||||
fn fifo_push(device: &mut device::Device, dir: DmaDir) {
|
||||
if device.rsp.regs[SP_DMA_FULL_REG as usize] != 0 {
|
||||
if device.rsp.regs[SP_DMA_FULL_REG] != 0 {
|
||||
panic!("RSP DMA already full")
|
||||
}
|
||||
|
||||
device.rsp.cpu.sync_point = true;
|
||||
|
||||
if device.rsp.regs[SP_DMA_BUSY_REG as usize] != 0 {
|
||||
if device.rsp.regs[SP_DMA_BUSY_REG] != 0 {
|
||||
device.rsp.fifo[1].dir = dir;
|
||||
if dir == DmaDir::Read {
|
||||
device.rsp.fifo[1].length = device.rsp.regs[SP_WR_LEN_REG as usize]
|
||||
device.rsp.fifo[1].length = device.rsp.regs[SP_WR_LEN_REG]
|
||||
} else {
|
||||
device.rsp.fifo[1].length = device.rsp.regs[SP_RD_LEN_REG as usize]
|
||||
device.rsp.fifo[1].length = device.rsp.regs[SP_RD_LEN_REG]
|
||||
}
|
||||
device.rsp.fifo[1].memaddr = device.rsp.regs[SP_MEM_ADDR_REG as usize];
|
||||
device.rsp.fifo[1].dramaddr = device.rsp.regs[SP_DRAM_ADDR_REG as usize];
|
||||
device.rsp.regs[SP_DMA_FULL_REG as usize] = 1;
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_DMA_FULL
|
||||
device.rsp.fifo[1].memaddr = device.rsp.regs[SP_MEM_ADDR_REG];
|
||||
device.rsp.fifo[1].dramaddr = device.rsp.regs[SP_DRAM_ADDR_REG];
|
||||
device.rsp.regs[SP_DMA_FULL_REG] = 1;
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_DMA_FULL
|
||||
} else {
|
||||
device.rsp.fifo[0].dir = dir;
|
||||
if dir == DmaDir::Read {
|
||||
device.rsp.fifo[0].length = device.rsp.regs[SP_WR_LEN_REG as usize]
|
||||
device.rsp.fifo[0].length = device.rsp.regs[SP_WR_LEN_REG]
|
||||
} else {
|
||||
device.rsp.fifo[0].length = device.rsp.regs[SP_RD_LEN_REG as usize]
|
||||
device.rsp.fifo[0].length = device.rsp.regs[SP_RD_LEN_REG]
|
||||
}
|
||||
device.rsp.fifo[0].memaddr = device.rsp.regs[SP_MEM_ADDR_REG as usize];
|
||||
device.rsp.fifo[0].dramaddr = device.rsp.regs[SP_DRAM_ADDR_REG as usize];
|
||||
device.rsp.regs[SP_DMA_BUSY_REG as usize] = 1;
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_DMA_BUSY;
|
||||
device.rsp.fifo[0].memaddr = device.rsp.regs[SP_MEM_ADDR_REG];
|
||||
device.rsp.fifo[0].dramaddr = device.rsp.regs[SP_DRAM_ADDR_REG];
|
||||
device.rsp.regs[SP_DMA_BUSY_REG] = 1;
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_DMA_BUSY;
|
||||
|
||||
do_dma(device, device.rsp.fifo[0])
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fifo_pop(device: &mut device::Device) {
|
||||
if device.rsp.regs[SP_DMA_FULL_REG as usize] != 0 {
|
||||
if device.rsp.regs[SP_DMA_FULL_REG] != 0 {
|
||||
device.rsp.fifo[0].dir = device.rsp.fifo[1].dir;
|
||||
device.rsp.fifo[0].length = device.rsp.fifo[1].length;
|
||||
device.rsp.fifo[0].memaddr = device.rsp.fifo[1].memaddr;
|
||||
device.rsp.fifo[0].dramaddr = device.rsp.fifo[1].dramaddr;
|
||||
device.rsp.regs[SP_DMA_FULL_REG as usize] = 0;
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_DMA_FULL;
|
||||
device.rsp.regs[SP_DMA_FULL_REG] = 0;
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_DMA_FULL;
|
||||
|
||||
do_dma(device, device.rsp.fifo[0])
|
||||
} else {
|
||||
device.rsp.regs[SP_DMA_BUSY_REG as usize] = 0;
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_DMA_BUSY;
|
||||
device.rsp.regs[SP_DMA_BUSY_REG] = 0;
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_DMA_BUSY;
|
||||
if device.rsp.run_after_dma {
|
||||
device.rsp.run_after_dma = false;
|
||||
do_task(device);
|
||||
@@ -279,7 +279,7 @@ pub fn read_regs(
|
||||
device::cop0::add_cycles(device, 20);
|
||||
}
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
SP_STATUS_REG => {
|
||||
let value = device.rsp.regs[reg as usize]
|
||||
& (SP_STATUS_SIG0
|
||||
@@ -311,7 +311,7 @@ pub fn read_regs(
|
||||
|
||||
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
SP_STATUS_REG => update_sp_status(device, value),
|
||||
SP_RD_LEN_REG => {
|
||||
device::memory::masked_write_32(&mut device.rsp.regs[reg as usize], value, mask);
|
||||
@@ -339,7 +339,7 @@ pub fn read_regs2(
|
||||
|
||||
pub fn write_regs2(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
SP_PC_REG => {
|
||||
device::memory::masked_write_32(
|
||||
&mut device.rsp.regs2[reg as usize],
|
||||
@@ -352,20 +352,20 @@ pub fn write_regs2(device: &mut device::Device, address: u64, value: u32, mask:
|
||||
}
|
||||
|
||||
fn update_sp_status(device: &mut device::Device, w: u32) {
|
||||
let was_halted = device.rsp.regs[SP_STATUS_REG as usize] & SP_STATUS_HALT != 0;
|
||||
let was_halted = device.rsp.regs[SP_STATUS_REG] & SP_STATUS_HALT != 0;
|
||||
|
||||
/* clear / set halt */
|
||||
if w & SP_CLR_HALT != 0 && w & SP_SET_HALT == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_HALT
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_HALT
|
||||
}
|
||||
if w & SP_SET_HALT != 0 && w & SP_CLR_HALT == 0 {
|
||||
device::events::remove_event(device, device::events::EVENT_TYPE_SP);
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_HALT
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_HALT
|
||||
}
|
||||
|
||||
/* clear broke */
|
||||
if w & SP_CLR_BROKE != 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_BROKE
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_BROKE
|
||||
}
|
||||
|
||||
/* clear SP interrupt */
|
||||
@@ -379,85 +379,85 @@ fn update_sp_status(device: &mut device::Device, w: u32) {
|
||||
|
||||
/* clear / set single step */
|
||||
if w & SP_CLR_SSTEP != 0 && w & SP_SET_SSTEP == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SSTEP
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SSTEP
|
||||
}
|
||||
if w & SP_SET_SSTEP != 0 && w & SP_CLR_SSTEP == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SSTEP
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SSTEP
|
||||
}
|
||||
|
||||
/* clear / set interrupt on break */
|
||||
if w & SP_CLR_INTR_BREAK != 0 && w & SP_SET_INTR_BREAK == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_INTR_BREAK
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_INTR_BREAK
|
||||
}
|
||||
if w & SP_SET_INTR_BREAK != 0 && w & SP_CLR_INTR_BREAK == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_INTR_BREAK
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_INTR_BREAK
|
||||
}
|
||||
|
||||
/* clear / set signal 0 */
|
||||
if w & SP_CLR_SIG0 != 0 && w & SP_SET_SIG0 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG0
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG0
|
||||
}
|
||||
if w & SP_SET_SIG0 != 0 && w & SP_CLR_SIG0 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG0
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG0
|
||||
}
|
||||
|
||||
/* clear / set signal 1 */
|
||||
if w & SP_CLR_SIG1 != 0 && w & SP_SET_SIG1 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG1
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG1
|
||||
}
|
||||
if w & SP_SET_SIG1 != 0 && w & SP_CLR_SIG1 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG1
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG1
|
||||
}
|
||||
|
||||
/* clear / set signal 2 */
|
||||
if w & SP_CLR_SIG2 != 0 && w & SP_SET_SIG2 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG2
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG2
|
||||
}
|
||||
if w & SP_SET_SIG2 != 0 && w & SP_CLR_SIG2 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG2
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG2
|
||||
}
|
||||
|
||||
/* clear / set signal 3 */
|
||||
if w & SP_CLR_SIG3 != 0 && w & SP_SET_SIG3 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG3
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG3
|
||||
}
|
||||
if w & SP_SET_SIG3 != 0 && w & SP_CLR_SIG3 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG3
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG3
|
||||
}
|
||||
|
||||
/* clear / set signal 4 */
|
||||
if w & SP_CLR_SIG4 != 0 && w & SP_SET_SIG4 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG4
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG4
|
||||
}
|
||||
if w & SP_SET_SIG4 != 0 && w & SP_CLR_SIG4 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG4
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG4
|
||||
}
|
||||
|
||||
/* clear / set signal 5 */
|
||||
if w & SP_CLR_SIG5 != 0 && w & SP_SET_SIG5 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG5
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG5
|
||||
}
|
||||
if w & SP_SET_SIG5 != 0 && w & SP_CLR_SIG5 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG5
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG5
|
||||
}
|
||||
|
||||
/* clear / set signal 6 */
|
||||
if w & SP_CLR_SIG6 != 0 && w & SP_SET_SIG6 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG6
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG6
|
||||
}
|
||||
if w & SP_SET_SIG6 != 0 && w & SP_CLR_SIG6 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG6
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG6
|
||||
}
|
||||
|
||||
/* clear / set signal 7 */
|
||||
if w & SP_CLR_SIG7 != 0 && w & SP_SET_SIG7 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG7
|
||||
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG7
|
||||
}
|
||||
if w & SP_SET_SIG7 != 0 && w & SP_CLR_SIG7 == 0 {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG7
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG7
|
||||
}
|
||||
|
||||
if device.rsp.regs[SP_STATUS_REG as usize] & SP_STATUS_HALT == 0 && was_halted {
|
||||
if device.rsp.regs[SP_STATUS_REG] & SP_STATUS_HALT == 0 && was_halted {
|
||||
device.rsp.cpu.broken = false;
|
||||
device.rsp.cpu.halted = false;
|
||||
do_task(device);
|
||||
@@ -468,7 +468,7 @@ fn do_task(device: &mut device::Device) {
|
||||
device.rsp.cpu.sync_point = false;
|
||||
device.rsp.last_status_value = 0;
|
||||
device.rdp.last_status_value = 0;
|
||||
if device.rsp.regs[SP_DMA_BUSY_REG as usize] == 1 {
|
||||
if device.rsp.regs[SP_DMA_BUSY_REG] == 1 {
|
||||
device.rsp.run_after_dma = true
|
||||
} else {
|
||||
let timer = device::rsp_cpu::run(device);
|
||||
@@ -479,21 +479,21 @@ fn do_task(device: &mut device::Device) {
|
||||
|
||||
pub fn rsp_event(device: &mut device::Device) {
|
||||
if device.rsp.cpu.broken {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_HALT | SP_STATUS_BROKE;
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_HALT | SP_STATUS_BROKE;
|
||||
|
||||
if device.rsp.regs[SP_STATUS_REG as usize] & SP_STATUS_INTR_BREAK != 0 {
|
||||
if device.rsp.regs[SP_STATUS_REG] & SP_STATUS_INTR_BREAK != 0 {
|
||||
device::mi::set_rcp_interrupt(device, device::mi::MI_INTR_SP)
|
||||
}
|
||||
return;
|
||||
}
|
||||
if device.rsp.cpu.halted {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_HALT;
|
||||
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_HALT;
|
||||
return;
|
||||
}
|
||||
do_task(device)
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
device.rsp.regs[SP_STATUS_REG as usize] = 1;
|
||||
device.rsp.regs[SP_STATUS_REG] = 1;
|
||||
device::rsp_cpu::init(device);
|
||||
}
|
||||
|
||||
@@ -183,23 +183,22 @@ pub fn j(device: &mut device::Device, opcode: u32) {
|
||||
return;
|
||||
}
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc =
|
||||
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 4) & 0xF0000000
|
||||
| ((opcode & 0x3FFFFFF) << 2)
|
||||
device.rsp.cpu.branch_state.pc = (device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 4)
|
||||
& 0xF0000000
|
||||
| ((opcode & 0x3FFFFFF) << 2)
|
||||
}
|
||||
|
||||
pub fn jal(device: &mut device::Device, opcode: u32) {
|
||||
if device::rsp_cpu::in_delay_slot_taken(device) {
|
||||
device.rsp.cpu.gpr[31] = (device.rsp.cpu.branch_state.pc + 4) & 0xFFF
|
||||
} else {
|
||||
device.rsp.cpu.gpr[31] =
|
||||
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 8) & 0xFFF
|
||||
device.rsp.cpu.gpr[31] = (device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 8) & 0xFFF
|
||||
}
|
||||
if !device::rsp_cpu::in_delay_slot_taken(device) {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc =
|
||||
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 4) & 0xF0000000
|
||||
| ((opcode & 0x3FFFFFF) << 2)
|
||||
device.rsp.cpu.branch_state.pc = (device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 4)
|
||||
& 0xF0000000
|
||||
| ((opcode & 0x3FFFFFF) << 2)
|
||||
} else if !device::rsp_cpu::in_delay_slot(device) {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::NotTaken;
|
||||
}
|
||||
@@ -208,8 +207,7 @@ pub fn jal(device: &mut device::Device, opcode: u32) {
|
||||
pub fn beq(device: &mut device::Device, opcode: u32) {
|
||||
if device.rsp.cpu.gpr[rs(opcode) as usize] == device.rsp.cpu.gpr[rt(opcode) as usize] {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2
|
||||
[device::rsp_interface::SP_PC_REG as usize]
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
|
||||
.wrapping_add(se16(imm(opcode) as i16) << 2)
|
||||
+ 4;
|
||||
} else {
|
||||
@@ -220,8 +218,7 @@ pub fn beq(device: &mut device::Device, opcode: u32) {
|
||||
pub fn bne(device: &mut device::Device, opcode: u32) {
|
||||
if device.rsp.cpu.gpr[rs(opcode) as usize] != device.rsp.cpu.gpr[rt(opcode) as usize] {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2
|
||||
[device::rsp_interface::SP_PC_REG as usize]
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
|
||||
.wrapping_add(se16(imm(opcode) as i16) << 2)
|
||||
+ 4;
|
||||
} else {
|
||||
@@ -232,8 +229,7 @@ pub fn bne(device: &mut device::Device, opcode: u32) {
|
||||
pub fn blez(device: &mut device::Device, opcode: u32) {
|
||||
if device.rsp.cpu.gpr[rs(opcode) as usize] as i32 <= 0 {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2
|
||||
[device::rsp_interface::SP_PC_REG as usize]
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
|
||||
.wrapping_add(se16(imm(opcode) as i16) << 2)
|
||||
+ 4;
|
||||
} else {
|
||||
@@ -244,8 +240,7 @@ pub fn blez(device: &mut device::Device, opcode: u32) {
|
||||
pub fn bgtz(device: &mut device::Device, opcode: u32) {
|
||||
if device.rsp.cpu.gpr[rs(opcode) as usize] as i32 > 0 {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2
|
||||
[device::rsp_interface::SP_PC_REG as usize]
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
|
||||
.wrapping_add(se16(imm(opcode) as i16) << 2)
|
||||
+ 4;
|
||||
} else {
|
||||
@@ -485,7 +480,7 @@ pub fn jalr(device: &mut device::Device, opcode: u32) {
|
||||
device.rsp.cpu.gpr[rd(opcode) as usize] = (device.rsp.cpu.branch_state.pc + 4) & 0xFFF
|
||||
} else {
|
||||
device.rsp.cpu.gpr[rd(opcode) as usize] =
|
||||
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 8) & 0xFFF
|
||||
(device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 8) & 0xFFF
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,8 +542,7 @@ pub fn sltu(device: &mut device::Device, opcode: u32) {
|
||||
pub fn bltz(device: &mut device::Device, opcode: u32) {
|
||||
if (device.rsp.cpu.gpr[rs(opcode) as usize] as i32) < 0 {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2
|
||||
[device::rsp_interface::SP_PC_REG as usize]
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
|
||||
.wrapping_add(se16(imm(opcode) as i16) << 2)
|
||||
+ 4;
|
||||
} else {
|
||||
@@ -559,8 +553,7 @@ pub fn bltz(device: &mut device::Device, opcode: u32) {
|
||||
pub fn bgez(device: &mut device::Device, opcode: u32) {
|
||||
if device.rsp.cpu.gpr[rs(opcode) as usize] as i32 >= 0 {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2
|
||||
[device::rsp_interface::SP_PC_REG as usize]
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
|
||||
.wrapping_add(se16(imm(opcode) as i16) << 2)
|
||||
+ 4;
|
||||
} else {
|
||||
@@ -571,34 +564,30 @@ pub fn bgez(device: &mut device::Device, opcode: u32) {
|
||||
pub fn bltzal(device: &mut device::Device, opcode: u32) {
|
||||
if (device.rsp.cpu.gpr[rs(opcode) as usize] as i32) < 0 {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2
|
||||
[device::rsp_interface::SP_PC_REG as usize]
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
|
||||
.wrapping_add(se16(imm(opcode) as i16) << 2)
|
||||
+ 4;
|
||||
} else {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::NotTaken;
|
||||
}
|
||||
device.rsp.cpu.gpr[31] =
|
||||
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 8) & 0xFFF
|
||||
device.rsp.cpu.gpr[31] = (device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 8) & 0xFFF
|
||||
}
|
||||
|
||||
pub fn bgezal(device: &mut device::Device, opcode: u32) {
|
||||
if (device.rsp.cpu.gpr[rs(opcode) as usize] as i32) >= 0 {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2
|
||||
[device::rsp_interface::SP_PC_REG as usize]
|
||||
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
|
||||
.wrapping_add(se16(imm(opcode) as i16) << 2)
|
||||
+ 4;
|
||||
} else {
|
||||
device.rsp.cpu.branch_state.state = device::cpu::State::NotTaken;
|
||||
}
|
||||
device.rsp.cpu.gpr[31] =
|
||||
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 8) & 0xFFF
|
||||
device.rsp.cpu.gpr[31] = (device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 8) & 0xFFF
|
||||
}
|
||||
|
||||
pub fn mfc0(device: &mut device::Device, opcode: u32) {
|
||||
device.rsp.cpu.cycle_counter += 2;
|
||||
if rd(opcode) < device::rsp_interface::SP_REGS_COUNT {
|
||||
if rd(opcode) < device::rsp_interface::SP_REGS_COUNT as u32 {
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] = device::rsp_interface::read_regs(
|
||||
device,
|
||||
(rd(opcode) << 2) as u64,
|
||||
@@ -607,14 +596,14 @@ pub fn mfc0(device: &mut device::Device, opcode: u32) {
|
||||
} else {
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] = device::rdp::read_regs_dpc(
|
||||
device,
|
||||
((rd(opcode) - device::rsp_interface::SP_REGS_COUNT) << 2) as u64,
|
||||
((rd(opcode) - device::rsp_interface::SP_REGS_COUNT as u32) << 2) as u64,
|
||||
device::memory::AccessSize::Word,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mtc0(device: &mut device::Device, opcode: u32) {
|
||||
if rd(opcode) < device::rsp_interface::SP_REGS_COUNT {
|
||||
if rd(opcode) < device::rsp_interface::SP_REGS_COUNT as u32 {
|
||||
device::rsp_interface::write_regs(
|
||||
device,
|
||||
(rd(opcode) << 2) as u64,
|
||||
@@ -624,15 +613,15 @@ pub fn mtc0(device: &mut device::Device, opcode: u32) {
|
||||
} else {
|
||||
device::rdp::write_regs_dpc(
|
||||
device,
|
||||
((rd(opcode) - device::rsp_interface::SP_REGS_COUNT) << 2) as u64,
|
||||
((rd(opcode) - device::rsp_interface::SP_REGS_COUNT as u32) << 2) as u64,
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize],
|
||||
0xFFFFFFFF,
|
||||
)
|
||||
}
|
||||
if rd(opcode) == device::rsp_interface::SP_STATUS_REG
|
||||
if rd(opcode) as usize == device::rsp_interface::SP_STATUS_REG
|
||||
&& device.rsp.cpu.gpr[rt(opcode) as usize] & device::rsp_interface::SP_SET_HALT != 0
|
||||
{
|
||||
device.rsp.regs[device::rsp_interface::SP_STATUS_REG as usize] &=
|
||||
device.rsp.regs[device::rsp_interface::SP_STATUS_REG] &=
|
||||
!device::rsp_interface::SP_STATUS_HALT; // set halt when event happens
|
||||
device.rsp.cpu.halted = true // the RSP can halt itself by setting SP_SET_HALT
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ use crate::device;
|
||||
use crate::ui;
|
||||
use rand::Rng;
|
||||
|
||||
const SI_DRAM_ADDR_REG: u32 = 0;
|
||||
const SI_PIF_ADDR_RD64B_REG: u32 = 1;
|
||||
//const SI_R2_REG: u32 = 2;
|
||||
//const SI_R3_REG: u32 = 3;
|
||||
const SI_PIF_ADDR_WR64B_REG: u32 = 4;
|
||||
//const SI_R5_REG: u32 = 5;
|
||||
pub const SI_STATUS_REG: u32 = 6;
|
||||
pub const SI_REGS_COUNT: u32 = 7;
|
||||
const SI_DRAM_ADDR_REG: usize = 0;
|
||||
const SI_PIF_ADDR_RD64B_REG: usize = 1;
|
||||
//const SI_R2_REG: usize = 2;
|
||||
//const SI_R3_REG: usize = 3;
|
||||
const SI_PIF_ADDR_WR64B_REG: usize = 4;
|
||||
//const SI_R5_REG: usize = 5;
|
||||
pub const SI_STATUS_REG: usize = 6;
|
||||
pub const SI_REGS_COUNT: usize = 7;
|
||||
|
||||
pub const SI_STATUS_DMA_BUSY: u32 = 1 << 0;
|
||||
pub const SI_STATUS_IO_BUSY: u32 = 1 << 1;
|
||||
@@ -25,7 +25,7 @@ pub enum DmaDir {
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Si {
|
||||
pub regs: [u32; SI_REGS_COUNT as usize],
|
||||
pub regs: [u32; SI_REGS_COUNT],
|
||||
pub dma_dir: DmaDir,
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ fn dma_read(device: &mut device::Device) {
|
||||
|
||||
let duration = device::pif::update_pif_ram(device);
|
||||
|
||||
device.si.regs[SI_STATUS_REG as usize] |= SI_STATUS_DMA_BUSY;
|
||||
device.si.regs[SI_STATUS_REG] |= SI_STATUS_DMA_BUSY;
|
||||
|
||||
let length = duration + randomize_interrupt_time(&mut device.rng);
|
||||
|
||||
@@ -59,7 +59,7 @@ fn dma_write(device: &mut device::Device) {
|
||||
|
||||
copy_pif_rdram(device);
|
||||
|
||||
device.si.regs[SI_STATUS_REG as usize] |= SI_STATUS_DMA_BUSY;
|
||||
device.si.regs[SI_STATUS_REG] |= SI_STATUS_DMA_BUSY;
|
||||
|
||||
let length = 6000 + randomize_interrupt_time(&mut device.rng); //based on https://github.com/rasky/n64-systembench
|
||||
|
||||
@@ -68,7 +68,7 @@ fn dma_write(device: &mut device::Device) {
|
||||
|
||||
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
SI_STATUS_REG => {
|
||||
device.si.regs[reg as usize] &= !SI_STATUS_INTERRUPT;
|
||||
device::mi::clear_rcp_interrupt(device, device::mi::MI_INTR_SI)
|
||||
@@ -81,7 +81,7 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
|
||||
//rdram is in native endian format, and pif memory is in big endian format
|
||||
fn copy_pif_rdram(device: &mut device::Device) {
|
||||
let dram_addr = device.si.regs[SI_DRAM_ADDR_REG as usize] as usize & device::rdram::RDRAM_MASK;
|
||||
let dram_addr = device.si.regs[SI_DRAM_ADDR_REG] as usize & device::rdram::RDRAM_MASK;
|
||||
if device.si.dma_dir == DmaDir::Write {
|
||||
let mut i = 0;
|
||||
while i < device::pif::PIF_RAM_SIZE {
|
||||
@@ -125,8 +125,8 @@ pub fn dma_event(device: &mut device::Device) {
|
||||
panic!("si dma unknown")
|
||||
}
|
||||
device.si.dma_dir = DmaDir::None;
|
||||
device.si.regs[SI_STATUS_REG as usize] &= !(SI_STATUS_DMA_BUSY | SI_STATUS_IO_BUSY);
|
||||
device.si.regs[SI_STATUS_REG as usize] |= SI_STATUS_INTERRUPT;
|
||||
device.si.regs[SI_STATUS_REG] &= !(SI_STATUS_DMA_BUSY | SI_STATUS_IO_BUSY);
|
||||
device.si.regs[SI_STATUS_REG] |= SI_STATUS_INTERRUPT;
|
||||
|
||||
device::mi::set_rcp_interrupt(device, device::mi::MI_INTR_SI)
|
||||
}
|
||||
|
||||
@@ -34,22 +34,22 @@ pub fn read(device: &mut device::Device, index: u64) {
|
||||
if index > 31 {
|
||||
return;
|
||||
}
|
||||
device.cpu.cop0.regs[device::cop0::COP0_PAGEMASK_REG as usize] =
|
||||
device.cpu.cop0.regs[device::cop0::COP0_PAGEMASK_REG] =
|
||||
device.cpu.cop0.tlb_entries[index as usize].mask << 13;
|
||||
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] =
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] =
|
||||
((device.cpu.cop0.tlb_entries[index as usize].region as u64) << 62)
|
||||
| (device.cpu.cop0.tlb_entries[index as usize].vpn2 << 13)
|
||||
| (device.cpu.cop0.tlb_entries[index as usize].asid) as u64;
|
||||
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG as usize] =
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG] =
|
||||
(device.cpu.cop0.tlb_entries[index as usize].pfn_even << 6)
|
||||
| (device.cpu.cop0.tlb_entries[index as usize].c_even << 3) as u64
|
||||
| (device.cpu.cop0.tlb_entries[index as usize].d_even << 2) as u64
|
||||
| (device.cpu.cop0.tlb_entries[index as usize].v_even << 1) as u64
|
||||
| (device.cpu.cop0.tlb_entries[index as usize].g) as u64;
|
||||
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize] =
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG] =
|
||||
(device.cpu.cop0.tlb_entries[index as usize].pfn_odd << 6)
|
||||
| (device.cpu.cop0.tlb_entries[index as usize].c_odd << 3) as u64
|
||||
| (device.cpu.cop0.tlb_entries[index as usize].d_odd << 2) as u64
|
||||
@@ -64,37 +64,37 @@ pub fn write(device: &mut device::Device, index: u64) {
|
||||
tlb_unmap(device, index);
|
||||
|
||||
device.cpu.cop0.tlb_entries[index as usize].g = (device.cpu.cop0.regs
|
||||
[device::cop0::COP0_ENTRYLO0_REG as usize]
|
||||
& device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize]
|
||||
[device::cop0::COP0_ENTRYLO0_REG]
|
||||
& device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG]
|
||||
& 1) as u8;
|
||||
|
||||
device.cpu.cop0.tlb_entries[index as usize].pfn_even =
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG as usize] >> 6) & 0xFFFFF;
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG] >> 6) & 0xFFFFF;
|
||||
device.cpu.cop0.tlb_entries[index as usize].pfn_odd =
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize] >> 6) & 0xFFFFF;
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG] >> 6) & 0xFFFFF;
|
||||
device.cpu.cop0.tlb_entries[index as usize].c_even =
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG as usize] >> 3) & 7) as u8;
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG] >> 3) & 7) as u8;
|
||||
device.cpu.cop0.tlb_entries[index as usize].c_odd =
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize] >> 3) & 7) as u8;
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG] >> 3) & 7) as u8;
|
||||
device.cpu.cop0.tlb_entries[index as usize].d_even =
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG as usize] >> 2) & 1) as u8;
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG] >> 2) & 1) as u8;
|
||||
device.cpu.cop0.tlb_entries[index as usize].d_odd =
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize] >> 2) & 1) as u8;
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG] >> 2) & 1) as u8;
|
||||
device.cpu.cop0.tlb_entries[index as usize].v_even =
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG as usize] >> 1) & 1) as u8;
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG] >> 1) & 1) as u8;
|
||||
device.cpu.cop0.tlb_entries[index as usize].v_odd =
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize] >> 1) & 1) as u8;
|
||||
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG] >> 1) & 1) as u8;
|
||||
device.cpu.cop0.tlb_entries[index as usize].asid =
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] as u8;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] as u8;
|
||||
|
||||
device.cpu.cop0.tlb_entries[index as usize].mask =
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_PAGEMASK_REG as usize] >> 13) & 0xFFF;
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_PAGEMASK_REG] >> 13) & 0xFFF;
|
||||
|
||||
device.cpu.cop0.tlb_entries[index as usize].vpn2 =
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] >> 13) & 0x7FFFFFF;
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] >> 13) & 0x7FFFFFF;
|
||||
|
||||
device.cpu.cop0.tlb_entries[index as usize].region =
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] >> 62) as u8;
|
||||
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] >> 62) as u8;
|
||||
|
||||
device.cpu.cop0.tlb_entries[index as usize].mask &= 0b101010101010;
|
||||
device.cpu.cop0.tlb_entries[index as usize].mask |=
|
||||
@@ -125,22 +125,20 @@ pub fn write(device: &mut device::Device, index: u64) {
|
||||
}
|
||||
|
||||
pub fn probe(device: &mut device::Device) {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_INDEX_REG as usize] = 0x80000000; // set probe failure
|
||||
device.cpu.cop0.regs[device::cop0::COP0_INDEX_REG] = 0x80000000; // set probe failure
|
||||
for (pos, e) in device.cpu.cop0.tlb_entries.iter().enumerate() {
|
||||
if e.vpn2 & !e.mask
|
||||
!= ((device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] >> 13) & 0x7FFFFFF)
|
||||
& !e.mask
|
||||
!= ((device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] >> 13) & 0x7FFFFFF) & !e.mask
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if e.region != (device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] >> 62) as u8 {
|
||||
if e.region != (device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] >> 62) as u8 {
|
||||
continue;
|
||||
}
|
||||
if e.g == 0 && e.asid != device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] as u8
|
||||
{
|
||||
if e.g == 0 && e.asid != device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] as u8 {
|
||||
continue;
|
||||
}
|
||||
device.cpu.cop0.regs[device::cop0::COP0_INDEX_REG as usize] = pos as u64;
|
||||
device.cpu.cop0.regs[device::cop0::COP0_INDEX_REG] = pos as u64;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
use crate::{cheats, device, netplay, retroachievements, ui};
|
||||
|
||||
const VI_STATUS_REG: u32 = 0;
|
||||
const VI_ORIGIN_REG: u32 = 1;
|
||||
//const VI_WIDTH_REG: u32 = 2;
|
||||
//const VI_V_INTR_REG: u32 = 3;
|
||||
const VI_CURRENT_REG: u32 = 4;
|
||||
//const VI_BURST_REG: u32 = 5;
|
||||
const VI_V_SYNC_REG: u32 = 6;
|
||||
const VI_H_SYNC_REG: u32 = 7;
|
||||
//const VI_LEAP_REG: u32 = 8;
|
||||
//const VI_H_START_REG: u32 = 9;
|
||||
//const VI_V_START_REG: u32 = 10;
|
||||
//const VI_V_BURST_REG: u32 = 11;
|
||||
//const VI_X_SCALE_REG: u32 = 12;
|
||||
//const VI_Y_SCALE_REG: u32 = 13;
|
||||
pub const VI_REGS_COUNT: u32 = 14;
|
||||
const VI_STATUS_REG: usize = 0;
|
||||
const VI_ORIGIN_REG: usize = 1;
|
||||
//const VI_WIDTH_REG: usize = 2;
|
||||
//const VI_V_INTR_REG: usize = 3;
|
||||
const VI_CURRENT_REG: usize = 4;
|
||||
//const VI_BURST_REG: usize = 5;
|
||||
const VI_V_SYNC_REG: usize = 6;
|
||||
const VI_H_SYNC_REG: usize = 7;
|
||||
//const VI_LEAP_REG: usize = 8;
|
||||
//const VI_H_START_REG: usize = 9;
|
||||
//const VI_V_START_REG: usize = 10;
|
||||
//const VI_V_BURST_REG: usize = 11;
|
||||
//const VI_X_SCALE_REG: usize = 12;
|
||||
//const VI_Y_SCALE_REG: usize = 13;
|
||||
pub const VI_REGS_COUNT: usize = 14;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Vi {
|
||||
pub regs: [u32; VI_REGS_COUNT as usize],
|
||||
pub regs: [u32; VI_REGS_COUNT],
|
||||
pub clock: u64,
|
||||
pub delay: u64,
|
||||
pub field: u32,
|
||||
@@ -29,6 +29,7 @@ pub struct Vi {
|
||||
pub vi_counter: u64,
|
||||
pub min_wait_time: std::time::Duration,
|
||||
pub frame_time: f64,
|
||||
pub elapsed_time: f64,
|
||||
pub limit_freq: u64,
|
||||
#[serde(skip)]
|
||||
#[serde(default = "std::time::Instant::now")]
|
||||
@@ -39,13 +40,12 @@ const MAX_LIMIT_FREQ: u64 = 3;
|
||||
|
||||
pub fn set_expected_refresh_rate(device: &mut device::Device) {
|
||||
let expected_refresh_rate = device.vi.clock as f64
|
||||
/ (device.vi.regs[VI_V_SYNC_REG as usize] + 1) as f64
|
||||
/ ((device.vi.regs[VI_H_SYNC_REG as usize] & 0xFFF) + 1) as f64
|
||||
/ (device.vi.regs[VI_V_SYNC_REG] + 1) as f64
|
||||
/ ((device.vi.regs[VI_H_SYNC_REG] & 0xFFF) + 1) as f64
|
||||
* 2.0;
|
||||
device.vi.frame_time = 1.0 / expected_refresh_rate;
|
||||
device.vi.delay = (device.cpu.clock_rate as f64 / expected_refresh_rate) as u64;
|
||||
device.vi.count_per_scanline =
|
||||
device.vi.delay / (device.vi.regs[VI_V_SYNC_REG as usize] + 1) as u64;
|
||||
device.vi.count_per_scanline = device.vi.delay / (device.vi.regs[VI_V_SYNC_REG] + 1) as u64;
|
||||
|
||||
reset_pace_deadline(device);
|
||||
}
|
||||
@@ -62,19 +62,19 @@ fn set_vertical_interrupt(device: &mut device::Device) {
|
||||
|
||||
fn set_current_line(device: &mut device::Device) {
|
||||
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;
|
||||
device.vi.regs[VI_CURRENT_REG] =
|
||||
((device.vi.delay.saturating_sub(
|
||||
next_vi.count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG],
|
||||
)) / 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] {
|
||||
device.vi.regs[VI_CURRENT_REG as usize] -= device.vi.regs[VI_V_SYNC_REG as usize]
|
||||
if device.vi.regs[VI_CURRENT_REG] >= device.vi.regs[VI_V_SYNC_REG] {
|
||||
device.vi.regs[VI_CURRENT_REG] -= device.vi.regs[VI_V_SYNC_REG]
|
||||
}
|
||||
}
|
||||
/* update current field */
|
||||
device.vi.regs[VI_CURRENT_REG as usize] =
|
||||
(device.vi.regs[VI_CURRENT_REG as usize] & !1) | device.vi.field;
|
||||
ui::video::set_register(VI_CURRENT_REG, device.vi.regs[VI_CURRENT_REG as usize])
|
||||
device.vi.regs[VI_CURRENT_REG] = (device.vi.regs[VI_CURRENT_REG] & !1) | device.vi.field;
|
||||
ui::video::set_register(VI_CURRENT_REG as u32, device.vi.regs[VI_CURRENT_REG])
|
||||
}
|
||||
|
||||
pub fn read_regs(
|
||||
@@ -83,7 +83,7 @@ pub fn read_regs(
|
||||
_access_size: device::memory::AccessSize,
|
||||
) -> u32 {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
if reg as u32 == VI_CURRENT_REG {
|
||||
if reg as usize == VI_CURRENT_REG {
|
||||
set_current_line(device)
|
||||
}
|
||||
device::cop0::add_cycles(device, 20);
|
||||
@@ -92,7 +92,7 @@ pub fn read_regs(
|
||||
|
||||
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
match reg as usize {
|
||||
VI_CURRENT_REG => device::mi::clear_rcp_interrupt(device, device::mi::MI_INTR_VI),
|
||||
VI_V_SYNC_REG => {
|
||||
if device.vi.regs[reg as usize] != value & mask {
|
||||
@@ -141,12 +141,19 @@ pub fn vertical_interrupt_event(device: &mut device::Device) {
|
||||
}
|
||||
}
|
||||
|
||||
if device.vi.vi_counter.is_multiple_of(device.vi.limit_freq) && device.vi.enable_speed_limiter {
|
||||
speed_limiter(device, speed_limiter_toggled);
|
||||
if speed_limiter_toggled {
|
||||
reset_pace_deadline(device);
|
||||
}
|
||||
|
||||
if device.vi.vi_counter.is_multiple_of(device.vi.limit_freq) && device.vi.enable_speed_limiter {
|
||||
speed_limiter(device);
|
||||
}
|
||||
|
||||
unsafe { sdl3_sys::events::SDL_PumpEvents() };
|
||||
|
||||
ui::video::update_screen();
|
||||
device.vi.vi_counter += 1;
|
||||
device.vi.elapsed_time += device.vi.frame_time;
|
||||
|
||||
if device.netplay.is_none() && paused {
|
||||
if retroachievements::get_hardcore() {
|
||||
@@ -159,10 +166,8 @@ pub fn vertical_interrupt_event(device: &mut device::Device) {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
device.vi.field ^= (device.vi.regs[VI_STATUS_REG] >> 6) & 0x1;
|
||||
|
||||
device::mi::set_rcp_interrupt(device, device::mi::MI_INTR_VI);
|
||||
|
||||
@@ -181,7 +186,8 @@ pub fn init(device: &mut device::Device) {
|
||||
}
|
||||
}
|
||||
|
||||
fn speed_limiter(device: &mut device::Device, mut speed_limiter_toggled: bool) {
|
||||
fn speed_limiter(device: &mut device::Device) {
|
||||
let mut speed_limiter_toggled = false;
|
||||
let interval =
|
||||
std::time::Duration::from_secs_f64(device.vi.frame_time * device.vi.limit_freq as f64);
|
||||
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
#![deny(warnings)]
|
||||
|
||||
mod cheats;
|
||||
mod device;
|
||||
mod netplay;
|
||||
mod retroachievements;
|
||||
mod savestates;
|
||||
mod ui;
|
||||
use clap::Parser;
|
||||
#[cfg(target_os = "android")]
|
||||
use slint::ComponentHandle;
|
||||
use std::io::Error;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
use ui::android;
|
||||
|
||||
/// N64 emulator
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version=env!("GIT_DESCRIBE"), about, long_about = None, arg_required_else_help = if cfg!(feature = "gui") { false } else { true })]
|
||||
pub struct Args {
|
||||
pub game: Option<String>,
|
||||
#[arg(short, long)]
|
||||
pub fullscreen: bool,
|
||||
#[arg(long)]
|
||||
pub overclock: Option<bool>,
|
||||
#[arg(long)]
|
||||
pub disable_expansion_pak: Option<bool>,
|
||||
#[arg(long, value_name = "CHEATS_FILE", hide = true)]
|
||||
pub cheats: Option<String>,
|
||||
#[arg(long, value_name = "NETPLAY_PEER_ADDR", hide = true)]
|
||||
pub netplay_peer_addr: Option<String>,
|
||||
#[arg(long, value_name = "NETPLAY_PLAYER_NUMBER", hide = true)]
|
||||
pub netplay_player_number: Option<u8>,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PROFILE_NAME",
|
||||
help = "Create a new input profile (keyboard/gamepad mappings)"
|
||||
)]
|
||||
pub configure_input_profile: Option<String>,
|
||||
#[arg(long, help = "Use DirectInput when configuring a new input profile")]
|
||||
pub use_dinput: bool,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "DEADZONE_PERCENTAGE",
|
||||
help = "Used along with --configure-input-profile to set the deadzone for analog sticks"
|
||||
)]
|
||||
pub deadzone: Option<i32>,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PROFILE_NAME",
|
||||
help = "Must also specify --port. Used to bind a previously created profile to a port"
|
||||
)]
|
||||
pub bind_input_profile: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Lists connected controllers which can be used in --assign-controller"
|
||||
)]
|
||||
pub list_controllers: bool,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "CONTROLLER_NUMBER",
|
||||
help = "Must also specify --port. Used to assign a controller listed in --list-controllers to a port"
|
||||
)]
|
||||
pub assign_controller: Option<i32>,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PORT",
|
||||
help = "Valid values: 1-4. To be used alongside --bind-input-profile and --assign-controller"
|
||||
)]
|
||||
pub port: Option<usize>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Clear all input profile bindings and controller assignments"
|
||||
)]
|
||||
pub clear_input_bindings: bool,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "SLOT",
|
||||
help = "Load savestate from slot 0-9 when starting the game"
|
||||
)]
|
||||
pub load_state: Option<u32>,
|
||||
#[arg(
|
||||
long = "ra-username",
|
||||
value_name = "USERNAME",
|
||||
help = "Username for RetroAchievements"
|
||||
)]
|
||||
pub ra_username: Option<String>,
|
||||
#[arg(
|
||||
long = "ra-password",
|
||||
value_name = "PASSWORD",
|
||||
help = "Password for RetroAchievements"
|
||||
)]
|
||||
pub ra_password: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
|
||||
let dirs = ui::get_dirs();
|
||||
|
||||
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"))?;
|
||||
|
||||
ui::sdl_hints();
|
||||
|
||||
if let Some(game) = args.game {
|
||||
let file_path = std::path::Path::new(&game).to_path_buf();
|
||||
let Some(rom_contents) = device::get_rom_contents(&file_path) else {
|
||||
return Err(Error::other(format!(
|
||||
"Could not read ROM file: {}",
|
||||
file_path.display()
|
||||
)));
|
||||
};
|
||||
|
||||
let cheats = if let Some(cheats_file) = args.cheats {
|
||||
if let Ok(data) = std::fs::read(cheats_file)
|
||||
&& let Ok(cheats) = serde_json::from_slice(&data)
|
||||
{
|
||||
cheats
|
||||
} else {
|
||||
return Err(Error::other("Could not read cheats file"));
|
||||
}
|
||||
} else {
|
||||
let game_crc = ui::storage::get_game_crc(&rom_contents);
|
||||
ui::config::Cheats::new()
|
||||
.cheats
|
||||
.get(&game_crc)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
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 mut shutdown_tx = None;
|
||||
let mut usb_handle = 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()
|
||||
&& let Ok(rom) = std::fs::read(&device.ui.config.input.gb_rom_path[i])
|
||||
&& let Ok(ram) = std::fs::read(&device.ui.config.input.gb_ram_path[i])
|
||||
{
|
||||
device::controller::gbcart::init(&mut device.transferpaks[i].cart, &rom, &ram);
|
||||
}
|
||||
}
|
||||
|
||||
if device.ui.config.emulation.usb {
|
||||
(shutdown_tx, usb_handle, device.ui.usb) = ui::usb::init();
|
||||
}
|
||||
}
|
||||
|
||||
let file_path = dirs.config_dir.join("retroachievements.json");
|
||||
let ra_config = if let ra_config = std::fs::read(&file_path).unwrap_or_default()
|
||||
&& let ra_config =
|
||||
serde_json::from_slice::<retroachievements::RAConfig>(ra_config.as_ref())
|
||||
.unwrap_or_default()
|
||||
&& (ra_config.enabled || args.ra_username.is_some())
|
||||
{
|
||||
let username = args.ra_username.unwrap_or(ra_config.username.clone());
|
||||
retroachievements::init_client(
|
||||
if cfg!(ra_hardcore_enabled) {
|
||||
ra_config.hardcore
|
||||
} else {
|
||||
false
|
||||
},
|
||||
ra_config.challenge,
|
||||
ra_config.leaderboard,
|
||||
);
|
||||
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
|
||||
if let Some(password) = args.ra_password {
|
||||
retroachievements::login_user(username, password, tx);
|
||||
} else if !ra_config.token.is_empty() {
|
||||
retroachievements::login_token_user(username, ra_config.token.clone(), tx);
|
||||
} else {
|
||||
tx.send(false).unwrap();
|
||||
}
|
||||
|
||||
rx.await.unwrap();
|
||||
ra_config
|
||||
} else {
|
||||
retroachievements::RAConfig::default()
|
||||
};
|
||||
|
||||
device::run_game(
|
||||
&mut device,
|
||||
&rom_contents,
|
||||
ui::GameSettings {
|
||||
overclock,
|
||||
disable_expansion_pak,
|
||||
cheats,
|
||||
load_savestate_slot: args.load_state,
|
||||
},
|
||||
ra_config,
|
||||
)
|
||||
.await;
|
||||
|
||||
// on Android, the client is shut down in the app_window function
|
||||
#[cfg(not(target_os = "android"))]
|
||||
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()
|
||||
&& !device.transferpaks[i].cart.ram.is_empty()
|
||||
{
|
||||
device::controller::gbcart::save(
|
||||
&mut device.transferpaks[i].cart,
|
||||
device.vi.elapsed_time as i64,
|
||||
&device.ui.config.input.gb_ram_path[i],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui::usb::close(shutdown_tx, usb_handle).await;
|
||||
} else if arg_count > 1 {
|
||||
let mut config = ui::config::Config::new();
|
||||
|
||||
if let Some(profile) = args.configure_input_profile {
|
||||
ui::input::configure_input_profile(
|
||||
&mut config,
|
||||
profile,
|
||||
args.use_dinput,
|
||||
args.deadzone.unwrap_or(ui::input::DEADZONE_DEFAULT),
|
||||
);
|
||||
|
||||
ui::sdl_close();
|
||||
return Ok(());
|
||||
} else {
|
||||
if args.clear_input_bindings {
|
||||
ui::input::clear_bindings(&mut config);
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(port) = args.port
|
||||
&& !(1..=4).contains(&port)
|
||||
{
|
||||
return Err(Error::other("Port must be between 1 and 4"));
|
||||
}
|
||||
|
||||
ui::sdl_init(sdl3_sys::init::SDL_INIT_GAMEPAD);
|
||||
|
||||
if args.list_controllers {
|
||||
let controllers = ui::input::get_controller_names();
|
||||
for (i, controller) in controllers.iter().enumerate() {
|
||||
println!("Controller {i}: {controller}");
|
||||
}
|
||||
} else {
|
||||
if let Some(assign_controller) = args.assign_controller {
|
||||
let Some(port) = args.port else {
|
||||
ui::sdl_close();
|
||||
return Err(Error::other("Must specify port number"));
|
||||
};
|
||||
ui::input::assign_controller(&mut config, assign_controller - 1, port);
|
||||
}
|
||||
if let Some(profile) = args.bind_input_profile {
|
||||
let Some(port) = args.port else {
|
||||
ui::sdl_close();
|
||||
return Err(Error::other("Must specify port number"));
|
||||
};
|
||||
ui::input::bind_input_profile(&mut config, profile, port);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
#[cfg(feature = "gui")]
|
||||
{
|
||||
let app = ui::gui::AppWindow::new().unwrap();
|
||||
ui::gui::app_window(&app, false);
|
||||
}
|
||||
}
|
||||
|
||||
ui::sdl_close();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[unsafe(no_mangle)]
|
||||
#[tokio::main(worker_threads = 4)]
|
||||
async fn android_main(app: slint::android::AndroidApp) {
|
||||
slint::android::init_with_event_listener(app.clone(), move |event| match event {
|
||||
slint::android::android_activity::PollEvent::Main(main_event) => match main_event {
|
||||
slint::android::android_activity::MainEvent::TerminateWindow { .. } => {
|
||||
if let Ok(weak_app_window) = android::WEAK_SLINT_WINDOW.lock()
|
||||
&& let Some(weak_app_window) = weak_app_window.as_ref()
|
||||
{
|
||||
weak_app_window
|
||||
.upgrade_in_event_loop(move |handle| ui::gui::save_settings(&handle))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
})
|
||||
.unwrap();
|
||||
let app_window = ui::gui::AppWindow::new().unwrap();
|
||||
*android::WEAK_SLINT_WINDOW.lock().unwrap() = Some(app_window.as_weak());
|
||||
|
||||
*android::ANDROID_APP.lock().unwrap() = Some(app);
|
||||
|
||||
let dirs = ui::get_dirs();
|
||||
|
||||
std::fs::create_dir_all(&dirs.config_dir).unwrap();
|
||||
std::fs::create_dir_all(&dirs.cache_dir).unwrap();
|
||||
std::fs::create_dir_all(dirs.data_dir.join("saves")).unwrap();
|
||||
std::fs::create_dir_all(dirs.data_dir.join("states")).unwrap();
|
||||
|
||||
ui::gui::app_window(&app_window, true);
|
||||
*android::WEAK_SLINT_WINDOW.lock().unwrap() = None;
|
||||
*android::ANDROID_APP.lock().unwrap() = None;
|
||||
}
|
||||
@@ -3,298 +3,10 @@
|
||||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
mod cheats;
|
||||
mod device;
|
||||
mod netplay;
|
||||
mod retroachievements;
|
||||
mod savestates;
|
||||
mod ui;
|
||||
use clap::Parser;
|
||||
use std::io::Error;
|
||||
|
||||
/// N64 emulator
|
||||
#[derive(Parser, Debug)]
|
||||
#[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(
|
||||
long,
|
||||
value_name = "PROFILE_NAME",
|
||||
help = "Create a new input profile (keyboard/gamepad mappings)"
|
||||
)]
|
||||
configure_input_profile: Option<String>,
|
||||
#[arg(long, help = "Use DirectInput when configuring a new input profile")]
|
||||
use_dinput: bool,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "DEADZONE_PERCENTAGE",
|
||||
help = "Used along with --configure-input-profile to set the deadzone for analog sticks"
|
||||
)]
|
||||
deadzone: Option<i32>,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PROFILE_NAME",
|
||||
help = "Must also specify --port. Used to bind a previously created profile to a port"
|
||||
)]
|
||||
bind_input_profile: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Lists connected controllers which can be used in --assign-controller"
|
||||
)]
|
||||
list_controllers: bool,
|
||||
#[arg(
|
||||
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(
|
||||
long,
|
||||
value_name = "PORT",
|
||||
help = "Valid values: 1-4. To be used alongside --bind-input-profile and --assign-controller"
|
||||
)]
|
||||
port: Option<usize>,
|
||||
#[arg(
|
||||
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(worker_threads = 4)]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let dirs = ui::get_dirs();
|
||||
|
||||
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();
|
||||
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 Ok(());
|
||||
}
|
||||
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 Ok(());
|
||||
}
|
||||
if let Some(profile) = args.configure_input_profile {
|
||||
ui::input::configure_input_profile(
|
||||
&mut ui,
|
||||
profile,
|
||||
args.use_dinput,
|
||||
args.deadzone.unwrap_or(ui::input::DEADZONE_DEFAULT),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
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 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 {
|
||||
#[cfg(feature = "gui")]
|
||||
{
|
||||
retroachievements::init_client(false, false, false);
|
||||
ui::gui::app_window();
|
||||
retroachievements::shutdown_client();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
let args = gopher64::Args::parse();
|
||||
gopher64::run(args, std::env::args().count()).await
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ fn request_input(netplay: &Netplay, channel: usize) {
|
||||
|
||||
fn process_incoming(netplay: &mut Netplay) {
|
||||
let mut buf: [u8; 1024] = [0; 1024];
|
||||
while let Ok(_incoming) = netplay.udp_socket.recv(&mut buf) {
|
||||
while netplay.udp_socket.recv(&mut buf).is_ok() {
|
||||
match buf[0] {
|
||||
UDP_RECEIVE_KEY_INFO | UDP_RECEIVE_KEY_INFO_GRATUITOUS => {
|
||||
let player = buf[1] as usize;
|
||||
|
||||
@@ -2,7 +2,9 @@ include!(concat!(env!("OUT_DIR"), "/retroachievements_bindings.rs"));
|
||||
|
||||
use crate::ui;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
use discord_rich_presence::DiscordIpc;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Default)]
|
||||
pub struct RAConfig {
|
||||
pub username: String,
|
||||
pub token: String,
|
||||
@@ -10,6 +12,7 @@ pub struct RAConfig {
|
||||
pub hardcore: bool,
|
||||
pub challenge: bool,
|
||||
pub leaderboard: bool,
|
||||
pub rich_presence: bool,
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
@@ -42,6 +45,7 @@ pub extern "C" fn store_retroachievements_credentials(
|
||||
hardcore: result.hardcore,
|
||||
challenge: result.challenge,
|
||||
leaderboard: result.leaderboard,
|
||||
rich_presence: result.rich_presence,
|
||||
}
|
||||
} else {
|
||||
RAConfig {
|
||||
@@ -51,6 +55,7 @@ pub extern "C" fn store_retroachievements_credentials(
|
||||
hardcore: false,
|
||||
challenge: false,
|
||||
leaderboard: false,
|
||||
rich_presence: false,
|
||||
}
|
||||
};
|
||||
let f = std::fs::File::create(&file_path).unwrap();
|
||||
@@ -132,25 +137,80 @@ pub extern "C" fn rust_server_call(
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn load_game(rom: &[u8], rom_size: usize) {
|
||||
pub async fn load_game(
|
||||
rom: &[u8],
|
||||
rom_size: usize,
|
||||
discord_rich_presence: bool,
|
||||
) -> (
|
||||
Option<tokio::sync::watch::Sender<()>>,
|
||||
Option<tokio::task::JoinHandle<()>>,
|
||||
) {
|
||||
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();
|
||||
|
||||
let mut c_title = std::ptr::null();
|
||||
let mut c_image_url = std::ptr::null();
|
||||
unsafe { ra_welcome(&mut c_title, &mut c_image_url) };
|
||||
|
||||
if !discord_rich_presence || c_title.is_null() || c_image_url.is_null() {
|
||||
(None, None)
|
||||
} else {
|
||||
let (discord_watch_tx, discord_watch_rx) = tokio::sync::watch::channel(());
|
||||
(
|
||||
Some(discord_watch_tx),
|
||||
Some(init_rich_presence(
|
||||
discord_watch_rx,
|
||||
unsafe { std::ffi::CStr::from_ptr(c_title) }
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
unsafe { std::ffi::CStr::from_ptr(c_image_url) }
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unload_game() {
|
||||
pub async fn unload_game(
|
||||
discord_watch_tx: Option<tokio::sync::watch::Sender<()>>,
|
||||
discord_handle: Option<tokio::task::JoinHandle<()>>,
|
||||
) {
|
||||
if let Some(mut discord_handle) = discord_handle
|
||||
&& let Some(discord_watch_tx) = discord_watch_tx
|
||||
{
|
||||
let _ = discord_watch_tx.send(());
|
||||
if tokio::time::timeout(std::time::Duration::from_secs(1), &mut discord_handle)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
discord_handle.abort();
|
||||
}
|
||||
}
|
||||
unsafe { ra_unload_game() };
|
||||
}
|
||||
|
||||
pub fn welcome() {
|
||||
unsafe { ra_welcome() };
|
||||
pub fn get_rich_presence() -> Option<String> {
|
||||
let c_rich_presence = unsafe { ra_get_rich_presence() };
|
||||
if c_rich_presence.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
unsafe { std::ffi::CStr::from_ptr(c_rich_presence) }
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dmem(dmem: *const u8, dmem_size: usize) {
|
||||
unsafe { ra_set_dmem(dmem, dmem_size) };
|
||||
pub fn set_rdram(rdram: *const u8, rdram_size: usize) {
|
||||
unsafe { ra_set_rdram(rdram, rdram_size) };
|
||||
}
|
||||
|
||||
pub fn do_frame() {
|
||||
@@ -211,21 +271,6 @@ pub fn get_username() -> Option<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() }
|
||||
}
|
||||
@@ -237,3 +282,52 @@ pub fn save_state(state: *mut u8, state_size: usize) {
|
||||
pub fn load_state(state: *const u8, state_size: usize) {
|
||||
unsafe { ra_load_state(state, state_size) };
|
||||
}
|
||||
|
||||
pub fn init_rich_presence(
|
||||
mut discord_watch_rx: tokio::sync::watch::Receiver<()>,
|
||||
game_title: String,
|
||||
game_image_url: String,
|
||||
) -> tokio::task::JoinHandle<()> {
|
||||
tokio::spawn(async move {
|
||||
let mut client = discord_rich_presence::DiscordIpcClient::new("1395482226463870986");
|
||||
let timestamps = discord_rich_presence::activity::Timestamps::new().start(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64,
|
||||
);
|
||||
|
||||
if let Err(e) = client.connect() {
|
||||
eprintln!("Failed to connect to Discord: {e}");
|
||||
return;
|
||||
}
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = discord_watch_rx.changed() => {
|
||||
if let Err(e) = client.clear_activity() {
|
||||
eprintln!("Failed to clear Discord activity: {e}");
|
||||
}
|
||||
if let Err(e) = client.close() {
|
||||
eprintln!("Failed to close Discord: {e}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {
|
||||
if let Some(rich_presence) = get_rich_presence()
|
||||
&& let Err(e) = client.set_activity(
|
||||
discord_rich_presence::activity::Activity::new()
|
||||
.details(game_title.clone())
|
||||
.state(rich_presence)
|
||||
.assets(
|
||||
discord_rich_presence::activity::Assets::new()
|
||||
.small_image(game_image_url.clone()),
|
||||
).timestamps(timestamps.clone()),
|
||||
)
|
||||
{
|
||||
eprintln!("Failed to set Discord activity: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -76,27 +76,38 @@ pub fn create_savestate(device: &device::Device) {
|
||||
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"),
|
||||
(
|
||||
&postcard::to_stdvec(&device.ui.storage.saves).unwrap(),
|
||||
"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);
|
||||
tokio::spawn(async move {
|
||||
tokio::fs::write(save_path, compressed_file).await.unwrap();
|
||||
});
|
||||
ui::video::onscreen_message(
|
||||
&format!(
|
||||
"Savestate created in slot {}",
|
||||
device.ui.storage.save_state_slot
|
||||
),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
|
||||
if let Ok(device_data) = postcard::to_stdvec(device)
|
||||
&& let Ok(saves_data) = postcard::to_stdvec(&device.ui.storage.saves)
|
||||
&& let Ok(compressed_file) = ui::storage::compress_file(&[
|
||||
(&device_data, "device"),
|
||||
(&saves_data, "saves"),
|
||||
(&rdp_state, "rdp_state"),
|
||||
(&ra_state, "ra_state"),
|
||||
])
|
||||
{
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = tokio::fs::write(save_path, compressed_file).await {
|
||||
eprintln!("Error writing savestate: {}", e);
|
||||
}
|
||||
});
|
||||
ui::video::onscreen_message(
|
||||
&format!(
|
||||
"Savestate created in slot {}",
|
||||
device.ui.storage.save_state_slot
|
||||
),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
|
||||
);
|
||||
} else {
|
||||
ui::video::onscreen_message(
|
||||
&format!(
|
||||
"Failed to create savestate in slot {}",
|
||||
device.ui.storage.save_state_slot
|
||||
),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_savestate(device: &mut device::Device) {
|
||||
@@ -109,38 +120,44 @@ pub fn load_savestate(device: &mut device::Device) {
|
||||
}
|
||||
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();
|
||||
if let Ok(device_bytes) = ui::storage::decompress_file(savestate, "device")
|
||||
&& let Ok(save_bytes) = ui::storage::decompress_file(savestate, "saves")
|
||||
&& let Ok(rdp_state) = ui::storage::decompress_file(savestate, "rdp_state")
|
||||
&& let Ok(ra_state) = ui::storage::decompress_file(savestate, "ra_state")
|
||||
&& let Ok(mut state) = postcard::from_bytes::<device::Device>(&device_bytes)
|
||||
&& let Ok(saves) = postcard::from_bytes(&save_bytes)
|
||||
&& device.rdram.size == state.rdram.size
|
||||
{
|
||||
device.ui.storage.saves = saves;
|
||||
|
||||
device.cpu = state.cpu;
|
||||
device.pif = state.pif;
|
||||
std::mem::swap(&mut device.cpu, &mut state.cpu);
|
||||
std::mem::swap(&mut device.pif, &mut state.pif);
|
||||
|
||||
let rom = device.cart.rom.clone();
|
||||
device.cart = state.cart;
|
||||
std::mem::swap(&mut device.cart, &mut state.cart);
|
||||
device.cart.rom = rom;
|
||||
|
||||
device.memory = state.memory;
|
||||
device.rsp = state.rsp;
|
||||
device.rdp = state.rdp;
|
||||
device.rdram = state.rdram;
|
||||
device.mi = state.mi;
|
||||
device.pi = state.pi;
|
||||
device.vi = state.vi;
|
||||
device.ai = state.ai;
|
||||
device.si = state.si;
|
||||
device.ri = state.ri;
|
||||
device.vru = state.vru;
|
||||
device.cheats = state.cheats;
|
||||
std::mem::swap(&mut device.memory, &mut state.memory);
|
||||
std::mem::swap(&mut device.rsp, &mut state.rsp);
|
||||
std::mem::swap(&mut device.rdp, &mut state.rdp);
|
||||
|
||||
device.rdram.mem.clone_from(&state.rdram.mem);
|
||||
device.rdram.regs = state.rdram.regs;
|
||||
|
||||
std::mem::swap(&mut device.mi, &mut state.mi);
|
||||
std::mem::swap(&mut device.pi, &mut state.pi);
|
||||
std::mem::swap(&mut device.vi, &mut state.vi);
|
||||
std::mem::swap(&mut device.ai, &mut state.ai);
|
||||
std::mem::swap(&mut device.si, &mut state.si);
|
||||
std::mem::swap(&mut device.ri, &mut state.ri);
|
||||
std::mem::swap(&mut device.vru, &mut state.vru);
|
||||
std::mem::swap(&mut device.cheats, &mut state.cheats);
|
||||
|
||||
let mut tpak_rom = [vec![], vec![], vec![], vec![]];
|
||||
for (i, item) in tpak_rom.iter_mut().enumerate() {
|
||||
*item = device.transferpaks[i].cart.rom.clone();
|
||||
}
|
||||
device.transferpaks = state.transferpaks;
|
||||
std::mem::swap(&mut device.transferpaks, &mut state.transferpaks);
|
||||
for (i, item) in tpak_rom.iter().enumerate() {
|
||||
device.transferpaks[i].cart.rom = item.clone();
|
||||
}
|
||||
@@ -201,9 +218,10 @@ 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::audio::init_game_audio(device);
|
||||
ui::video::load_state(device, rdp_state.as_ptr());
|
||||
|
||||
retroachievements::set_rdram(device.rdram.mem.as_ptr(), device.rdram.size as usize);
|
||||
if !ra_state.is_empty() {
|
||||
retroachievements::load_state(ra_state.as_ptr(), ra_state.len());
|
||||
} else {
|
||||
@@ -219,7 +237,7 @@ pub fn load_savestate(device: &mut device::Device) {
|
||||
"Savestate loaded from slot {}",
|
||||
device.ui.storage.save_state_slot
|
||||
),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
|
||||
);
|
||||
} else {
|
||||
ui::video::onscreen_message(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#[cfg(target_os = "android")]
|
||||
pub mod android;
|
||||
pub mod audio;
|
||||
#[cfg(feature = "gui")]
|
||||
pub mod cheats;
|
||||
@@ -12,7 +14,7 @@ pub mod retroachievements;
|
||||
pub mod storage;
|
||||
pub mod usb;
|
||||
pub mod video;
|
||||
#[cfg(feature = "gui")]
|
||||
#[cfg(all(feature = "gui", not(target_os = "android")))]
|
||||
pub mod vru;
|
||||
|
||||
pub static WEB_CLIENT: std::sync::LazyLock<reqwest::Client> = std::sync::LazyLock::new(|| {
|
||||
@@ -26,6 +28,7 @@ pub static WEB_CLIENT: std::sync::LazyLock<reqwest::Client> = std::sync::LazyLoc
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Dirs {
|
||||
pub config_dir: std::path::PathBuf,
|
||||
pub data_dir: std::path::PathBuf,
|
||||
@@ -38,7 +41,6 @@ pub struct Audio {
|
||||
}
|
||||
|
||||
pub struct Input {
|
||||
pub joysticks: Vec<sdl3_sys::joystick::SDL_JoystickID>,
|
||||
pub keyboard_state: *const bool,
|
||||
pub controllers: [input::Controllers; 4],
|
||||
}
|
||||
@@ -77,7 +79,6 @@ pub struct Ui {
|
||||
pub config: config::Config,
|
||||
pub game_id: String,
|
||||
pub game_hash: String,
|
||||
pub with_sdl: bool,
|
||||
pub audio: Audio,
|
||||
pub input: Input,
|
||||
pub storage: Storage,
|
||||
@@ -85,27 +86,32 @@ pub struct Ui {
|
||||
pub usb: Usb,
|
||||
}
|
||||
|
||||
impl Drop for Ui {
|
||||
fn drop(&mut self) {
|
||||
if self.with_sdl {
|
||||
unsafe {
|
||||
sdl3_ttf_sys::ttf::TTF_Quit();
|
||||
sdl3_sys::init::SDL_Quit();
|
||||
}
|
||||
}
|
||||
pub fn sdl_hints() {
|
||||
unsafe {
|
||||
let hint = std::ffi::CString::new("1").unwrap();
|
||||
sdl3_sys::everything::SDL_SetHint(
|
||||
sdl3_sys::everything::SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS,
|
||||
hint.as_ptr(),
|
||||
);
|
||||
sdl3_sys::everything::SDL_SetHint(
|
||||
sdl3_sys::everything::SDL_HINT_ANDROID_ALLOW_RECREATE_ACTIVITY,
|
||||
hint.as_ptr(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sdl_init(flag: sdl3_sys::init::SDL_InitFlags) {
|
||||
unsafe {
|
||||
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();
|
||||
let err = sdl3_sys::error::SDL_GetError();
|
||||
panic!(
|
||||
"Could not initialize SDL subsystem: {}, {}",
|
||||
u32::from(flag),
|
||||
err
|
||||
if err.is_null() {
|
||||
"Unknown error"
|
||||
} else {
|
||||
std::ffi::CStr::from_ptr(err).to_str().unwrap()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -119,27 +125,40 @@ pub fn ttf_init() {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sdl_close() {
|
||||
unsafe {
|
||||
sdl3_ttf_sys::ttf::TTF_Quit();
|
||||
sdl3_sys::init::SDL_Quit();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
if portable {
|
||||
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 {
|
||||
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"),
|
||||
#[cfg(target_os = "android")]
|
||||
return android::get_dirs();
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let exe_path = std::env::current_exe().unwrap();
|
||||
let portable_dir = exe_path.parent();
|
||||
let portable = portable_dir.unwrap().join("portable.txt").exists();
|
||||
if portable {
|
||||
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 {
|
||||
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<sdl3_sys::everything::SDL_JoystickID>, with_sdl: bool) -> Ui {
|
||||
pub fn new() -> Ui {
|
||||
let dirs = get_dirs();
|
||||
|
||||
let (fps_tx, fps_rx) = tokio::sync::mpsc::channel(1000);
|
||||
@@ -151,25 +170,32 @@ impl Ui {
|
||||
game_controller: std::ptr::null_mut(),
|
||||
joystick: std::ptr::null_mut(),
|
||||
rumble: false,
|
||||
guid: sdl3_sys::guid::SDL_GUID::default(),
|
||||
last_key_state: 0,
|
||||
},
|
||||
input::Controllers {
|
||||
game_controller: std::ptr::null_mut(),
|
||||
joystick: std::ptr::null_mut(),
|
||||
rumble: false,
|
||||
guid: sdl3_sys::guid::SDL_GUID::default(),
|
||||
last_key_state: 0,
|
||||
},
|
||||
input::Controllers {
|
||||
game_controller: std::ptr::null_mut(),
|
||||
joystick: std::ptr::null_mut(),
|
||||
rumble: false,
|
||||
guid: sdl3_sys::guid::SDL_GUID::default(),
|
||||
last_key_state: 0,
|
||||
},
|
||||
input::Controllers {
|
||||
game_controller: std::ptr::null_mut(),
|
||||
joystick: std::ptr::null_mut(),
|
||||
rumble: false,
|
||||
guid: sdl3_sys::guid::SDL_GUID::default(),
|
||||
last_key_state: 0,
|
||||
},
|
||||
],
|
||||
keyboard_state: std::ptr::null_mut(),
|
||||
joysticks,
|
||||
},
|
||||
storage: Storage {
|
||||
save_state_slot: 0,
|
||||
@@ -231,26 +257,6 @@ impl Ui {
|
||||
cart_rx: None,
|
||||
},
|
||||
dirs,
|
||||
with_sdl,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default() -> Ui {
|
||||
Self::construct_ui(vec![], false)
|
||||
}
|
||||
|
||||
pub fn new() -> Ui {
|
||||
sdl_init(sdl3_sys::init::SDL_INIT_GAMEPAD);
|
||||
let mut num_joysticks = 0;
|
||||
let joysticks = unsafe { sdl3_sys::joystick::SDL_GetJoysticks(&mut num_joysticks) };
|
||||
if joysticks.is_null() {
|
||||
panic!("Could not get joystick list");
|
||||
}
|
||||
let mut joystick_vec = vec![];
|
||||
for i in 0..num_joysticks {
|
||||
joystick_vec.push(unsafe { *joysticks.add(i as usize) });
|
||||
}
|
||||
unsafe { sdl3_sys::stdinc::SDL_free(joysticks as *mut std::ffi::c_void) }
|
||||
Self::construct_ui(joystick_vec, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,667 @@
|
||||
use crate::Args;
|
||||
use crate::run;
|
||||
use crate::ui;
|
||||
use clap::Parser;
|
||||
use jni::objects::{JClass, JObject, JObjectArray, JString};
|
||||
use jni::refs::Global;
|
||||
use jni::sys::jint;
|
||||
use jni::{Env, EnvUnowned, JavaVM, bind_java_type};
|
||||
use std::os::fd::FromRawFd;
|
||||
|
||||
const REQUEST_SELECT_ROM: jint = 1;
|
||||
const CONFIGURE_INPUT_PROFILE: jint = 2;
|
||||
const RUN_ROM: jint = 3;
|
||||
|
||||
pub static ANDROID_APP: std::sync::Mutex<Option<slint::android::AndroidApp>> =
|
||||
std::sync::Mutex::new(None);
|
||||
|
||||
pub static SELECT_ROM_TX: std::sync::Mutex<
|
||||
Option<tokio::sync::oneshot::Sender<Option<std::path::PathBuf>>>,
|
||||
> = std::sync::Mutex::new(None);
|
||||
|
||||
pub static WEAK_SLINT_WINDOW: std::sync::Mutex<Option<slint::Weak<ui::gui::AppWindow>>> =
|
||||
std::sync::Mutex::new(None);
|
||||
|
||||
bind_java_type! {
|
||||
DocumentsContract => "android.provider.DocumentsContract",
|
||||
fields {
|
||||
#[allow(non_snake_case)]
|
||||
static EXTRA_INITIAL_URI: JString,
|
||||
},
|
||||
}
|
||||
|
||||
bind_java_type! {
|
||||
AndroidActivity => "android.app.Activity",
|
||||
type_map = {
|
||||
AndroidIntent => "android.content.Intent",
|
||||
AndroidContentResolver => "android.content.ContentResolver",
|
||||
},
|
||||
methods {
|
||||
fn start_activity(intent: AndroidIntent) -> (),
|
||||
fn start_activity_for_result(intent: AndroidIntent, request_code: jint) -> (),
|
||||
fn get_content_resolver() -> AndroidContentResolver,
|
||||
},
|
||||
fields {
|
||||
#[allow(non_snake_case)]
|
||||
static RESULT_OK: jint,
|
||||
},
|
||||
}
|
||||
|
||||
bind_java_type! {
|
||||
AndroidIntent => "android.content.Intent",
|
||||
type_map = {
|
||||
AndroidUri => "android.net.Uri",
|
||||
},
|
||||
fields {
|
||||
#[allow(non_snake_case)]
|
||||
static ACTION_VIEW: JString,
|
||||
#[allow(non_snake_case)]
|
||||
static ACTION_OPEN_DOCUMENT: JString,
|
||||
#[allow(non_snake_case)]
|
||||
static CATEGORY_OPENABLE: JString,
|
||||
#[allow(non_snake_case)]
|
||||
static FLAG_ACTIVITY_NEW_TASK: jint,
|
||||
#[allow(non_snake_case)]
|
||||
static FLAG_GRANT_READ_URI_PERMISSION: jint,
|
||||
},
|
||||
constructors {
|
||||
fn new(),
|
||||
fn with_action(action: JString),
|
||||
},
|
||||
methods {
|
||||
fn set_data(uri: AndroidUri) -> AndroidIntent,
|
||||
fn set_type(r#type: JString) -> AndroidIntent,
|
||||
fn add_category(category: JString) -> AndroidIntent,
|
||||
fn get_data() -> AndroidUri,
|
||||
fn put_extra_string {
|
||||
sig = (extra: JString, value: JString) -> AndroidIntent,
|
||||
name = "putExtra",
|
||||
},
|
||||
fn put_extra_string_array {
|
||||
sig = (extra: JString, value: JString[]) -> AndroidIntent,
|
||||
name = "putExtra",
|
||||
},
|
||||
fn get_string_extra(name: JString) -> JString,
|
||||
fn set_class_name(package_name: JString, class_name: JString) -> AndroidIntent,
|
||||
},
|
||||
}
|
||||
|
||||
bind_java_type! {
|
||||
ParcelFileDescriptor => "android.os.ParcelFileDescriptor",
|
||||
methods {
|
||||
fn close() -> (),
|
||||
fn detach_fd() -> jint,
|
||||
},
|
||||
}
|
||||
|
||||
bind_java_type! {
|
||||
AndroidContentResolver => "android.content.ContentResolver",
|
||||
type_map = {
|
||||
AndroidUri => "android.net.Uri",
|
||||
ParcelFileDescriptor => "android.os.ParcelFileDescriptor",
|
||||
},
|
||||
methods {
|
||||
fn take_persistable_uri_permission(uri: AndroidUri, flags: jint) -> (),
|
||||
fn open_file_descriptor(uri: AndroidUri, mode: JString) -> ParcelFileDescriptor,
|
||||
},
|
||||
}
|
||||
|
||||
bind_java_type! {
|
||||
AndroidUri => "android.net.Uri",
|
||||
methods {
|
||||
static fn parse(uri_string: JString) -> AndroidUri,
|
||||
fn to_string() -> JString,
|
||||
static fn decode(path: JString) -> JString,
|
||||
},
|
||||
}
|
||||
|
||||
bind_java_type! {
|
||||
pub AndroidInputDevice => "android.view.InputDevice",
|
||||
fields {
|
||||
#[allow(non_snake_case)]
|
||||
static SOURCE_JOYSTICK: jint,
|
||||
#[allow(non_snake_case)]
|
||||
static SOURCE_GAMEPAD: jint,
|
||||
},
|
||||
methods {
|
||||
static fn get_device_ids() -> jint[],
|
||||
static fn get_device(device_id: jint) -> AndroidInputDevice,
|
||||
fn supports_source(source: jint) -> jboolean,
|
||||
fn is_virtual() -> jboolean,
|
||||
fn is_external() -> jboolean,
|
||||
fn get_vendor_id() -> jint,
|
||||
fn get_product_id() -> jint,
|
||||
fn get_name() -> JString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ControllerInfo {
|
||||
pub name: String,
|
||||
/// Stable ID from [`InputDevice.getDescriptor`](https://developer.android.com/reference/android/view/InputDevice#getDescriptor()).
|
||||
pub descriptor: String,
|
||||
}
|
||||
|
||||
fn argv_to_strings(argc: std::ffi::c_int, argv: *mut *mut std::ffi::c_char) -> Vec<String> {
|
||||
if argc <= 0 || argv.is_null() {
|
||||
return Vec::new();
|
||||
}
|
||||
unsafe {
|
||||
(0..argc as usize)
|
||||
.map(|i| {
|
||||
std::ffi::CStr::from_ptr(*argv.add(i))
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
#[tokio::main(worker_threads = 4)]
|
||||
pub async extern "C" fn gopher64_sdl_main(
|
||||
argc: std::ffi::c_int,
|
||||
argv: *mut *mut std::ffi::c_char,
|
||||
) -> std::ffi::c_int {
|
||||
let raw = argv_to_strings(argc, argv);
|
||||
let args = Args::try_parse_from(raw).unwrap();
|
||||
if let Err(err) = run(args, argc as usize).await {
|
||||
eprintln!("Error running game: {err:?}");
|
||||
return 1;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
pub fn spawn_configure_input_profile(
|
||||
profile_name: slint::SharedString,
|
||||
dinput: bool,
|
||||
deadzone: i32,
|
||||
) {
|
||||
if let Some(vm) = get_vm() {
|
||||
if let Err(err) = vm.attach_current_thread(|env| {
|
||||
start_configure_input_profile_on_jvm(env, profile_name.to_string(), dinput, deadzone)
|
||||
}) {
|
||||
eprintln!("JNI error while starting N64Activity: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_configure_input_profile_on_jvm(
|
||||
env: &mut Env<'_>,
|
||||
profile_name: String,
|
||||
dinput: bool,
|
||||
deadzone: i32,
|
||||
) -> jni::errors::Result<()> {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
|
||||
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? };
|
||||
|
||||
let package_name = JString::from_str(env, "io.github.gopher64.gopher64")?;
|
||||
let class_name = JString::from_str(env, "io.github.gopher64.gopher64.N64Activity")?;
|
||||
|
||||
let args_key = JString::from_str(env, "args")?;
|
||||
let mut args = vec![
|
||||
JString::from_str(env, "--configure-input-profile")?,
|
||||
JString::from_str(env, &profile_name)?,
|
||||
JString::from_str(env, "--deadzone")?,
|
||||
JString::from_str(env, &deadzone.to_string())?,
|
||||
];
|
||||
if dinput {
|
||||
args.push(JString::from_str(env, "--use-dinput")?);
|
||||
}
|
||||
let empty_arg = JString::from_str(env, "")?;
|
||||
let j_args = JObjectArray::<JString>::new(env, args.len(), empty_arg)?;
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
j_args.set_element(env, i, arg)?;
|
||||
}
|
||||
let intent = AndroidIntent::new(env)?
|
||||
.set_class_name(env, &package_name, &class_name)?
|
||||
.put_extra_string_array(env, &args_key, &j_args)?;
|
||||
|
||||
activity
|
||||
.as_ref()
|
||||
.start_activity_for_result(env, &intent, CONFIGURE_INPUT_PROFILE)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(jni::errors::Error::UninitializedJavaVM)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_rom(
|
||||
file_path: std::path::PathBuf,
|
||||
game_settings: ui::GameSettings,
|
||||
netplay: Option<ui::gui::NetplayDevice>,
|
||||
weak: slint::Weak<ui::gui::AppWindow>,
|
||||
) {
|
||||
if let Some(vm) = get_vm() {
|
||||
if let Err(err) = vm.attach_current_thread(|env| {
|
||||
start_run_rom_on_jvm(env, file_path, game_settings, netplay, weak)
|
||||
}) {
|
||||
eprintln!("JNI error while starting N64Activity: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_run_rom_on_jvm(
|
||||
env: &mut Env<'_>,
|
||||
file_path: std::path::PathBuf,
|
||||
game_settings: ui::GameSettings,
|
||||
netplay: Option<ui::gui::NetplayDevice>,
|
||||
weak: slint::Weak<ui::gui::AppWindow>,
|
||||
) -> jni::errors::Result<()> {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
|
||||
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? };
|
||||
|
||||
let package_name = JString::from_str(env, "io.github.gopher64.gopher64")?;
|
||||
let class_name = JString::from_str(env, "io.github.gopher64.gopher64.N64Activity")?;
|
||||
|
||||
let file_path_key = JString::from_str(env, "file_path")?;
|
||||
let file_path = file_path.to_str().unwrap();
|
||||
let cheats_path_key = JString::from_str(env, "cheats_path")?;
|
||||
let cheats_path = app
|
||||
.internal_data_path()
|
||||
.unwrap()
|
||||
.join("cache")
|
||||
.join("cheats.json");
|
||||
|
||||
let args_key = JString::from_str(env, "args")?;
|
||||
let mut args = vec![
|
||||
JString::from_str(env, file_path)?,
|
||||
JString::from_str(env, "--fullscreen")?,
|
||||
JString::from_str(env, "--overclock")?,
|
||||
JString::from_str(env, &game_settings.overclock.to_string())?,
|
||||
JString::from_str(env, "--disable-expansion-pak")?,
|
||||
JString::from_str(env, &game_settings.disable_expansion_pak.to_string())?,
|
||||
];
|
||||
|
||||
if let Some(netplay) = netplay {
|
||||
args.push(JString::from_str(env, "--netplay-peer-addr")?);
|
||||
args.push(JString::from_str(env, &netplay.peer_addr.to_string())?);
|
||||
args.push(JString::from_str(env, "--netplay-player-number")?);
|
||||
args.push(JString::from_str(env, &netplay.player_number.to_string())?);
|
||||
args.push(JString::from_str(env, "--cheats")?);
|
||||
args.push(JString::from_str(env, cheats_path.to_str().unwrap())?);
|
||||
|
||||
let f = std::fs::File::create(&cheats_path).unwrap();
|
||||
serde_json::to_writer_pretty(f, &game_settings.cheats).unwrap();
|
||||
}
|
||||
|
||||
let empty_arg = JString::from_str(env, "")?;
|
||||
let j_args = JObjectArray::<JString>::new(env, args.len(), empty_arg)?;
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
j_args.set_element(env, i, arg)?;
|
||||
}
|
||||
|
||||
let file_path_string = JString::from_str(env, file_path)?;
|
||||
let cheats_path_string = JString::from_str(env, cheats_path.to_str().unwrap())?;
|
||||
let intent = AndroidIntent::new(env)?
|
||||
.set_class_name(env, &package_name, &class_name)?
|
||||
.put_extra_string(env, &file_path_key, &file_path_string)?
|
||||
.put_extra_string(env, &cheats_path_key, &cheats_path_string)?
|
||||
.put_extra_string_array(env, &args_key, &j_args)?;
|
||||
|
||||
weak.upgrade_in_event_loop(move |handle| handle.set_game_running(true))
|
||||
.unwrap();
|
||||
|
||||
activity
|
||||
.as_ref()
|
||||
.start_activity_for_result(env, &intent, RUN_ROM)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(jni::errors::Error::UninitializedJavaVM)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_vm() -> Option<JavaVM> {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
Some(unsafe { JavaVM::from_raw(app.vm_as_ptr().cast()) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode_path(path: &str) -> String {
|
||||
if let Some(vm) = get_vm() {
|
||||
match vm.attach_current_thread(|env| decode_path_on_jvm(env, path)) {
|
||||
Ok(decoded_path) => decoded_path,
|
||||
Err(err) => {
|
||||
eprintln!("JNI error while decoding path: {err:?}");
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Android app not initialized");
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_path_on_jvm(env: &mut Env<'_>, path: &str) -> jni::errors::Result<String> {
|
||||
let path = JString::from_str(env, path)?;
|
||||
let decoded_path = AndroidUri::decode(env, &path)?;
|
||||
Ok(decoded_path.try_to_string(env)?)
|
||||
}
|
||||
|
||||
/// Lists connected gamepads and joysticks using the Android framework.
|
||||
pub fn list_controllers() -> Vec<ControllerInfo> {
|
||||
if let Some(vm) = get_vm() {
|
||||
match vm.attach_current_thread(list_controllers_on_jvm) {
|
||||
Ok(controllers) => controllers,
|
||||
Err(err) => {
|
||||
eprintln!("JNI error while listing controllers: {err:?}");
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Android app not initialized");
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn list_controllers_on_jvm(env: &mut Env<'_>) -> jni::errors::Result<Vec<ControllerInfo>> {
|
||||
let device_ids = AndroidInputDevice::get_device_ids(env)?;
|
||||
let count = device_ids.len(env)?;
|
||||
let mut ids = vec![0i32; count];
|
||||
if count > 0 {
|
||||
device_ids.get_region(env, 0, &mut ids)?;
|
||||
}
|
||||
|
||||
let mut controllers = Vec::new();
|
||||
for device_id in ids {
|
||||
let device = AndroidInputDevice::get_device(env, device_id)?;
|
||||
if device.is_null() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let supports_gamepad =
|
||||
device.supports_source(env, AndroidInputDevice::SOURCE_GAMEPAD(env)?)?;
|
||||
let supports_joystick =
|
||||
device.supports_source(env, AndroidInputDevice::SOURCE_JOYSTICK(env)?)?;
|
||||
if !supports_gamepad && !supports_joystick {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = if let Ok(name) = device.get_name(env)
|
||||
&& let Ok(name) = name.try_to_string(env)
|
||||
{
|
||||
name
|
||||
} else {
|
||||
ui::input::UNKNOWN_CONTROLLER_NAME.to_string()
|
||||
};
|
||||
let descriptor = if let Ok(product_id) = device.get_product_id(env)
|
||||
&& let Ok(vendor_id) = device.get_vendor_id(env)
|
||||
{
|
||||
format!("{}:{}:{}", name, vendor_id, product_id)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
controllers.push(ControllerInfo { name, descriptor });
|
||||
}
|
||||
|
||||
Ok(controllers)
|
||||
}
|
||||
|
||||
/// Opens a URI in the user's default app via [`Intent::ACTION_VIEW`](https://developer.android.com/reference/android/content/Intent#ACTION_VIEW).
|
||||
pub fn open_uri(path: &str) {
|
||||
if let Some(vm) = get_vm() {
|
||||
let path = path.to_string();
|
||||
if let Err(err) = vm.attach_current_thread(|env| open_uri_on_jvm(env, &path)) {
|
||||
eprintln!("JNI error while opening URI: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_uri_on_jvm(env: &mut Env<'_>, path: &str) -> jni::errors::Result<()> {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
|
||||
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? };
|
||||
|
||||
let uri_string = JString::from_str(env, path)?;
|
||||
let uri = AndroidUri::parse(env, &uri_string)?;
|
||||
|
||||
let action_view = AndroidIntent::ACTION_VIEW(env)?;
|
||||
let intent = AndroidIntent::with_action(env, &action_view)?.set_data(env, &uri)?;
|
||||
|
||||
activity.as_ref().start_activity(env, &intent)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(jni::errors::Error::UninitializedJavaVM)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn select_rom(rom_dir: slint::SharedString) -> Option<std::path::PathBuf> {
|
||||
if let Some(vm) = get_vm() {
|
||||
if let Err(err) =
|
||||
vm.attach_current_thread(|env| select_rom_on_jvm(env, rom_dir.to_string()))
|
||||
{
|
||||
eprintln!("JNI error while opening URI: {err:?}");
|
||||
return None;
|
||||
}
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<Option<std::path::PathBuf>>();
|
||||
if let Ok(mut tx_lock) = SELECT_ROM_TX.lock() {
|
||||
tx_lock.replace(tx);
|
||||
} else {
|
||||
eprintln!("Error locking SELECT_ROM_TX");
|
||||
return None;
|
||||
}
|
||||
rx.await.unwrap_or(None)
|
||||
} else {
|
||||
eprintln!("Android app not initialized");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn select_gb_rom(_player: i32) -> Option<std::path::PathBuf> {
|
||||
select_rom(slint::SharedString::new()).await
|
||||
}
|
||||
|
||||
pub async fn select_gb_ram(_player: i32) -> Option<std::path::PathBuf> {
|
||||
select_rom(slint::SharedString::new()).await
|
||||
}
|
||||
|
||||
fn select_rom_on_jvm(env: &mut Env<'_>, rom_dir: String) -> jni::errors::Result<()> {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
|
||||
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? };
|
||||
|
||||
let action = AndroidIntent::ACTION_OPEN_DOCUMENT(env)?;
|
||||
let category = AndroidIntent::CATEGORY_OPENABLE(env)?;
|
||||
let mime_type = JString::from_str(env, "*/*")?;
|
||||
let mut intent = AndroidIntent::with_action(env, &action)?
|
||||
.set_type(env, &mime_type)?
|
||||
.add_category(env, &category)?;
|
||||
if !rom_dir.is_empty() {
|
||||
let start_dir = JString::from_str(env, rom_dir)?;
|
||||
let extra_initial_uri = DocumentsContract::EXTRA_INITIAL_URI(env)?;
|
||||
intent = intent.put_extra_string(env, &extra_initial_uri, &start_dir)?;
|
||||
}
|
||||
|
||||
activity.start_activity_for_result(env, &intent, REQUEST_SELECT_ROM)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(jni::errors::Error::UninitializedJavaVM)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_dirs() -> ui::Dirs {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
ui::Dirs {
|
||||
config_dir: app.internal_data_path().unwrap().join("config"),
|
||||
data_dir: app.external_data_path().unwrap().join("data"),
|
||||
cache_dir: app.internal_data_path().unwrap().join("cache"),
|
||||
}
|
||||
} else {
|
||||
panic!("Android app not initialized");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_controller_names() -> Vec<String> {
|
||||
let mut controllers: Vec<String> = list_controllers().into_iter().map(|c| c.name).collect();
|
||||
controllers.insert(0, "None".into());
|
||||
controllers
|
||||
}
|
||||
|
||||
pub fn get_controller_paths() -> Vec<String> {
|
||||
let mut controller_paths: Vec<String> = list_controllers()
|
||||
.into_iter()
|
||||
.map(|c| c.descriptor)
|
||||
.collect();
|
||||
controller_paths.insert(0, String::new());
|
||||
controller_paths
|
||||
}
|
||||
|
||||
pub fn rom_exists(path: &str) -> bool {
|
||||
get_file_from_uri(&std::path::PathBuf::from(path)).is_some()
|
||||
}
|
||||
|
||||
pub fn get_file_from_uri(path: &std::path::PathBuf) -> Option<std::fs::File> {
|
||||
if let Some(vm) = get_vm() {
|
||||
let path = path.to_str().unwrap().into();
|
||||
match vm.attach_current_thread(|env| get_file_from_uri_on_jvm(env, path)) {
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
eprintln!("JNI error while opening URI: {err:?}");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Android app not initialized");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_file_from_uri_on_jvm(
|
||||
env: &mut Env<'_>,
|
||||
path: String,
|
||||
) -> jni::errors::Result<Option<std::fs::File>> {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
|
||||
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? };
|
||||
let path = JString::from_str(env, path)?;
|
||||
let mode = JString::from_str(env, "r")?;
|
||||
let uri = AndroidUri::parse(env, &path)?;
|
||||
|
||||
let content_resolver = activity.as_ref().get_content_resolver(env)?;
|
||||
let parcel_file_descriptor = content_resolver.open_file_descriptor(env, &uri, &mode);
|
||||
if let Ok(descriptor) = parcel_file_descriptor
|
||||
&& !descriptor.is_null()
|
||||
{
|
||||
let owned_fd = unsafe { std::os::fd::OwnedFd::from_raw_fd(descriptor.detach_fd(env)?) };
|
||||
let file = std::fs::File::from(owned_fd);
|
||||
descriptor.close(env)?;
|
||||
return Ok(Some(file));
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
} else {
|
||||
eprintln!("Android app not initialized");
|
||||
return Err(jni::errors::Error::UninitializedJavaVM);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_io_github_gopher64_gopher64_SlintActivity_nativeOnActivityResult<
|
||||
'caller,
|
||||
>(
|
||||
mut unowned_env: EnvUnowned<'caller>,
|
||||
_class: JClass<'caller>,
|
||||
request_code: jint,
|
||||
result_code: jint,
|
||||
intent_data: JObject<'caller>,
|
||||
) {
|
||||
let outcome = unowned_env.with_env(|env| -> Result<_, jni::errors::Error> {
|
||||
if result_code == AndroidActivity::RESULT_OK(env)? {
|
||||
match request_code {
|
||||
REQUEST_SELECT_ROM => {
|
||||
if let Ok(mut tx_lock) = SELECT_ROM_TX.lock()
|
||||
&& let Some(tx) = tx_lock.take()
|
||||
&& !intent_data.is_null()
|
||||
{
|
||||
let result_intent =
|
||||
unsafe { env.as_cast_raw::<AndroidIntent>(&intent_data)? };
|
||||
|
||||
let uri = result_intent.as_ref().get_data(env)?;
|
||||
if uri.is_null() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
|
||||
let activity = unsafe {
|
||||
env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)?
|
||||
};
|
||||
|
||||
let content_resolver = activity.as_ref().get_content_resolver(env)?;
|
||||
let take_flags = AndroidIntent::FLAG_GRANT_READ_URI_PERMISSION(env)?;
|
||||
content_resolver
|
||||
.take_persistable_uri_permission(env, &uri, take_flags)?;
|
||||
|
||||
let path = uri.to_string(env)?;
|
||||
|
||||
let _ = tx.send(Some(std::path::PathBuf::from(path.to_string())));
|
||||
} else {
|
||||
eprintln!("Android app not initialized");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
CONFIGURE_INPUT_PROFILE => {
|
||||
if let Ok(weak_app_window) = WEAK_SLINT_WINDOW.lock()
|
||||
&& let Some(weak_app_window) = weak_app_window.as_ref()
|
||||
{
|
||||
let config = ui::config::Config::new();
|
||||
ui::gui::update_input_profiles(&weak_app_window, &config);
|
||||
}
|
||||
}
|
||||
RUN_ROM => {
|
||||
let result_intent = unsafe { env.as_cast_raw::<AndroidIntent>(&intent_data)? };
|
||||
|
||||
let file_path_key = JString::from_str(env, "file_path")?;
|
||||
let file_path = result_intent
|
||||
.as_ref()
|
||||
.get_string_extra(env, &file_path_key)?
|
||||
.try_to_string(env)?;
|
||||
|
||||
let cheats_path_key = JString::from_str(env, "cheats_path")?;
|
||||
if let Ok(cheats_path) = result_intent
|
||||
.as_ref()
|
||||
.get_string_extra(env, &cheats_path_key)
|
||||
&& let Ok(cheats_path) = cheats_path.try_to_string(env)
|
||||
{
|
||||
let _ = std::fs::remove_file(cheats_path);
|
||||
}
|
||||
if let Ok(weak_app_window) = WEAK_SLINT_WINDOW.lock()
|
||||
&& let Some(weak_app_window) = weak_app_window.as_ref()
|
||||
{
|
||||
weak_app_window
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
ui::gui::update_recent_roms(&handle, file_path.into());
|
||||
handle.set_game_running(false)
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
outcome.resolve::<jni::errors::ThrowRuntimeExAndDefault>()
|
||||
}
|
||||
@@ -1,19 +1,40 @@
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
|
||||
pub fn init(ui: &mut ui::Ui, frequency: u64) {
|
||||
static ADJUST_LOCKED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
ui::sdl_init(sdl3_sys::init::SDL_INIT_AUDIO);
|
||||
init_game_audio(ui, frequency);
|
||||
init_game_audio(device);
|
||||
}
|
||||
|
||||
pub fn init_game_audio(ui: &mut ui::Ui, frequency: u64) {
|
||||
unsafe extern "C" fn audio_callback(
|
||||
_userdata: *mut std::ffi::c_void,
|
||||
audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
|
||||
additional_amount: i32,
|
||||
_total_amount: i32,
|
||||
) {
|
||||
if additional_amount > 0 {
|
||||
unsafe {
|
||||
sdl3_sys::audio::SDL_PutAudioStreamData(
|
||||
audio_stream,
|
||||
vec![0; additional_amount as usize].as_ptr() as *const std::ffi::c_void,
|
||||
additional_amount,
|
||||
)
|
||||
};
|
||||
|
||||
adjust_audio_frequency(audio_stream, -0.0005);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_game_audio(device: &mut device::Device) {
|
||||
let game_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
|
||||
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
|
||||
freq: frequency as i32,
|
||||
freq: device.ai.freq as i32,
|
||||
channels: 2,
|
||||
};
|
||||
|
||||
ui.audio.audio_stream = unsafe {
|
||||
device.ui.audio.audio_stream = unsafe {
|
||||
sdl3_sys::audio::SDL_OpenAudioDeviceStream(
|
||||
sdl3_sys::audio::SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
|
||||
&game_audio_spec,
|
||||
@@ -21,14 +42,23 @@ pub fn init_game_audio(ui: &mut ui::Ui, frequency: u64) {
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ui.audio.audio_stream.is_null() {
|
||||
if device.ui.audio.audio_stream.is_null() {
|
||||
panic!("Could not create audio stream");
|
||||
}
|
||||
if !unsafe {
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain)
|
||||
&& sdl3_sys::audio::SDL_ResumeAudioStreamDevice(ui.audio.audio_stream)
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(device.ui.audio.audio_stream, device.ui.audio.gain)
|
||||
&& sdl3_sys::audio::SDL_SetAudioStreamGetCallback(
|
||||
device.ui.audio.audio_stream,
|
||||
Some(audio_callback),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
} {
|
||||
panic!("Could not resume audio stream");
|
||||
panic!("Could not initialize audio stream");
|
||||
}
|
||||
if device.ai.regs[device::ai::AI_STATUS_REG] & device::ai::AI_STATUS_BUSY != 0 {
|
||||
resume_game_audio(&mut device.ui);
|
||||
} else {
|
||||
pause_game_audio(&mut device.ui);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +66,18 @@ pub fn close(ui: &mut ui::Ui) {
|
||||
close_game_audio(ui);
|
||||
}
|
||||
|
||||
pub fn resume_game_audio(ui: &mut ui::Ui) {
|
||||
unsafe {
|
||||
sdl3_sys::audio::SDL_ResumeAudioStreamDevice(ui.audio.audio_stream);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause_game_audio(ui: &mut ui::Ui) {
|
||||
unsafe {
|
||||
sdl3_sys::audio::SDL_PauseAudioStreamDevice(ui.audio.audio_stream);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_game_audio(ui: &mut ui::Ui) {
|
||||
unsafe {
|
||||
if !ui.audio.audio_stream.is_null() {
|
||||
@@ -73,22 +115,34 @@ pub fn raise_audio_volume(ui: &mut ui::Ui) {
|
||||
);
|
||||
}
|
||||
|
||||
fn adjust_audio_frequency(device: &device::Device, frequency: f32) {
|
||||
if !device.vi.enable_speed_limiter {
|
||||
unsafe extern "C" fn lock_callback(
|
||||
_userdata: *mut std::ffi::c_void,
|
||||
_id: sdl3_sys::timer::SDL_TimerID,
|
||||
_interval: u32,
|
||||
) -> u32 {
|
||||
ADJUST_LOCKED.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
0
|
||||
}
|
||||
|
||||
fn adjust_audio_frequency(audio_stream: *mut sdl3_sys::audio::SDL_AudioStream, frequency: f32) {
|
||||
if ADJUST_LOCKED.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
ADJUST_LOCKED.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
unsafe {
|
||||
let current_ratio =
|
||||
sdl3_sys::everything::SDL_GetAudioStreamFrequencyRatio(device.ui.audio.audio_stream);
|
||||
sdl3_sys::timer::SDL_AddTimer(1000, Some(lock_callback), std::ptr::null_mut());
|
||||
let current_ratio = sdl3_sys::everything::SDL_GetAudioStreamFrequencyRatio(audio_stream);
|
||||
sdl3_sys::everything::SDL_SetAudioStreamFrequencyRatio(
|
||||
device.ui.audio.audio_stream,
|
||||
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)
|
||||
sdl3_sys::everything::SDL_GetAudioStreamFrequencyRatio(audio_stream)
|
||||
);
|
||||
*/
|
||||
}
|
||||
@@ -116,44 +170,15 @@ pub fn play_audio(device: &device::Device, dram_addr: usize, length: u64) {
|
||||
unsafe { sdl3_sys::audio::SDL_GetAudioStreamQueued(device.ui.audio.audio_stream) } as f64;
|
||||
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 < 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 {
|
||||
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");
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
println!(
|
||||
"Audio overrun: queued {} samples, expected at most {} samples",
|
||||
audio_queued, max_latency
|
||||
);
|
||||
*/
|
||||
adjust_audio_frequency(device, 0.0005);
|
||||
};
|
||||
} else if device.vi.enable_speed_limiter {
|
||||
adjust_audio_frequency(device.ui.audio.audio_stream, 0.0005);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,16 @@
|
||||
use crate::cheats;
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
use crate::ui::gui::AppWindow;
|
||||
use crate::ui::gui::ErrorDialog;
|
||||
use crate::{cheats, device};
|
||||
use slint::ComponentHandle;
|
||||
|
||||
pub fn cheats_window(app: &AppWindow) {
|
||||
let weak = app.as_weak();
|
||||
app.on_cheats_select_rom_clicked(move |rom_dir| {
|
||||
let select_rom = if !rom_dir.is_empty()
|
||||
&& let Ok(exists) = std::fs::exists(&rom_dir)
|
||||
&& exists
|
||||
{
|
||||
rfd::AsyncFileDialog::new().set_directory(rom_dir)
|
||||
} else {
|
||||
rfd::AsyncFileDialog::new()
|
||||
}
|
||||
.set_title("Select ROM")
|
||||
.add_filter("ROM files", &ui::gui::N64_EXTENSIONS)
|
||||
.pick_file();
|
||||
let select_rom = ui::gui::select_rom(rom_dir);
|
||||
let weak = weak.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Some(file) = select_rom.await
|
||||
&& let Some(rom_contents) = device::get_rom_contents(file.path())
|
||||
&& let Some(rom_contents) = device::get_rom_contents(&file)
|
||||
{
|
||||
let game_crc = ui::storage::get_game_crc(&rom_contents);
|
||||
let game_name = ui::storage::get_game_name(&rom_contents);
|
||||
@@ -77,14 +65,8 @@ pub fn cheats_window(app: &AppWindow) {
|
||||
}
|
||||
} else {
|
||||
clear_cheats(&weak, true);
|
||||
weak.upgrade_in_event_loop(move |_handle| {
|
||||
let message_dialog = ErrorDialog::new().unwrap();
|
||||
let weak_dialog = message_dialog.as_weak();
|
||||
message_dialog.on_close_clicked(move || {
|
||||
weak_dialog.unwrap().window().hide().unwrap();
|
||||
});
|
||||
message_dialog.set_text("Could not read ROM".into());
|
||||
message_dialog.show().unwrap();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.invoke_show_message("Could not read ROM".into(), true);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ pub struct InputControllerAxis {
|
||||
pub enabled: bool,
|
||||
pub id: i32,
|
||||
pub axis: i16,
|
||||
pub initial_state: i16,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
@@ -47,6 +48,7 @@ pub struct Input {
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Video {
|
||||
pub upscale: u32,
|
||||
pub ssaa: bool,
|
||||
pub integer_scaling: bool,
|
||||
pub fullscreen: bool,
|
||||
pub widescreen: bool,
|
||||
@@ -85,8 +87,11 @@ impl Drop for Cheats {
|
||||
fn write_cheats(cheats: &Cheats) {
|
||||
let dirs = ui::get_dirs();
|
||||
let file_path = dirs.config_dir.join("cheats.json");
|
||||
let f = std::fs::File::create(file_path).unwrap();
|
||||
serde_json::to_writer_pretty(f, &cheats).unwrap();
|
||||
if let Ok(f) = std::fs::File::create(file_path)
|
||||
&& let Err(e) = serde_json::to_writer_pretty(f, &cheats)
|
||||
{
|
||||
eprintln!("Error writing cheats: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
impl Cheats {
|
||||
@@ -115,8 +120,11 @@ impl Drop for Config {
|
||||
fn write_config(config: &Config) {
|
||||
let dirs = ui::get_dirs();
|
||||
let file_path = dirs.config_dir.join("config.json");
|
||||
let f = std::fs::File::create(file_path).unwrap();
|
||||
serde_json::to_writer_pretty(f, &config).unwrap();
|
||||
if let Ok(f) = std::fs::File::create(file_path)
|
||||
&& let Err(e) = serde_json::to_writer_pretty(f, &config)
|
||||
{
|
||||
eprintln!("Error writing config: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -150,6 +158,7 @@ impl Config {
|
||||
},
|
||||
video: Video {
|
||||
upscale: 1,
|
||||
ssaa: false,
|
||||
integer_scaling: false,
|
||||
fullscreen: false,
|
||||
widescreen: false,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use crate::retroachievements;
|
||||
use crate::ui;
|
||||
#[cfg(target_os = "android")]
|
||||
use crate::ui::android;
|
||||
use slint::Model;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use slint::winit_030::WinitWindowAccessor;
|
||||
|
||||
slint::include_modules!();
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub const N64_EXTENSIONS: [&str; 12] = [
|
||||
"n64", "v64", "z64", "7z", "zip", "bin", "N64", "V64", "Z64", "7Z", "ZIP", "BIN",
|
||||
];
|
||||
@@ -19,14 +23,6 @@ pub struct NetplayDevice {
|
||||
pub player_number: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RASettings {
|
||||
pub enabled: bool,
|
||||
pub hardcore: bool,
|
||||
pub challenge: bool,
|
||||
pub leaderboard: bool,
|
||||
}
|
||||
|
||||
fn check_latest_version(weak: slint::Weak<AppWindow>) {
|
||||
let task = ui::WEB_CLIENT
|
||||
.get("https://api.github.com/repos/gopher64/gopher64/releases/latest")
|
||||
@@ -36,9 +32,12 @@ fn check_latest_version(weak: slint::Weak<AppWindow>) {
|
||||
if let Ok(response) = response {
|
||||
let data: Result<GithubData, reqwest::Error> = response.json().await;
|
||||
|
||||
let latest_version = if let Ok(data) = data {
|
||||
semver::Version::parse(&data.tag_name[1..]).unwrap()
|
||||
let latest_version = if let Ok(data) = data
|
||||
&& let Ok(github_version) = semver::Version::parse(&data.tag_name[1..])
|
||||
{
|
||||
github_version
|
||||
} else {
|
||||
eprintln!("Error getting latest version from GitHub");
|
||||
semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap()
|
||||
};
|
||||
let current_version = semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
|
||||
@@ -50,18 +49,23 @@ fn check_latest_version(weak: slint::Weak<AppWindow>) {
|
||||
});
|
||||
}
|
||||
|
||||
fn run_with_path(
|
||||
weak: slint::Weak<AppWindow>,
|
||||
path: std::path::PathBuf,
|
||||
controller_paths: &[Option<String>],
|
||||
) {
|
||||
let controller_paths = controller_paths.to_owned();
|
||||
pub fn open_uri(path: impl AsRef<std::ffi::OsStr>) {
|
||||
#[cfg(target_os = "android")]
|
||||
return ui::android::open_uri(path.as_ref().to_str().unwrap());
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
if let Err(e) = open::that_detached(path) {
|
||||
eprintln!("Error opening path: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_with_path(weak: slint::Weak<AppWindow>, path: std::path::PathBuf) {
|
||||
let weak2 = weak.clone();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
if handle.get_game_running() {
|
||||
return;
|
||||
}
|
||||
save_settings(&handle, &controller_paths);
|
||||
save_settings(&handle);
|
||||
|
||||
run_rom(
|
||||
path,
|
||||
@@ -72,55 +76,42 @@ fn run_with_path(
|
||||
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>]) {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn file_dropped(app: &AppWindow) {
|
||||
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);
|
||||
run_with_path(weak.clone(), path.to_path_buf());
|
||||
}
|
||||
slint::winit_030::EventResult::Propagate
|
||||
});
|
||||
}
|
||||
|
||||
fn local_game_window(
|
||||
app: &AppWindow,
|
||||
config: &ui::config::Config,
|
||||
controller_paths: &[Option<String>],
|
||||
) {
|
||||
let dirs = ui::get_dirs();
|
||||
fn rom_exists(path: &str) -> bool {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
return std::fs::exists(path).unwrap_or(false);
|
||||
#[cfg(target_os = "android")]
|
||||
return android::rom_exists(path);
|
||||
}
|
||||
|
||||
fn local_game_window(app: &AppWindow, config: &ui::config::Config) {
|
||||
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
|
||||
}
|
||||
})
|
||||
.filter(|x| rom_exists(x))
|
||||
.map(|x| {
|
||||
(
|
||||
x.into(),
|
||||
std::path::Path::new(x)
|
||||
std::path::Path::new(&decode_path(x))
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
@@ -133,35 +124,31 @@ fn local_game_window(
|
||||
)));
|
||||
|
||||
let weak = app.as_weak();
|
||||
let owned_controller_paths = controller_paths.to_owned();
|
||||
app.on_open_rom_button_clicked(move || {
|
||||
let controller_paths = owned_controller_paths.clone();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
save_settings(&handle, &controller_paths);
|
||||
save_settings(&handle);
|
||||
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,
|
||||
);
|
||||
run_with_path(handle.as_weak(), std::path::PathBuf::from(rom.to_string()));
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let saves_path = dirs.data_dir.join("saves");
|
||||
app.on_saves_folder_button_clicked(move || {
|
||||
open::that_detached(saves_path.clone()).unwrap();
|
||||
});
|
||||
file_dropped(app, controller_paths);
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let saves_path = ui::get_dirs().data_dir.join("saves");
|
||||
app.on_saves_folder_button_clicked(move || {
|
||||
open_uri(&saves_path);
|
||||
});
|
||||
|
||||
file_dropped(app);
|
||||
}
|
||||
}
|
||||
|
||||
fn input_profiles(config: &ui::config::Config) -> Vec<String> {
|
||||
@@ -169,11 +156,18 @@ fn input_profiles(config: &ui::config::Config) -> Vec<String> {
|
||||
for key in config.input.input_profiles.keys() {
|
||||
profiles.push(key.clone())
|
||||
}
|
||||
|
||||
// make sure default profile is always first
|
||||
if let Some(pos) = profiles.iter().position(|x| x == "default") {
|
||||
let default_profile = profiles.remove(pos);
|
||||
profiles.insert(0, default_profile);
|
||||
}
|
||||
profiles
|
||||
}
|
||||
|
||||
fn settings_window(app: &AppWindow, config: &ui::config::Config) {
|
||||
app.set_integer_scaling(config.video.integer_scaling);
|
||||
app.set_ssaa(config.video.ssaa);
|
||||
app.set_fullscreen(config.video.fullscreen);
|
||||
app.set_widescreen(config.video.widescreen);
|
||||
app.set_vsync(config.video.vsync);
|
||||
@@ -195,10 +189,9 @@ fn settings_window(app: &AppWindow, config: &ui::config::Config) {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_input_profiles(weak: &slint::Weak<AppWindow>, config: &ui::config::Config) {
|
||||
pub fn update_input_profiles(weak: &slint::Weak<AppWindow>, config: &ui::config::Config) {
|
||||
let profiles = input_profiles(config);
|
||||
let config_bindings = config.input.input_profile_binding.clone();
|
||||
let weak2 = weak.clone();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
let profile_bindings = slint::VecModel::default();
|
||||
for (i, input_profile_binding) in handle.get_selected_profile_binding().iter().enumerate() {
|
||||
@@ -210,7 +203,7 @@ fn update_input_profiles(weak: &slint::Weak<AppWindow>, config: &ui::config::Con
|
||||
let position = profiles
|
||||
.iter()
|
||||
.position(|profile| *profile == currently_selected);
|
||||
profile_bindings.push(position.unwrap() as i32);
|
||||
profile_bindings.push(position.unwrap_or(0) as i32);
|
||||
}
|
||||
|
||||
handle.set_input_profiles(slint::ModelRc::from(std::rc::Rc::new(
|
||||
@@ -224,26 +217,26 @@ fn update_input_profiles(weak: &slint::Weak<AppWindow>, config: &ui::config::Con
|
||||
|
||||
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);
|
||||
slint::Timer::single_shot(std::time::Duration::from_millis(200), move || {
|
||||
weak2
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_blank_profiles(false);
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn controller_window(
|
||||
app: &AppWindow,
|
||||
config: &ui::config::Config,
|
||||
controller_names: &[String],
|
||||
controller_paths: &[Option<String>],
|
||||
) {
|
||||
fn clear_gb_paths(weak: &slint::Weak<AppWindow>, player: i32) {
|
||||
weak.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();
|
||||
}
|
||||
|
||||
fn controller_window(app: &AppWindow, config: &ui::config::Config) {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
ui::sdl_init(sdl3_sys::init::SDL_INIT_GAMEPAD);
|
||||
|
||||
app.set_emulate_vru(config.input.emulate_vru);
|
||||
|
||||
app.set_controller_enabled(slint::ModelRc::from(std::rc::Rc::new(
|
||||
@@ -278,126 +271,174 @@ fn controller_window(
|
||||
|
||||
update_input_profiles(&app.as_weak(), config);
|
||||
|
||||
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>>(),
|
||||
),
|
||||
app.set_controller_changed(slint::ModelRc::from(std::rc::Rc::new(
|
||||
slint::VecModel::from(vec![false, false, false, false]),
|
||||
)));
|
||||
|
||||
let controller_changed = slint::VecModel::default();
|
||||
let selected_controllers = slint::VecModel::default();
|
||||
for selected in config.input.controller_assignment.iter() {
|
||||
let selected_index = controller_paths
|
||||
.iter()
|
||||
.position(|path| selected == path)
|
||||
.unwrap_or(0) as i32;
|
||||
selected_controllers.push(selected_index);
|
||||
controller_changed.push(false);
|
||||
}
|
||||
app.set_selected_controller(slint::ModelRc::from(std::rc::Rc::new(selected_controllers)));
|
||||
let config_controller_assignment = config.input.controller_assignment.clone();
|
||||
let weak_app = app.as_weak();
|
||||
app.on_controller_window_created(move || {
|
||||
let controller_assignment = config_controller_assignment.clone();
|
||||
weak_app
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
let mut current_selected_paths = vec![None; 4];
|
||||
for (i, selected_controller) in handle.get_selected_controller().iter().enumerate()
|
||||
{
|
||||
current_selected_paths[i] = handle
|
||||
.get_controller_paths()
|
||||
.row_data(selected_controller as usize);
|
||||
}
|
||||
|
||||
app.set_controller_changed(slint::ModelRc::from(std::rc::Rc::new(controller_changed)));
|
||||
let controller_names = ui::input::get_controller_names();
|
||||
handle.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_paths = ui::input::get_controller_paths();
|
||||
handle.set_controller_paths(slint::ModelRc::from(std::rc::Rc::new(
|
||||
slint::VecModel::from(
|
||||
controller_paths
|
||||
.iter()
|
||||
.map(|x| x.into())
|
||||
.collect::<Vec<slint::SharedString>>(),
|
||||
),
|
||||
)));
|
||||
|
||||
let selected_controllers = slint::VecModel::default();
|
||||
for i in 0..4 {
|
||||
let assigned_path =
|
||||
if let Some(current_selected_path) = ¤t_selected_paths[i] {
|
||||
current_selected_path.to_string()
|
||||
} else if let Some(config_assigned_path) = &controller_assignment[i] {
|
||||
config_assigned_path.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let selected_index = controller_paths
|
||||
.iter()
|
||||
.position(|controller_path| assigned_path == *controller_path)
|
||||
.unwrap_or(0) as i32;
|
||||
selected_controllers.push(selected_index);
|
||||
}
|
||||
handle.set_selected_controller(slint::ModelRc::from(std::rc::Rc::new(
|
||||
selected_controllers,
|
||||
)));
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let weak_app = app.as_weak();
|
||||
app.on_input_profile_button_clicked(move || {
|
||||
let dialog = InputProfileDialog::new().unwrap();
|
||||
dialog.set_deadzone(ui::input::DEADZONE_DEFAULT);
|
||||
let weak_dialog = dialog.as_weak();
|
||||
let weak_app = weak_app.clone();
|
||||
dialog.on_profile_creation_button_clicked(move || {
|
||||
let weak_app = weak_app.clone();
|
||||
weak_dialog
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
handle.hide().unwrap();
|
||||
let profile_name = handle.get_profile_name();
|
||||
let dinput = handle.get_dinput();
|
||||
let deadzone = handle.get_deadzone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
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();
|
||||
weak_app
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_input_deadzone(ui::input::DEADZONE_DEFAULT);
|
||||
handle.set_input_profile_name(String::new().into());
|
||||
handle.set_input_dinput(false);
|
||||
handle.set_show_input_profile(true);
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
let weak_app = app.as_weak();
|
||||
app.on_input_profile_creation_button_clicked(move || {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let weak_app2 = weak_app.clone();
|
||||
weak_app
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
let profile_name = handle.get_input_profile_name();
|
||||
let dinput = handle.get_input_dinput();
|
||||
let deadzone = handle.get_input_deadzone();
|
||||
handle.set_show_input_profile(false);
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
ui::android::spawn_configure_input_profile(profile_name, dinput, deadzone);
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
tokio::spawn(async move {
|
||||
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_app2, &config);
|
||||
});
|
||||
})
|
||||
.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 select_gb_rom = select_gb_rom(player);
|
||||
|
||||
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) {
|
||||
if let Some(gb_rom) = select_gb_rom.await {
|
||||
let weak_app4 = weak_app3.clone();
|
||||
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);
|
||||
.upgrade_in_event_loop(move |_handle| {
|
||||
let select_gb_ram = select_gb_ram(player);
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Some(gb_ram) = select_gb_ram.await {
|
||||
weak_app4
|
||||
.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.to_str().unwrap().into(),
|
||||
);
|
||||
ram_paths.set_row_data(
|
||||
player as usize,
|
||||
gb_ram.to_str().unwrap().into(),
|
||||
);
|
||||
handle.set_gb_rom_paths(rom_paths);
|
||||
handle.set_gb_ram_paths(ram_paths);
|
||||
})
|
||||
.unwrap();
|
||||
} else {
|
||||
clear_gb_paths(&weak_app4, player);
|
||||
}
|
||||
});
|
||||
})
|
||||
.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();
|
||||
clear_gb_paths(&weak_app3, player);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
|
||||
pub fn save_settings(app: &AppWindow) {
|
||||
let mut config = ui::config::Config::new();
|
||||
config.rom_dir = app.get_rom_dir().to_string().into();
|
||||
config.video.integer_scaling = app.get_integer_scaling();
|
||||
config.video.ssaa = app.get_ssaa();
|
||||
config.video.fullscreen = app.get_fullscreen();
|
||||
config.video.widescreen = app.get_widescreen();
|
||||
config.video.vsync = app.get_vsync();
|
||||
@@ -427,65 +468,85 @@ pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
|
||||
|
||||
for (i, selected_controller) in app.get_selected_controller().iter().enumerate() {
|
||||
if app.get_controller_changed().row_data(i).unwrap_or(false) {
|
||||
config.input.controller_assignment[i] =
|
||||
controller_paths[selected_controller as usize].clone();
|
||||
let controller_path = app
|
||||
.get_controller_paths()
|
||||
.row_data(selected_controller as usize)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
if controller_path.is_empty() {
|
||||
config.input.controller_assignment[i] = None;
|
||||
} else {
|
||||
config.input.controller_assignment[i] = Some(controller_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn about_window(app: &AppWindow) {
|
||||
app.on_wiki_button_clicked(move || {
|
||||
open::that_detached("https://github.com/gopher64/gopher64/wiki").unwrap();
|
||||
open_uri("https://github.com/gopher64/gopher64/wiki");
|
||||
});
|
||||
app.on_discord_button_clicked(move || {
|
||||
open::that_detached("https://discord.gg/9RGXq8W8JQ").unwrap();
|
||||
open_uri("https://discord.gg/9RGXq8W8JQ");
|
||||
});
|
||||
app.on_patreon_button_clicked(move || {
|
||||
open::that_detached("https://patreon.com/loganmc10").unwrap();
|
||||
open_uri("https://patreon.com/loganmc10");
|
||||
});
|
||||
app.on_github_sponsors_button_clicked(move || {
|
||||
open::that_detached("https://github.com/sponsors/loganmc10").unwrap();
|
||||
open_uri("https://github.com/sponsors/loganmc10");
|
||||
});
|
||||
app.on_source_code_button_clicked(move || {
|
||||
open::that_detached("https://github.com/gopher64/gopher64").unwrap();
|
||||
open_uri("https://github.com/gopher64/gopher64");
|
||||
});
|
||||
app.on_newversion_button_clicked(move || {
|
||||
open::that_detached("https://github.com/gopher64/gopher64/releases/latest").unwrap();
|
||||
open_uri("https://github.com/gopher64/gopher64/releases/latest");
|
||||
});
|
||||
app.set_version(format!("Version: {}", env!("GIT_DESCRIBE")).into());
|
||||
if std::env::var("FLATPAK_ID").is_err() {
|
||||
|
||||
//flatpak and itch.io have their own update checking mechanism
|
||||
if std::env::var("FLATPAK_ID").is_err() && std::env::var("ITCHIO_APP").is_err() {
|
||||
check_latest_version(app.as_weak());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn app_window() {
|
||||
let app = AppWindow::new().unwrap();
|
||||
about_window(&app);
|
||||
ui::retroachievements::ra_window(&app);
|
||||
let mut controller_paths;
|
||||
pub fn app_window(app: &AppWindow, is_android: bool) {
|
||||
retroachievements::init_client(false, false, false);
|
||||
app.set_is_android(is_android);
|
||||
about_window(app);
|
||||
ui::retroachievements::ra_window(app);
|
||||
{
|
||||
let game_ui = ui::Ui::new();
|
||||
let mut controller_names = ui::input::get_controller_names(&game_ui);
|
||||
controller_names.insert(0, "None".into());
|
||||
controller_paths = ui::input::get_controller_paths(&game_ui);
|
||||
controller_paths.insert(0, None);
|
||||
settings_window(&app, &game_ui.config);
|
||||
controller_window(&app, &game_ui.config, &controller_names, &controller_paths);
|
||||
local_game_window(&app, &game_ui.config, &controller_paths);
|
||||
let config = ui::config::Config::new();
|
||||
settings_window(app, &config);
|
||||
controller_window(app, &config);
|
||||
local_game_window(app, &config);
|
||||
}
|
||||
ui::netplay::netplay_window(app);
|
||||
ui::cheats::cheats_window(app);
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let weak_app = app.as_weak();
|
||||
app.window().on_close_requested(move || {
|
||||
weak_app
|
||||
.upgrade_in_event_loop(move |handle| save_settings(&handle))
|
||||
.unwrap();
|
||||
slint::CloseRequestResponse::HideWindow
|
||||
});
|
||||
}
|
||||
ui::netplay::netplay_window(&app, &controller_paths);
|
||||
ui::cheats::cheats_window(&app);
|
||||
app.run().unwrap();
|
||||
save_settings(&app, &controller_paths);
|
||||
retroachievements::shutdown_client();
|
||||
}
|
||||
|
||||
pub fn run_rom(
|
||||
file_path: std::path::PathBuf,
|
||||
game_settings: ui::GameSettings,
|
||||
netplay: Option<NetplayDevice>,
|
||||
ra_settings: RASettings,
|
||||
weak: slint::Weak<AppWindow>,
|
||||
) {
|
||||
#[cfg(target_os = "android")]
|
||||
ui::android::run_rom(file_path, game_settings, netplay, weak);
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
tokio::spawn(async move {
|
||||
weak.upgrade_in_event_loop(move |handle| handle.set_game_running(true))
|
||||
.unwrap();
|
||||
@@ -509,7 +570,7 @@ pub fn run_rom(
|
||||
]);
|
||||
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();
|
||||
let f = std::fs::File::create(&cheats_path).unwrap();
|
||||
serde_json::to_writer_pretty(f, &game_settings.cheats).unwrap();
|
||||
|
||||
command.args([
|
||||
@@ -521,23 +582,6 @@ pub fn run_rom(
|
||||
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())
|
||||
@@ -550,29 +594,14 @@ pub fn run_rom(
|
||||
eprintln!("Failed to run game");
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_file(cheats_path.to_str().unwrap());
|
||||
let _ = std::fs::remove_file(cheats_path);
|
||||
|
||||
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)));
|
||||
update_recent_roms(&handle, file_path);
|
||||
}
|
||||
handle.set_game_running(false);
|
||||
})
|
||||
@@ -580,32 +609,94 @@ pub fn run_rom(
|
||||
});
|
||||
}
|
||||
|
||||
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)
|
||||
&& exists
|
||||
{
|
||||
rfd::AsyncFileDialog::new().set_directory(rom_dir)
|
||||
} else {
|
||||
rfd::AsyncFileDialog::new()
|
||||
fn decode_path(path: &str) -> String {
|
||||
#[cfg(target_os = "android")]
|
||||
return ui::android::decode_path(path);
|
||||
#[cfg(not(target_os = "android"))]
|
||||
return path.to_string();
|
||||
}
|
||||
|
||||
pub fn update_recent_roms(app: &AppWindow, file_path: std::path::PathBuf) {
|
||||
let recent_roms = slint::VecModel::default();
|
||||
recent_roms.push((
|
||||
file_path.to_str().unwrap().into(),
|
||||
std::path::Path::new(&decode_path(file_path.to_str().unwrap()))
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.into(),
|
||||
));
|
||||
|
||||
for rom in app.get_recent_roms().iter() {
|
||||
if rom.0 != file_path.to_str().unwrap() && recent_roms.row_count() < 5 && rom_exists(&rom.0)
|
||||
{
|
||||
recent_roms.push(rom);
|
||||
}
|
||||
}
|
||||
.set_title("Select ROM")
|
||||
.add_filter("ROM files", &N64_EXTENSIONS)
|
||||
.pick_file();
|
||||
app.set_recent_roms(slint::ModelRc::from(std::rc::Rc::new(recent_roms)));
|
||||
}
|
||||
|
||||
pub async fn select_rom(rom_dir: slint::SharedString) -> Option<std::path::PathBuf> {
|
||||
#[cfg(target_os = "android")]
|
||||
return ui::android::select_rom(rom_dir).await;
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
if !rom_dir.is_empty() && std::fs::exists(&rom_dir).unwrap_or(false) {
|
||||
rfd::AsyncFileDialog::new().set_directory(rom_dir)
|
||||
} else {
|
||||
rfd::AsyncFileDialog::new()
|
||||
}
|
||||
.set_title("Select ROM")
|
||||
.add_filter("ROM files", &N64_EXTENSIONS)
|
||||
.pick_file()
|
||||
.await
|
||||
.map(|file| file.path().to_path_buf())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn select_gb_rom(player: i32) -> Option<std::path::PathBuf> {
|
||||
#[cfg(target_os = "android")]
|
||||
return ui::android::select_gb_rom(player).await;
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
rfd::AsyncFileDialog::new()
|
||||
.set_title(format!("GB ROM P{}", player + 1))
|
||||
.add_filter("GB ROM files", &["gb", "gbc", "GB", "GBC"])
|
||||
.pick_file()
|
||||
.await
|
||||
.map(|file| file.path().to_path_buf())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn select_gb_ram(player: i32) -> Option<std::path::PathBuf> {
|
||||
#[cfg(target_os = "android")]
|
||||
return ui::android::select_gb_ram(player).await;
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
rfd::AsyncFileDialog::new()
|
||||
.set_title(format!("GB RAM P{}", player + 1))
|
||||
.add_filter("GB RAM files", &["sav", "ram", "srm", "SAV", "RAM", "SRM"])
|
||||
.pick_file()
|
||||
.await
|
||||
.map(|file| file.path().to_path_buf())
|
||||
}
|
||||
}
|
||||
|
||||
fn open_rom(app: &AppWindow) {
|
||||
let select_rom = select_rom(app.get_rom_dir());
|
||||
|
||||
let overclock = app.get_overclock_n64_cpu();
|
||||
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();
|
||||
|
||||
let weak = app.as_weak();
|
||||
tokio::spawn(async move {
|
||||
if let Some(file) = select_rom.await {
|
||||
run_rom(
|
||||
file.path().to_path_buf(),
|
||||
file,
|
||||
ui::GameSettings {
|
||||
overclock,
|
||||
disable_expansion_pak,
|
||||
@@ -613,12 +704,6 @@ fn open_rom(app: &AppWindow) {
|
||||
load_savestate_slot: None,
|
||||
},
|
||||
None,
|
||||
RASettings {
|
||||
enabled: ra_enabled,
|
||||
hardcore: ra_hardcore,
|
||||
challenge: ra_challenge,
|
||||
leaderboard: ra_leaderboard,
|
||||
},
|
||||
weak,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { StandardButton } from "std-widgets.slint";
|
||||
import { Palette, StandardButton, VerticalBox, HorizontalBox } from "std-widgets.slint";
|
||||
import { LocalGame, ControllerConfig, Netplay, Cheats, RetroAchievements, Settings, About } from "pages.slint";
|
||||
import { SideBar } from "side_bar.slint";
|
||||
import { SettingsData } from "settings_page.slint";
|
||||
@@ -10,13 +10,12 @@ import { AboutData } from "about_page.slint";
|
||||
import { ControllerData } from "controller_page.slint";
|
||||
import { State } from "localgame_page.slint";
|
||||
import { CheatsData } from "cheat_page.slint";
|
||||
export { InputProfileDialog } from "input_profile.slint";
|
||||
import { InputProfileDialog, InputProfileData } from "input_profile.slint";
|
||||
import { NetplayData } from "netplay_page.slint";
|
||||
export { VruDialog } from "vru_dialog.slint";
|
||||
export { NetplayCreate } from "netplay_create.slint";
|
||||
export { NetplayJoin } from "netplay_join.slint";
|
||||
export { NetplayWait } from "netplay_wait.slint";
|
||||
export { CustomNetplayServer } from "netplay_page.slint";
|
||||
export { DispatcherDialog } from "netplay_create.slint";
|
||||
import { NetplayCreate, CustomServerDialog } from "netplay_create.slint";
|
||||
import { NetplayJoin } from "netplay_join.slint";
|
||||
import { NetplayWait, NetplayWaitData } from "netplay_wait.slint";
|
||||
|
||||
export component AppWindow inherits Window {
|
||||
callback wiki_button_clicked;
|
||||
@@ -29,18 +28,28 @@ export component AppWindow inherits Window {
|
||||
callback open_rom_button_clicked;
|
||||
callback saves_folder_button_clicked;
|
||||
callback input_profile_button_clicked;
|
||||
callback input_profile_creation_button_clicked;
|
||||
callback transferpak_toggled(int,bool);
|
||||
callback create_session_button_clicked;
|
||||
callback join_session_button_clicked;
|
||||
callback netplay_discord_button_clicked;
|
||||
callback netplay_feedback_button_clicked;
|
||||
callback netplay_begin_game;
|
||||
callback netplay_send_chat_message(string);
|
||||
callback netplay_refresh_sessions;
|
||||
callback netplay_create_session(string, string, string, string, string, string, string);
|
||||
callback netplay_get_custom_url;
|
||||
callback netplay_select_rom(string);
|
||||
callback netplay_join_session(string, string, string, string, int);
|
||||
callback netplay_close;
|
||||
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);
|
||||
callback ra_toggled(bool,bool,bool,bool,bool);
|
||||
callback controller_window_created;
|
||||
in property version <=> AboutData.version;
|
||||
in property has_update <=> AboutData.has_update;
|
||||
in-out property ra_username <=> RAData.ra_username;
|
||||
@@ -52,8 +61,11 @@ export component AppWindow inherits Window {
|
||||
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_rich_presence <=> RAData.rich_presence;
|
||||
in-out property ra_softcore_only <=> RAData.ra_softcore_only;
|
||||
out property ra_not_logged_in <=> RAData.not_logged_in;
|
||||
in-out property integer_scaling <=> SettingsData.integer_scaling;
|
||||
in-out property ssaa <=> SettingsData.ssaa;
|
||||
in-out property fullscreen <=> SettingsData.fullscreen;
|
||||
in-out property widescreen <=> SettingsData.widescreen;
|
||||
in-out property vsync <=> SettingsData.vsync;
|
||||
@@ -62,8 +74,8 @@ export component AppWindow inherits Window {
|
||||
in-out property disable_expansion_pak <=> SettingsData.disable_expansion_pak;
|
||||
in-out property resolution <=> SettingsData.resolution;
|
||||
in-out property emulate_usb <=> SettingsData.emulate_usb;
|
||||
in-out property emulate_vru <=> SettingsData.emulate_vru;
|
||||
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;
|
||||
@@ -72,33 +84,113 @@ export component AppWindow inherits Window {
|
||||
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 controller_paths <=> ControllerData.controller_paths;
|
||||
in-out property selected_controller <=> ControllerData.selected_controller;
|
||||
in-out property game_running <=> State.game_running;
|
||||
in-out property is_android <=> State.is_android;
|
||||
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;
|
||||
in-out property cheats <=> CheatsData.cheats;
|
||||
preferred-width: 700px;
|
||||
preferred-height: 500px;
|
||||
title: @tr("Gopher64");
|
||||
|
||||
in-out property input_deadzone <=> InputProfileData.deadzone;
|
||||
in-out property input_profile_name <=> InputProfileData.profile_name;
|
||||
in-out property input_dinput <=> InputProfileData.dinput;
|
||||
in-out property show_input_profile <=> InputProfileData.show_input_profile;
|
||||
|
||||
in-out property show_netplay_wait_room <=> NetplayData.show_wait_room;
|
||||
in-out property show_netplay_create_room <=> NetplayData.show_create_room;
|
||||
in-out property show_netplay_join_room <=> NetplayData.show_join_room;
|
||||
|
||||
in-out property netplay_server_names <=> NetplayData.server_names;
|
||||
in-out property netplay_server_urls <=> NetplayData.server_urls;
|
||||
in-out property netplay_custom_server_url <=> NetplayData.custom_server_url;
|
||||
in-out property netplay_game_name <=> NetplayData.game_name;
|
||||
in-out property netplay_game_hash <=> NetplayData.game_hash;
|
||||
in-out property netplay_game_cheats <=> NetplayData.game_cheats;
|
||||
in-out property netplay_pending_session <=> NetplayData.pending_session;
|
||||
in-out property netplay_rom_path <=> NetplayData.rom_path;
|
||||
in-out property netplay_peer_addr <=> NetplayData.peer_addr;
|
||||
in-out property netplay_room_urls <=> NetplayData.room_urls;
|
||||
in-out property netplay_room_ports <=> NetplayData.room_ports;
|
||||
in-out property netplay_current_session <=> NetplayData.current_session;
|
||||
in-out property netplay_sessions <=> NetplayData.sessions;
|
||||
in-out property netplay_pending_refresh <=> NetplayData.pending_refresh;
|
||||
in-out property netplay_session_name <=> NetplayData.session_name;
|
||||
in-out property netplay_port <=> NetplayWaitData.port;
|
||||
in-out property netplay_ping <=> NetplayWaitData.ping;
|
||||
in-out property netplay_motd <=> NetplayWaitData.motd;
|
||||
in-out property netplay_can_start <=> NetplayWaitData.can_start;
|
||||
in-out property netplay_players <=> NetplayWaitData.players;
|
||||
in-out property netplay_chat_text <=> NetplayWaitData.chat_text;
|
||||
|
||||
private property <string> popup_message;
|
||||
private property <bool> popup_has_close;
|
||||
private property <bool> is-portrait: root.width < root.height;
|
||||
private property <length> bar-thickness: 56px;
|
||||
private property <length> safe-left: root.safe-area-insets.left;
|
||||
private property <length> safe-top: root.safe-area-insets.top;
|
||||
private property <length> safe-right: root.safe-area-insets.right;
|
||||
private property <length> safe-bottom: root.safe-area-insets.bottom;
|
||||
private property <length> safe-width: root.width - root.safe-left - root.safe-right;
|
||||
private property <length> safe-height: root.height - root.safe-top - root.safe-bottom;
|
||||
|
||||
min-width: 320px;
|
||||
min-height: 240px;
|
||||
preferred-width: 800px;
|
||||
preferred-height: 600px;
|
||||
title: "Gopher64";
|
||||
icon: @image-url("../../../data/icon/gopher64.png");
|
||||
|
||||
HorizontalLayout {
|
||||
side-bar := SideBar {
|
||||
title: @tr("Gopher64");
|
||||
model: [
|
||||
@tr("Menu" => "Local Game"),
|
||||
@tr("Menu" => "Controller Config"),
|
||||
@tr("Menu" => "Netplay"),
|
||||
@tr("Menu" => "Cheats"),
|
||||
@tr("Menu" => "RetroAchievements"),
|
||||
@tr("Menu" => "Settings"),
|
||||
@tr("Menu" => "About")
|
||||
];
|
||||
}
|
||||
side-bar := SideBar {
|
||||
portrait: root.is-portrait;
|
||||
title: "Gopher64";
|
||||
model: [
|
||||
{
|
||||
icon: @image-url("../../../data/ui/icons/local_game.svg"),
|
||||
label: @tr("Menu" => "Local Game"),
|
||||
},
|
||||
{
|
||||
icon: @image-url("../../../data/ui/icons/controller.svg"),
|
||||
label: @tr("Menu" => "Controller Config"),
|
||||
},
|
||||
{
|
||||
icon: @image-url("../../../data/ui/icons/netplay.svg"),
|
||||
label: @tr("Menu" => "Netplay"),
|
||||
},
|
||||
{
|
||||
icon: @image-url("../../../data/ui/icons/cheats.svg"),
|
||||
label: @tr("Menu" => "Cheats"),
|
||||
},
|
||||
{
|
||||
icon: @image-url("../../../data/ui/icons/achievements.svg"),
|
||||
label: @tr("Menu" => "RetroAchievements"),
|
||||
},
|
||||
{
|
||||
icon: @image-url("../../../data/ui/icons/settings.svg"),
|
||||
label: @tr("Menu" => "Settings"),
|
||||
},
|
||||
{
|
||||
icon: @image-url("../../../data/ui/icons/about.svg"),
|
||||
label: @tr("Menu" => "About"),
|
||||
},
|
||||
];
|
||||
x: root.safe-left;
|
||||
y: root.is-portrait ? parent.height - root.bar-thickness - root.safe-bottom : root.safe-top;
|
||||
width: root.is-portrait ? root.safe-width : root.bar-thickness;
|
||||
height: root.is-portrait ? root.bar-thickness : root.safe-height;
|
||||
}
|
||||
|
||||
content-area := Rectangle {
|
||||
clip: true;
|
||||
x: root.is-portrait ? root.safe-left : root.safe-left + root.bar-thickness;
|
||||
y: root.safe-top;
|
||||
width: root.is-portrait ? root.safe-width : root.safe-width - root.bar-thickness;
|
||||
height: root.is-portrait ? root.safe-height - root.bar-thickness : root.safe-height;
|
||||
|
||||
if(side-bar.current-item == 0): LocalGame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
open_rom_button_clicked => {
|
||||
open_rom_button_clicked();
|
||||
}
|
||||
@@ -112,15 +204,29 @@ export component AppWindow inherits Window {
|
||||
recent_rom_button_clicked(rom);
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 1): ControllerConfig {
|
||||
if(side-bar.current-item == 1 && !show_input_profile): ControllerConfig {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
input_profile_button_clicked => {
|
||||
input_profile_button_clicked();
|
||||
}
|
||||
transferpak_toggled(player,enabled) => {
|
||||
transferpak_toggled(player,enabled);
|
||||
}
|
||||
controller_window_created => {
|
||||
controller_window_created();
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 2): Netplay {
|
||||
if(side-bar.current-item == 1 && show_input_profile): InputProfileDialog {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
profile_creation_button_clicked => {
|
||||
input_profile_creation_button_clicked();
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 2 && !show_netplay_wait_room && !show_netplay_create_room && !show_netplay_join_room): Netplay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
create_session_button_clicked => {
|
||||
create_session_button_clicked();
|
||||
}
|
||||
@@ -134,7 +240,54 @@ export component AppWindow inherits Window {
|
||||
netplay_feedback_button_clicked();
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 2 && show_netplay_wait_room): NetplayWait {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
send_chat_message(message) => {
|
||||
netplay_send_chat_message(message);
|
||||
}
|
||||
begin_game => {
|
||||
netplay_begin_game();
|
||||
}
|
||||
close => {
|
||||
netplay_close();
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 2 && show_netplay_create_room && !show_netplay_wait_room): NetplayCreate {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
get_custom_url => {
|
||||
netplay_get_custom_url();
|
||||
}
|
||||
select_rom(rom_dir) => {
|
||||
netplay_select_rom(rom_dir);
|
||||
}
|
||||
create_session(a, b, c, d, e, f, g) => {
|
||||
netplay_create_session(a,b,c,d,e,f,g);
|
||||
}
|
||||
close => {
|
||||
netplay_close();
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 2 && show_netplay_join_room && !show_netplay_wait_room): NetplayJoin {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
refresh_sessions => {
|
||||
netplay_refresh_sessions();
|
||||
}
|
||||
select_rom(rom_dir) => {
|
||||
netplay_select_rom(rom_dir);
|
||||
}
|
||||
join_session(a, b, c, d, e) => {
|
||||
netplay_join_session(a,b,c,d,e);
|
||||
}
|
||||
close => {
|
||||
netplay_close();
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 3): Cheats {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cheats_select_rom_clicked(rom_dir) => {
|
||||
cheats_select_rom_clicked(rom_dir);
|
||||
}
|
||||
@@ -146,11 +299,13 @@ export component AppWindow inherits Window {
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 4): RetroAchievements {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
ra_button_clicked(password) => {
|
||||
ra_button_clicked(password);
|
||||
}
|
||||
ra_toggled(enabled,hardcore,challenge,leaderboard) => {
|
||||
ra_toggled(enabled,hardcore,challenge,leaderboard);
|
||||
ra_toggled(enabled,hardcore,challenge,leaderboard,rich_presence) => {
|
||||
ra_toggled(enabled,hardcore,challenge,leaderboard,rich_presence);
|
||||
}
|
||||
ra_games_clicked => {
|
||||
ra_games_clicked();
|
||||
@@ -158,9 +313,14 @@ export component AppWindow inherits Window {
|
||||
ra_show_profile_clicked => {
|
||||
ra_show_profile_clicked();
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 5): Settings { }
|
||||
}
|
||||
if(side-bar.current-item == 5): Settings {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
if(side-bar.current-item == 6): About {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
wiki_button_clicked => {
|
||||
wiki_button_clicked();
|
||||
}
|
||||
@@ -180,17 +340,62 @@ export component AppWindow inherits Window {
|
||||
source_code_button_clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component ErrorDialog inherits Dialog {
|
||||
in-out property <string> text;
|
||||
title: @tr("Error");
|
||||
Text {
|
||||
text: text;
|
||||
}
|
||||
|
||||
StandardButton {
|
||||
kind: close;
|
||||
popup := PopupWindow {
|
||||
close-policy: no-auto-close;
|
||||
Rectangle {
|
||||
height:100%;
|
||||
width: 100%;
|
||||
background: Palette.background;
|
||||
VerticalBox {
|
||||
alignment: center;
|
||||
HorizontalBox{
|
||||
alignment: center;
|
||||
Text {
|
||||
text: popup_message;
|
||||
}
|
||||
}
|
||||
if (popup_has_close):HorizontalBox{
|
||||
alignment: center;
|
||||
StandardButton {
|
||||
kind: ok;
|
||||
clicked() => {
|
||||
popup.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
x: content-area.x;
|
||||
y: content-area.y;
|
||||
height: content-area.height;
|
||||
width: content-area.width;
|
||||
}
|
||||
customserverdialog := CustomServerDialog{
|
||||
x: content-area.x;
|
||||
y: content-area.y;
|
||||
height: content-area.height;
|
||||
width: content-area.width;
|
||||
}
|
||||
}
|
||||
public function show_custom_server_dialog() {
|
||||
customserverdialog.show();
|
||||
}
|
||||
public function show_message(message: string, has_close: bool) {
|
||||
popup_message = message;
|
||||
popup_has_close = has_close;
|
||||
popup.show();
|
||||
}
|
||||
public function close_message() {
|
||||
popup.close();
|
||||
}
|
||||
public function clear_netplay_data() {
|
||||
NetplayData.player_name = "";
|
||||
NetplayData.game_name = "";
|
||||
NetplayData.password = "";
|
||||
NetplayData.session_name = "";
|
||||
NetplayData.custom_server_url = "";
|
||||
NetplayWaitData.chat_text = "";
|
||||
NetplayData.pending_session = false;
|
||||
NetplayData.pending_refresh = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,9 +48,11 @@ export component Cheats inherits Page {
|
||||
Rectangle { }
|
||||
|
||||
if CheatsData.cheats.length > 0:ListView {
|
||||
preferred-height: 500px;
|
||||
preferred-width: 520px;
|
||||
mouse-drag-pan-enabled: true;
|
||||
preferred-height: root.height;
|
||||
horizontal-stretch: 1;
|
||||
for data in CheatsData.cheats: VerticalBox {
|
||||
width: parent.width;
|
||||
Text {
|
||||
text: data.cheat_name;
|
||||
font-size: 16px;
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Page } from "page.slint";
|
||||
import { State } from "localgame_page.slint";
|
||||
|
||||
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;
|
||||
@@ -14,20 +13,160 @@ export global ControllerData {
|
||||
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 <[string]> controller_paths;
|
||||
in-out property <[int]> selected_controller;
|
||||
in-out property <[bool]> controller_changed;
|
||||
in-out property <bool> blank_profiles;
|
||||
}
|
||||
|
||||
component ControllerPortLabel inherits Text {
|
||||
height: 32px;
|
||||
width: 36px;
|
||||
vertical-alignment: center;
|
||||
horizontal-alignment: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
component ControllerFieldHeader inherits Text {
|
||||
height: 28px;
|
||||
horizontal-alignment: left;
|
||||
vertical-alignment: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
component ControllerSettingsColumn inherits VerticalLayout {
|
||||
in property <string> title;
|
||||
in property <length> content-min-width: 0px;
|
||||
spacing: 6px;
|
||||
alignment: start;
|
||||
horizontal-stretch: 0;
|
||||
min-width: content-min-width > header.preferred-width ? content-min-width : header.preferred-width;
|
||||
|
||||
header := ControllerFieldHeader {
|
||||
text: root.title;
|
||||
}
|
||||
|
||||
@children
|
||||
}
|
||||
|
||||
component ControllerSettingsCell inherits HorizontalLayout {
|
||||
height: 32px;
|
||||
alignment: start;
|
||||
|
||||
@children
|
||||
}
|
||||
|
||||
component ControllerSettingsGrid inherits HorizontalLayout {
|
||||
callback transferpak_toggled(int, bool);
|
||||
|
||||
spacing: 12px;
|
||||
alignment: start;
|
||||
|
||||
VerticalLayout {
|
||||
spacing: 6px;
|
||||
alignment: start;
|
||||
horizontal-stretch: 0;
|
||||
|
||||
Rectangle {
|
||||
width: 36px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
for player[index] in [0, 1, 2, 3]: ControllerPortLabel {
|
||||
text: index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
ControllerSettingsColumn {
|
||||
title: @tr("ControllerSettingsGrid" => "Enabled");
|
||||
|
||||
for player[index] in [0, 1, 2, 3]: ControllerSettingsCell {
|
||||
Switch {
|
||||
checked: ControllerData.controller_enabled[index];
|
||||
toggled => {
|
||||
ControllerData.controller_enabled[index] = self.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ControllerSettingsColumn {
|
||||
title: @tr("ControllerSettingsGrid" => "Transfer Pak");
|
||||
|
||||
for player[index] in [0, 1, 2, 3]: ControllerSettingsCell {
|
||||
Switch {
|
||||
checked: ControllerData.transferpak[index];
|
||||
toggled => {
|
||||
ControllerData.transferpak[index] = self.checked;
|
||||
root.transferpak_toggled(index, self.checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ControllerSettingsColumn {
|
||||
title: @tr("ControllerSettingsGrid" => "Profile");
|
||||
content-min-width: 150px;
|
||||
|
||||
for player[index] in [0, 1, 2, 3]: ControllerSettingsCell {
|
||||
ComboBox {
|
||||
horizontal-stretch: 0;
|
||||
min-width: self.preferred-width > 150px ? self.preferred-width : 150px;
|
||||
model: ControllerData.input_profiles;
|
||||
current-index: ControllerData.selected_profile_binding[index];
|
||||
selected => {
|
||||
ControllerData.selected_profile_binding[index] = self.current-index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component ControllerAssignmentGrid inherits VerticalLayout {
|
||||
spacing: 6px;
|
||||
alignment: start;
|
||||
|
||||
for player[index] in [0, 1, 2, 3]: HorizontalLayout {
|
||||
spacing: 8px;
|
||||
alignment: start;
|
||||
|
||||
ControllerPortLabel {
|
||||
text: index + 1;
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
alignment: start;
|
||||
|
||||
ComboBox {
|
||||
horizontal-stretch: 0;
|
||||
min-width: self.preferred-width > 300px ? self.preferred-width : 300px;
|
||||
model: ControllerData.controller_names;
|
||||
current-index: ControllerData.selected_controller[index];
|
||||
selected => {
|
||||
ControllerData.selected_controller[index] = self.current-index;
|
||||
ControllerData.controller_changed[index] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component ControllerConfig inherits Page {
|
||||
callback input_profile_button_clicked;
|
||||
callback transferpak_toggled(int,bool);
|
||||
callback transferpak_toggled(int, bool);
|
||||
callback controller_window_created;
|
||||
title: @tr("Controller Configuration");
|
||||
|
||||
init => {
|
||||
controller_window_created();
|
||||
}
|
||||
|
||||
VerticalBox {
|
||||
spacing: 8px;
|
||||
alignment: start;
|
||||
|
||||
HorizontalBox {
|
||||
alignment: start;
|
||||
|
||||
Button {
|
||||
text: @tr("Configure Input Profile");
|
||||
enabled: !State.game_running;
|
||||
@@ -37,93 +176,20 @@ export component ControllerConfig inherits Page {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { }
|
||||
GroupBox {
|
||||
title: @tr("Controller Settings");
|
||||
|
||||
HorizontalBox {
|
||||
GroupBox {
|
||||
title: @tr("Port");
|
||||
VerticalLayout {
|
||||
alignment: space-between;
|
||||
for player in [0, 1, 2, 3]: Text {
|
||||
text: player + 1;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
title: @tr("Enabled");
|
||||
VerticalLayout {
|
||||
alignment: space-between;
|
||||
for player in [0, 1, 2, 3]: Switch {
|
||||
checked: ControllerData.controller_enabled[player];
|
||||
toggled => {
|
||||
ControllerData.controller_enabled[player] = self.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
title: @tr("Emulate VRU");
|
||||
VerticalLayout {
|
||||
alignment: space-between;
|
||||
for valid in [false, false, false, true]: Switch {
|
||||
enabled: valid;
|
||||
checked: valid && ControllerData.emulate_vru;
|
||||
toggled => {
|
||||
ControllerData.emulate_vru = self.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
title: @tr("Transfer Pak");
|
||||
VerticalLayout {
|
||||
alignment: space-between;
|
||||
for player in [0, 1, 2, 3]: Switch {
|
||||
checked: ControllerData.transferpak[player];
|
||||
toggled => {
|
||||
ControllerData.transferpak[player] = self.checked;
|
||||
transferpak_toggled(player,self.checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !ControllerData.blank_profiles: GroupBox {
|
||||
title: @tr("Profile");
|
||||
horizontal-stretch: 0.25;
|
||||
VerticalLayout {
|
||||
alignment: space-between;
|
||||
for player in [0, 1, 2, 3]: ComboBox {
|
||||
model: ControllerData.input_profiles;
|
||||
min-width: self.preferred-width > 150px ? self.preferred-width : 150px;
|
||||
current-index: ControllerData.selected_profile_binding[player];
|
||||
selected => {
|
||||
ControllerData.selected_profile_binding[player] = self.current-index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
title: @tr("Controller");
|
||||
horizontal-stretch: 0.75;
|
||||
VerticalLayout {
|
||||
alignment: space-between;
|
||||
for player in [0, 1, 2, 3]: ComboBox {
|
||||
model: ControllerData.controller_names;
|
||||
min-width: self.preferred-width > 300px ? self.preferred-width : 300px;
|
||||
current-index: ControllerData.selected_controller[player];
|
||||
selected => {
|
||||
ControllerData.selected_controller[player] = self.current-index;
|
||||
ControllerData.controller_changed[player] = true;
|
||||
}
|
||||
}
|
||||
ControllerSettingsGrid {
|
||||
transferpak_toggled(index, enabled) => {
|
||||
root.transferpak_toggled(index, enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
title: @tr("Controller Assignment");
|
||||
|
||||
ControllerAssignmentGrid { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
import { StandardButton, HorizontalBox, VerticalBox, Button, LineEdit, Switch, Slider } from "std-widgets.slint";
|
||||
import { HorizontalBox, VerticalBox, Button, LineEdit, Switch, Slider } from "std-widgets.slint";
|
||||
import { Page } from "page.slint";
|
||||
|
||||
export global InputProfileData {
|
||||
in-out property <bool> dinput;
|
||||
in-out property <string> profile_name;
|
||||
in-out property <int> deadzone;
|
||||
in-out property <bool> show_input_profile;
|
||||
}
|
||||
|
||||
export component InputProfileDialog inherits Window {
|
||||
title: @tr("Create Input Profile");
|
||||
export component InputProfileDialog inherits Page {
|
||||
title: @tr("Create/Modify Input Profile");
|
||||
callback profile_creation_button_clicked;
|
||||
in-out property dinput <=> InputProfileData.dinput;
|
||||
in-out property profile_name <=> InputProfileData.profile_name;
|
||||
in-out property deadzone <=> InputProfileData.deadzone;
|
||||
VerticalBox {
|
||||
alignment: start;
|
||||
|
||||
HorizontalBox {
|
||||
alignment: center;
|
||||
vertical-stretch: 0;
|
||||
LineEdit {
|
||||
placeholder-text: @tr("Enter profile name");
|
||||
preferred-width: 300px;
|
||||
vertical-stretch: 0;
|
||||
placeholder-text: @tr("Enter profile name to create or modify");
|
||||
text: InputProfileData.profile_name;
|
||||
preferred-width: 400px;
|
||||
edited(text) => {
|
||||
InputProfileData.profile_name = text;
|
||||
}
|
||||
@@ -24,6 +32,7 @@ export component InputProfileDialog inherits Window {
|
||||
}
|
||||
|
||||
HorizontalBox {
|
||||
vertical-stretch: 0;
|
||||
alignment: center;
|
||||
Text {
|
||||
text: @tr("Deadzone Percentage");
|
||||
@@ -46,6 +55,7 @@ export component InputProfileDialog inherits Window {
|
||||
}
|
||||
|
||||
HorizontalBox {
|
||||
vertical-stretch: 0;
|
||||
alignment: center;
|
||||
Switch {
|
||||
text: @tr("Use DirectInput");
|
||||
@@ -57,13 +67,23 @@ export component InputProfileDialog inherits Window {
|
||||
}
|
||||
|
||||
HorizontalBox {
|
||||
alignment: center;
|
||||
vertical-stretch: 0;
|
||||
Button {
|
||||
vertical-stretch: 0;
|
||||
text: @tr("Start Profile Creation Wizard");
|
||||
enabled: InputProfileData.profile_name != "" && InputProfileData.profile_name != "default";
|
||||
clicked => {
|
||||
profile_creation_button_clicked();
|
||||
}
|
||||
}
|
||||
Button {
|
||||
vertical-stretch: 0;
|
||||
text: @tr("Cancel");
|
||||
clicked => {
|
||||
InputProfileData.show_input_profile = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AboutData, NewVersionButton } from "about_page.slint";
|
||||
|
||||
export global State {
|
||||
in-out property <bool> game_running;
|
||||
in-out property <bool> is_android;
|
||||
in-out property <[{a:string, b:string}]> recent_roms;
|
||||
}
|
||||
|
||||
@@ -30,7 +31,7 @@ export component LocalGame inherits Page {
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalBox {
|
||||
if !State.is_android:HorizontalBox {
|
||||
alignment: center;
|
||||
Button {
|
||||
text: @tr("Open Saves Folder");
|
||||
|
||||
@@ -1,130 +1,182 @@
|
||||
import { LineEdit, HorizontalBox, VerticalBox, Button, ComboBox } from "std-widgets.slint";
|
||||
import { Palette, StandardButton, LineEdit, HorizontalBox, VerticalBox, Button, ComboBox, ScrollView } from "std-widgets.slint";
|
||||
import { NetplayData } from "netplay_page.slint";
|
||||
import { SettingsData } from "settings_page.slint";
|
||||
import { Page } from "page.slint";
|
||||
|
||||
export component NetplayCreate inherits Window {
|
||||
export component NetplayCreate inherits Page {
|
||||
title: @tr("Netplay: Create Session");
|
||||
in-out property server_names <=> NetplayData.server_names;
|
||||
in-out property server_urls <=> NetplayData.server_urls;
|
||||
in-out property custom_server_url <=> NetplayData.custom_server_url;
|
||||
in-out property game_name <=> NetplayData.game_name;
|
||||
in-out property game_hash <=> NetplayData.game_hash;
|
||||
in-out property game_cheats <=> NetplayData.game_cheats;
|
||||
in-out property pending_session <=> NetplayData.pending_session;
|
||||
in-out property rom_path <=> NetplayData.rom_path;
|
||||
in-out property peer_addr <=> NetplayData.peer_addr;
|
||||
in-out property rom_dir <=> NetplayData.rom_dir;
|
||||
callback get_custom_url();
|
||||
callback select_rom(string);
|
||||
callback create_session(string, string, string, string, string, string, string);
|
||||
callback close;
|
||||
|
||||
VerticalBox {
|
||||
alignment: start;
|
||||
HorizontalBox {
|
||||
alignment: center;
|
||||
VerticalBox {
|
||||
Text {
|
||||
text: @tr("Session Name:");
|
||||
}
|
||||
alignment: start;
|
||||
horizontal-stretch: 0;
|
||||
HorizontalBox {
|
||||
VerticalBox {
|
||||
Text {
|
||||
text: @tr("Session Name:");
|
||||
}
|
||||
|
||||
LineEdit {
|
||||
placeholder-text: @tr("Enter session name");
|
||||
edited(text) => {
|
||||
NetplayData.session_name = text;
|
||||
LineEdit {
|
||||
placeholder-text: @tr("Enter session name");
|
||||
text: NetplayData.session_name;
|
||||
edited(text) => {
|
||||
NetplayData.session_name = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalBox {
|
||||
Text {
|
||||
text: @tr("Password (Optional):");
|
||||
}
|
||||
VerticalBox {
|
||||
Text {
|
||||
text: @tr("Password (Optional):");
|
||||
}
|
||||
|
||||
LineEdit {
|
||||
placeholder-text: @tr("Enter session password");
|
||||
edited(text) => {
|
||||
NetplayData.password = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalBox {
|
||||
Text {
|
||||
text: @tr("Player Name:");
|
||||
}
|
||||
|
||||
LineEdit {
|
||||
placeholder-text: @tr("Enter player name");
|
||||
edited(text) => {
|
||||
NetplayData.player_name = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalBox {
|
||||
Text {
|
||||
text: @tr("Server:");
|
||||
}
|
||||
|
||||
HorizontalBox {
|
||||
server := ComboBox {
|
||||
model: NetplayData.server_names;
|
||||
selected => {
|
||||
if (self.current-index >= 0) {
|
||||
if (self.current-value == "Custom") {
|
||||
get_custom_url();
|
||||
LineEdit {
|
||||
placeholder-text: @tr("Enter session password");
|
||||
text: NetplayData.password;
|
||||
edited(text) => {
|
||||
NetplayData.password = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if NetplayData.game_name == "": Button {
|
||||
text: @tr("Select ROM");
|
||||
clicked => {
|
||||
select_rom(NetplayData.rom_dir);
|
||||
}
|
||||
}
|
||||
if NetplayData.game_name != "": Button {
|
||||
text: NetplayData.game_name;
|
||||
clicked => {
|
||||
select_rom(NetplayData.rom_dir);
|
||||
}
|
||||
}
|
||||
VerticalBox {
|
||||
Text {
|
||||
text: @tr("Player Name:");
|
||||
}
|
||||
|
||||
Rectangle { }
|
||||
|
||||
if NetplayData.game_cheats != "":Text {
|
||||
text: @tr("This game has cheats enabled. Cheats will be synchronized with other players.");
|
||||
}
|
||||
Rectangle { }
|
||||
|
||||
Button {
|
||||
property <string> server_url;
|
||||
text: @tr("Create Session");
|
||||
enabled: NetplayData.session_name != "" && NetplayData.player_name != "" && NetplayData.game_name != "" && !NetplayData.pending_session;
|
||||
clicked => {
|
||||
NetplayData.pending_session = true;
|
||||
if (server.current-value == "Custom") {
|
||||
server_url = "ws://" + NetplayData.custom_server_url;
|
||||
} else {
|
||||
server_url = NetplayData.server_urls[server.current-index];
|
||||
LineEdit {
|
||||
placeholder-text: @tr("Enter player name");
|
||||
text: NetplayData.player_name;
|
||||
edited(text) => {
|
||||
NetplayData.player_name = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
create_session(
|
||||
server_url,
|
||||
NetplayData.session_name,
|
||||
NetplayData.player_name,
|
||||
NetplayData.game_name,
|
||||
NetplayData.game_hash,
|
||||
NetplayData.game_cheats,
|
||||
NetplayData.password);
|
||||
|
||||
VerticalBox {
|
||||
Text {
|
||||
text: @tr("Server:");
|
||||
}
|
||||
|
||||
HorizontalBox {
|
||||
server := ComboBox {
|
||||
model: NetplayData.server_names;
|
||||
selected => {
|
||||
if (self.current-index >= 0) {
|
||||
if (self.current-value == "Custom") {
|
||||
get_custom_url();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalBox {
|
||||
alignment: center;
|
||||
if NetplayData.game_name == "": Button {
|
||||
text: @tr("Select ROM");
|
||||
clicked => {
|
||||
select_rom(SettingsData.rom_dir);
|
||||
}
|
||||
}
|
||||
if NetplayData.game_name != "": Button {
|
||||
text: NetplayData.game_name;
|
||||
clicked => {
|
||||
select_rom(SettingsData.rom_dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { }
|
||||
|
||||
if NetplayData.game_cheats != "": Text {
|
||||
text: @tr("This game has cheats enabled. Cheats will be synchronized with other players.");
|
||||
horizontal-alignment: center;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
Rectangle { }
|
||||
|
||||
HorizontalBox {
|
||||
alignment: center;
|
||||
Button {
|
||||
property <string> server_url;
|
||||
text: @tr("Create Session");
|
||||
enabled: NetplayData.session_name != "" && NetplayData.player_name != "" && NetplayData.game_name != "" && !NetplayData.pending_session;
|
||||
clicked => {
|
||||
NetplayData.pending_session = true;
|
||||
if (server.current-value == "Custom") {
|
||||
server_url = "ws://" + NetplayData.custom_server_url;
|
||||
} else {
|
||||
server_url = NetplayData.server_urls[server.current-index];
|
||||
}
|
||||
create_session(
|
||||
server_url,
|
||||
NetplayData.session_name,
|
||||
NetplayData.player_name,
|
||||
NetplayData.game_name,
|
||||
NetplayData.game_hash,
|
||||
NetplayData.game_cheats,
|
||||
NetplayData.password);
|
||||
}
|
||||
}
|
||||
Button {
|
||||
text: @tr("Cancel");
|
||||
clicked => {
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component DispatcherDialog inherits Dialog {
|
||||
title: @tr("Creating Server");
|
||||
Text {
|
||||
text: "Creating server, please wait...This may take about 30 seconds.";
|
||||
export component CustomServerDialog inherits PopupWindow {
|
||||
close-policy: no-auto-close;
|
||||
Rectangle {
|
||||
height:100%;
|
||||
width: 100%;
|
||||
background: Palette.background;
|
||||
VerticalBox {
|
||||
alignment: center;
|
||||
HorizontalBox{
|
||||
alignment: center;
|
||||
Text {
|
||||
text: @tr("Enter a custom server URL");
|
||||
}
|
||||
}
|
||||
HorizontalBox {
|
||||
alignment: center;
|
||||
vertical-stretch: 0;
|
||||
LineEdit {
|
||||
vertical-stretch: 0;
|
||||
text: NetplayData.custom_server_url;
|
||||
placeholder-text: "example.com:45000";
|
||||
preferred-width: 300px;
|
||||
edited(text) => {
|
||||
NetplayData.custom_server_url = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalBox{
|
||||
alignment: center;
|
||||
StandardButton {
|
||||
kind: ok;
|
||||
clicked() => {
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||