mirror of
https://github.com/gopher64/gopher64.git
synced 2026-07-11 09:35:21 +02:00
Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1ad3aac06 | |||
| e8a7abffe3 | |||
| f7ad6a4b0d | |||
| 1bfb50de8a | |||
| 3bc7c96191 | |||
| 032bf1acf3 | |||
| 8bc765c1a3 | |||
| da061240af | |||
| 407a1b967c | |||
| ad756cd6bd | |||
| 6e11bc18d6 | |||
| c8eee9f727 | |||
| 01a53ce1c0 | |||
| 0eddf31f92 | |||
| e257ba717b | |||
| 7a797165fd | |||
| a659413090 | |||
| 08e7ec71d9 | |||
| c97f14980b | |||
| 5d090f496b | |||
| d6b46037b4 | |||
| 5b5acfdd25 | |||
| 926338ba62 | |||
| cc420013f9 | |||
| ba0a21feca | |||
| 70b9d20c64 | |||
| dee2b85daa | |||
| 119e9044a8 | |||
| e6a260bd6a | |||
| a23ede433b | |||
| 346b0a139d | |||
| f5cda2901f | |||
| 2dda5c8706 | |||
| d6aa2ddce3 | |||
| 6f9801a185 | |||
| c82edae338 | |||
| 57327c1f73 | |||
| e3a22bf7d0 | |||
| 3718d914fd | |||
| 97a8b63c9c | |||
| d6d0b66078 | |||
| b173598320 | |||
| f211eede3b | |||
| b123ed9fa3 | |||
| 2b0d8e5423 | |||
| 49b8024fbe | |||
| be987e1434 | |||
| 27e2b89fcf | |||
| c261a746a3 | |||
| c290e60af1 | |||
| 19142a30c6 | |||
| 2ca317d742 | |||
| 45963aeea5 | |||
| 4929f2cc51 | |||
| 90e51f5f09 | |||
| 9b7aeccd48 | |||
| 10d55bd25c | |||
| 16224cd5a6 | |||
| d62d4c852a | |||
| 6d6349c79d | |||
| 9d2eec1732 | |||
| d6da1656b8 | |||
| ae7bac65d5 | |||
| 99c4d6dff8 | |||
| 74600b4b25 | |||
| 6cae2ed3ac | |||
| 611b56186d | |||
| dfa673e6e9 | |||
| be74e3358a | |||
| 4cc85d595f | |||
| b2d7a12033 | |||
| 65cffbba0c | |||
| 5f8e3b9760 | |||
| 397a15349c | |||
| 235a4af0a9 | |||
| b171d48f87 | |||
| bfeba94e20 | |||
| aa0bde0999 | |||
| 72e4a6b94a | |||
| 88f00f5638 | |||
| db21800d9f | |||
| 4a63f469bf | |||
| 810852997a | |||
| 02d53beeac | |||
| d589d8bac2 | |||
| 0deb1d11fc | |||
| 6cc6153abb | |||
| af163dac62 | |||
| 8c4be03ab7 | |||
| fce57f8ee2 | |||
| d48423da77 | |||
| f691578698 | |||
| 42a4a46dd9 | |||
| e1d715bc38 | |||
| 4285290452 | |||
| d79834eb50 | |||
| 1244706b6e | |||
| a1a7699516 | |||
| 577741907f | |||
| ba35935d2e | |||
| a7df8d837f | |||
| 820c577f5f | |||
| 8b3a1a40f2 | |||
| 21238c8e41 | |||
| 73f145a35c | |||
| 3184305c41 | |||
| 06dc40fd3e | |||
| c7ac333cb7 | |||
| 1d6e415422 | |||
| c89c5d1cc3 | |||
| 19dc966abd | |||
| 077c1f439c | |||
| 9140138843 | |||
| 618e3a32f2 | |||
| 681f8670f5 | |||
| 0a44124c20 | |||
| eb64a9ad43 | |||
| 5a5ff40efc | |||
| cd6726d451 | |||
| cbc973a559 | |||
| d968983ceb | |||
| 5175d50aa8 | |||
| e25818b451 | |||
| 4453e66bcf | |||
| 19432f33ba | |||
| 5c7170268e | |||
| 79d2c023e6 | |||
| 864175fae6 | |||
| 7fc2412c9c | |||
| 3fdbe9a4f7 | |||
| 399b5d65e5 | |||
| 24e5a3fac4 | |||
| 31420b6777 | |||
| 7379484932 | |||
| d36ce10ab3 | |||
| 57807f09c8 | |||
| 0745d75c2f |
@@ -24,3 +24,4 @@ CC = "clang"
|
||||
CXX = "clang++"
|
||||
AR = "llvm-ar"
|
||||
SDKROOT = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"
|
||||
CMAKE_POLICY_VERSION_MINIMUM = "3.5" # for freetype
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Bugs only! No feature requests
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
type: Bug
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Device (please complete the following information):**
|
||||
- OS: [e.g. Windows 11]
|
||||
- CPU: [e.g. Intel i7-1365U]
|
||||
- GPU: [e.g. Nvidia 5060 Ti]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
arch: linux-aarch64
|
||||
llvm: ARM64
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
arch: windows-aarch64
|
||||
llvm: woa64
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
@@ -163,7 +163,7 @@ jobs:
|
||||
arch: macos-aarch64
|
||||
llvm: ARM64
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
@@ -227,9 +227,9 @@ jobs:
|
||||
include:
|
||||
- os: ubuntu-24.04
|
||||
platform: 33
|
||||
ndk: 27.3.13750724
|
||||
ndk: 29.0.14206865
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
lint-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
checks: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: rustsec/audit-check@v2
|
||||
|
||||
Generated
+2098
-850
File diff suppressed because it is too large
Load Diff
+8
-9
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gopher64"
|
||||
version = "1.1.22"
|
||||
version = "1.1.32"
|
||||
edition = "2024"
|
||||
rust-version = "1.96.0"
|
||||
|
||||
@@ -19,21 +19,24 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde-big-array = "0.5"
|
||||
sha2 = "0.11"
|
||||
tokio = {version = "1.46", features = ["rt-multi-thread", "macros", "fs", "process"] }
|
||||
tokio = {version = "1.46", features = ["rt-multi-thread", "fs", "process"] }
|
||||
spin_sleep = "1.3"
|
||||
encoding_rs = "0.8"
|
||||
mimalloc = "0.1"
|
||||
regex = "1.11"
|
||||
tokio-tungstenite = "0.29"
|
||||
tokio-tungstenite = { version = "0.29", features = ["native-tls-vendored"] }
|
||||
matchbox_socket = "0.14"
|
||||
ggrs = { version = "0.13", features = ["sync-send"] }
|
||||
rustc-hash = "2.1"
|
||||
futures = "0.3"
|
||||
socket2 = { version = "0.6", features = ["all"] }
|
||||
rand = "0.10"
|
||||
rand = { version = "0.10", features = ["serde"] }
|
||||
fatfs = "0.3"
|
||||
semver = "1.0"
|
||||
postcard = { version = "1.1", features = ["use-std"] }
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
reqwest = { version = "0.13", default-features = false, features = ["native-tls-vendored", "charset", "http2", "system-proxy", "json", "query"] }
|
||||
discord-rich-presence = "1.1"
|
||||
quick-xml = "0.40"
|
||||
|
||||
[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 }
|
||||
@@ -42,14 +45,12 @@ 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"
|
||||
@@ -59,7 +60,6 @@ slint-build = { version = "1.12", optional = true }
|
||||
cc = { version = "1.2", features = ["parallel"] }
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
lto = "thin"
|
||||
strip = true
|
||||
|
||||
@@ -67,7 +67,6 @@ strip = true
|
||||
inherits = "release"
|
||||
debug = true
|
||||
strip = false
|
||||
panic = "unwind"
|
||||
|
||||
[package.metadata.bundle]
|
||||
name = "Gopher64"
|
||||
|
||||
@@ -24,6 +24,9 @@ Linux:
|
||||
* Standalone executable: [gopher64-linux-x86_64](https://github.com/gopher64/gopher64/releases/latest/download/gopher64-linux-x86_64)
|
||||
* Flatpak: `flatpak install flathub io.github.gopher64.gopher64`
|
||||
|
||||
Android:
|
||||
* APK: [gopher64-android.apk](https://github.com/gopher64/gopher64/releases/latest/download/gopher64-android.apk)
|
||||
|
||||
## wiki
|
||||
|
||||
https://github.com/gopher64/gopher64/wiki
|
||||
@@ -38,7 +41,9 @@ Keys are mapped according to [these defaults](https://github.com/gopher64/gopher
|
||||
|
||||
## netplay
|
||||
|
||||
Gopher64 supports netplay (online play with others) via cloud hosted servers. You can also run the [server](https://github.com/gopher64/gopher64-netplay-server) yourself on a LAN.
|
||||
Gopher64 supports P2P netplay (online play with others).
|
||||
|
||||
Please read the [guide](https://github.com/gopher64/gopher64/wiki/Netplay-Guide) before trying out netplay.
|
||||
|
||||
## portable mode
|
||||
|
||||
@@ -63,7 +68,9 @@ flatpak run --filesystem=host:ro io.github.gopher64.gopher64 /path/to/rom.z64
|
||||
|
||||
## contributing
|
||||
|
||||
I am very open to contributions! Please contact me via a GitHub issue or Discord (loganmc10) before doing substantial work on a PR.
|
||||
PRs that are vibe coded and/or co-authored by AI agents (Plan/Agent mode) are not permitted. If you are going to submit a PR, it should be scoped to a single feature/improvement.
|
||||
|
||||
Please contact me via a GitHub issue or Discord (loganmc10) before doing substantial work on a PR.
|
||||
|
||||
## license
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
ndkVersion = "27.3.13750724"
|
||||
ndkVersion = "29.0.14206865"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "io.github.gopher64.gopher64"
|
||||
@@ -100,6 +100,7 @@ val ndkBuild = tasks.register<Exec>("ndkBuild") {
|
||||
var minSdk = android.defaultConfig.minSdk
|
||||
var ndkDir = androidComponents.sdkComponents.ndkDirectory.get().asFile.absolutePath
|
||||
environment("ANDROID_NDK_HOME", "$ndkDir")
|
||||
environment("ANDROID_NDK_ROOT", "$ndkDir")
|
||||
environment("LIBCLANG_PATH", "$ndkDir/toolchains/llvm/prebuilt/linux-x86_64/musl/lib")
|
||||
|
||||
val jniType = if (isRelease) "release" else "debug"
|
||||
|
||||
@@ -24,7 +24,6 @@ class N64Activity : SDLActivity() {
|
||||
override fun getLibraries(): Array<String> = arrayOf(
|
||||
"SDL3",
|
||||
"SDL3_ttf",
|
||||
"SDL3_image",
|
||||
"gopher64"
|
||||
)
|
||||
|
||||
|
||||
@@ -206,6 +206,7 @@ fn main() {
|
||||
.allowlist_function("rdp_state_size")
|
||||
.allowlist_function("rdp_save_state")
|
||||
.allowlist_function("rdp_load_state")
|
||||
.allowlist_function("rdp_idle")
|
||||
.allowlist_function("rdp_set_fps")
|
||||
.allowlist_function("get_joystick_event")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-05-25 08:28+0000\n"
|
||||
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
@@ -47,37 +47,37 @@ msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Spenden (über GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:151
|
||||
#: src/ui/gui/appwindow.slint:164
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Lokales Spiel"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:155
|
||||
#: src/ui/gui/appwindow.slint:168
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Controller-Konfiguration"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:159
|
||||
#: src/ui/gui/appwindow.slint:172
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:163
|
||||
#: src/ui/gui/appwindow.slint:176
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Cheats"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:167
|
||||
#: src/ui/gui/appwindow.slint:180
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:171
|
||||
#: src/ui/gui/appwindow.slint:184
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:175
|
||||
#: src/ui/gui/appwindow.slint:188
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "Über"
|
||||
@@ -97,7 +97,7 @@ msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Cheats für alle ROMs löschen"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:88
|
||||
#: src/ui/gui/cheat_page.slint:83
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "Keine Cheats verfügbar"
|
||||
@@ -192,47 +192,42 @@ msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Netplay: Sitzung erstellen"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:23
|
||||
#: src/ui/gui/netplay_create.slint:22
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Sitzungsname:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:27
|
||||
#: src/ui/gui/netplay_create.slint:26
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Sitzungsname eingeben"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:37
|
||||
#: src/ui/gui/netplay_create.slint:36
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Passwort (optional):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:41
|
||||
#: src/ui/gui/netplay_create.slint:40
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Sitzungspasswort eingeben"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:52
|
||||
#: src/ui/gui/netplay_create.slint:51
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Spielername:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:56
|
||||
#: src/ui/gui/netplay_create.slint:55
|
||||
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
|
||||
#: src/ui/gui/netplay_create.slint:82
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
@@ -240,21 +235,16 @@ msgstr ""
|
||||
"Für dieses Spiel sind Cheats aktiviert. Cheats werden mit anderen Spielern "
|
||||
"synchronisiert."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:112
|
||||
#: src/ui/gui/netplay_create.slint:91
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Sitzung erstellen"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:132
|
||||
#: src/ui/gui/netplay_create.slint:107
|
||||
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"
|
||||
@@ -275,152 +265,163 @@ 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
|
||||
#: src/ui/gui/netplay_join.slint:42
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROM auswählen"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:68
|
||||
#: src/ui/gui/netplay_join.slint:57
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Sitzungsliste wird aktualisiert, bitte warten …"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
#: src/ui/gui/netplay_join.slint:62
|
||||
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
|
||||
#: src/ui/gui/netplay_join.slint:67
|
||||
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
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Sitzungsname"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:86
|
||||
#: src/ui/gui/netplay_join.slint:74
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Spielname"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:87
|
||||
#: src/ui/gui/netplay_join.slint:75
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Passwort"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:88
|
||||
#: src/ui/gui/netplay_join.slint:76
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Cheats"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:102
|
||||
#: src/ui/gui/netplay_join.slint:90
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Sitzungsliste aktualisieren"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:119
|
||||
#: src/ui/gui/netplay_join.slint:105
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Sitzung beitreten"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:132
|
||||
#: src/ui/gui/netplay_join.slint:117
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:37
|
||||
#: src/ui/gui/netplay_page.slint:31
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:45
|
||||
#: src/ui/gui/netplay_page.slint:39
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Sitzung erstellen"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:56
|
||||
#: src/ui/gui/netplay_page.slint:50
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Sitzung beitreten"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:70
|
||||
#: src/ui/gui/netplay_page.slint:64
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Netplay-Discord-Kanal"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:77
|
||||
#: src/ui/gui/netplay_page.slint:71
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Feedback geben"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:15
|
||||
#: src/ui/gui/netplay_wait.slint:17
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Netplay: Warteraum"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:27
|
||||
#: src/ui/gui/netplay_wait.slint:29
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Sitzungsname:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:33
|
||||
#: src/ui/gui/netplay_wait.slint:35
|
||||
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
|
||||
#: src/ui/gui/netplay_wait.slint:45
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Spieler 1:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:52
|
||||
#: src/ui/gui/netplay_wait.slint:49
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Spieler 2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:58
|
||||
#: src/ui/gui/netplay_wait.slint:55
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Spieler 3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:62
|
||||
#: src/ui/gui/netplay_wait.slint:59
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Spieler 4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:76
|
||||
#: src/ui/gui/netplay_wait.slint:73
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Chatnachricht eingeben"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:87
|
||||
#: src/ui/gui/netplay_wait.slint:84
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Nachricht senden"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:105
|
||||
#: src/ui/gui/netplay_wait.slint:94
|
||||
msgctxt "NetplayWait"
|
||||
msgid ""
|
||||
"Setting the input delay too low will cause the game to lag.\n"
|
||||
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
|
||||
"Alt+]."
|
||||
msgstr ""
|
||||
"Eine zu niedrige Eingabeverzögerung führt zu Ruckeln im Spiel.\n"
|
||||
"Sie können die Eingabeverzögerung im Spiel mit Alt+[ verringern und mit "
|
||||
"Alt+] erhöhen."
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:101
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Local Input Delay:"
|
||||
msgstr "Lokale Eingabeverzögerung:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:118
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Recommended: "
|
||||
msgstr "Empfohlen: "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Spiel starten"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:113
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Waiting for P2P mesh "
|
||||
msgstr "Warten auf P2P-Mesh "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:135
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
@@ -485,71 +486,126 @@ msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Unterstützte Spiele anzeigen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:23
|
||||
#: src/ui/gui/settings_page.slint:25
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:34
|
||||
#: src/ui/gui/settings_page.slint:32
|
||||
msgctxt "Settings"
|
||||
msgid "Game"
|
||||
msgstr "Spiel"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:38
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Auflösung:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:48
|
||||
#: src/ui/gui/settings_page.slint:51
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "SSAA-Herunterskalierung"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:56
|
||||
#: src/ui/gui/settings_page.slint:59
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Pixelgenaue Skalierung"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:64
|
||||
#: src/ui/gui/settings_page.slint:67
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Vollbild"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:72
|
||||
#: src/ui/gui/settings_page.slint:75
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "16:9 (gestreckt)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:80
|
||||
#: src/ui/gui/settings_page.slint:83
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "VSync"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:88
|
||||
#: src/ui/gui/settings_page.slint:91
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "CRT-Shader anwenden"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:100
|
||||
#: src/ui/gui/settings_page.slint:103
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "N64-CPU übertakten (kann Fehler verursachen)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:108
|
||||
#: src/ui/gui/settings_page.slint:111
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Expansion Pak deaktivieren"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:116
|
||||
#: src/ui/gui/settings_page.slint:119
|
||||
msgctxt "Settings"
|
||||
msgid "Enable Rewind"
|
||||
msgstr "Rewind aktivieren"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:127
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "UNFLoader emulieren"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:124
|
||||
#: src/ui/gui/settings_page.slint:135
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "VRU emulieren"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:144
|
||||
msgctxt "Settings"
|
||||
msgid "UI"
|
||||
msgstr "Benutzeroberfläche"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:150
|
||||
msgctxt "Settings"
|
||||
msgid "Theme:"
|
||||
msgstr "Thema:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "System"
|
||||
msgstr "System"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Dark"
|
||||
msgstr "Dunkel"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Light"
|
||||
msgstr "Hell"
|
||||
|
||||
#: 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 "Server:"
|
||||
#~ msgstr "Server:"
|
||||
|
||||
#~ msgctxt "CustomServerDialog"
|
||||
#~ msgid "Enter a custom server URL"
|
||||
#~ msgstr "Eigene Server-URL eingeben"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Custom Server URL (optional):"
|
||||
#~ msgstr "Eigene Server-URL (optional):"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Server"
|
||||
#~ msgstr "Server"
|
||||
|
||||
#~ msgctxt "NetplayWait"
|
||||
#~ msgid "Ping:"
|
||||
#~ msgstr "Ping:"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-05-25 08:28+0000\n"
|
||||
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: Gemini\n"
|
||||
"Language-Team: Spanish (Spain)\n"
|
||||
@@ -47,37 +47,37 @@ msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Donar (a través de GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:151
|
||||
#: src/ui/gui/appwindow.slint:164
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Juego local"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:155
|
||||
#: src/ui/gui/appwindow.slint:168
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Config. del mando"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:159
|
||||
#: src/ui/gui/appwindow.slint:172
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:163
|
||||
#: src/ui/gui/appwindow.slint:176
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Trucos"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:167
|
||||
#: src/ui/gui/appwindow.slint:180
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:171
|
||||
#: src/ui/gui/appwindow.slint:184
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Ajustes"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:175
|
||||
#: src/ui/gui/appwindow.slint:188
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "Acerca de"
|
||||
@@ -97,7 +97,7 @@ msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Borrar trucos de todas las ROMs"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:88
|
||||
#: src/ui/gui/cheat_page.slint:83
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "No hay trucos disponibles"
|
||||
@@ -192,47 +192,42 @@ msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Netplay: Crear sesión"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:23
|
||||
#: src/ui/gui/netplay_create.slint:22
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nombre de la sesión:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:27
|
||||
#: src/ui/gui/netplay_create.slint:26
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Introduce el nombre de la sesión"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:37
|
||||
#: src/ui/gui/netplay_create.slint:36
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Contraseña (Opcional):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:41
|
||||
#: src/ui/gui/netplay_create.slint:40
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Introduce la contraseña"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:52
|
||||
#: src/ui/gui/netplay_create.slint:51
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Nombre del jugador:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:56
|
||||
#: src/ui/gui/netplay_create.slint:55
|
||||
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
|
||||
#: src/ui/gui/netplay_create.slint:82
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
@@ -240,21 +235,16 @@ msgstr ""
|
||||
"Este juego tiene trucos activados. Los trucos se sincronizarán con los otros "
|
||||
"jugadores."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:112
|
||||
#: src/ui/gui/netplay_create.slint:91
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Crear sesión"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:132
|
||||
#: src/ui/gui/netplay_create.slint:107
|
||||
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"
|
||||
@@ -275,152 +265,164 @@ 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
|
||||
#: src/ui/gui/netplay_join.slint:42
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "Seleccionar ROM"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:68
|
||||
#: src/ui/gui/netplay_join.slint:57
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Actualizando lista de sesiones, espera..."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
#: src/ui/gui/netplay_join.slint:62
|
||||
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
|
||||
#: src/ui/gui/netplay_join.slint:67
|
||||
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
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Nombre de la sesión"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:86
|
||||
#: src/ui/gui/netplay_join.slint:74
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Nombre del juego"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:87
|
||||
#: src/ui/gui/netplay_join.slint:75
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Contraseña"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:88
|
||||
#: src/ui/gui/netplay_join.slint:76
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Trucos"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:102
|
||||
#: src/ui/gui/netplay_join.slint:90
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Actualizar lista"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:119
|
||||
#: src/ui/gui/netplay_join.slint:105
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Unirse a la sesión"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:132
|
||||
#: src/ui/gui/netplay_join.slint:117
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:37
|
||||
#: src/ui/gui/netplay_page.slint:31
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:45
|
||||
#: src/ui/gui/netplay_page.slint:39
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Crear sesión"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:56
|
||||
#: src/ui/gui/netplay_page.slint:50
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Unirse a la sesión"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:70
|
||||
#: src/ui/gui/netplay_page.slint:64
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Canal de Discord de Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:77
|
||||
#: src/ui/gui/netplay_page.slint:71
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Enviar comentarios"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:15
|
||||
#: src/ui/gui/netplay_wait.slint:17
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Netplay: Sala de espera"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:27
|
||||
#: src/ui/gui/netplay_wait.slint:29
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nombre de la sesión:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:33
|
||||
#: src/ui/gui/netplay_wait.slint:35
|
||||
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
|
||||
#: src/ui/gui/netplay_wait.slint:45
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Jugador 1:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:52
|
||||
#: src/ui/gui/netplay_wait.slint:49
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Jugador 2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:58
|
||||
#: src/ui/gui/netplay_wait.slint:55
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Jugador 3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:62
|
||||
#: src/ui/gui/netplay_wait.slint:59
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Jugador 4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:76
|
||||
#: src/ui/gui/netplay_wait.slint:73
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Escribe un mensaje en el chat"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:87
|
||||
#: src/ui/gui/netplay_wait.slint:84
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Enviar mensaje"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:105
|
||||
#: src/ui/gui/netplay_wait.slint:94
|
||||
msgctxt "NetplayWait"
|
||||
msgid ""
|
||||
"Setting the input delay too low will cause the game to lag.\n"
|
||||
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
|
||||
"Alt+]."
|
||||
msgstr ""
|
||||
"Configurar un retardo de entrada demasiado bajo hará que el juego se "
|
||||
"ralentice.\n"
|
||||
"Puedes reducir el retardo de entrada en el juego pulsando Alt+[ y aumentarlo "
|
||||
"con Alt+]."
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:101
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Local Input Delay:"
|
||||
msgstr "Retardo de entrada local:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:118
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Recommended: "
|
||||
msgstr "Recomendado: "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Iniciar juego"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:113
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Waiting for P2P mesh "
|
||||
msgstr "Esperando malla P2P "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:135
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
@@ -485,71 +487,126 @@ msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Ver juegos compatibles"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:23
|
||||
#: src/ui/gui/settings_page.slint:25
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Ajustes"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:34
|
||||
#: src/ui/gui/settings_page.slint:32
|
||||
msgctxt "Settings"
|
||||
msgid "Game"
|
||||
msgstr "Juego"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:38
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Resolución:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:48
|
||||
#: src/ui/gui/settings_page.slint:51
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "Reducción de escala SSAA"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:56
|
||||
#: src/ui/gui/settings_page.slint:59
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Escalado por enteros"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:64
|
||||
#: src/ui/gui/settings_page.slint:67
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Pantalla completa"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:72
|
||||
#: src/ui/gui/settings_page.slint:75
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "Pantalla ancha (estirado)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:80
|
||||
#: src/ui/gui/settings_page.slint:83
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "Sincronización vertical (VSync)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:88
|
||||
#: src/ui/gui/settings_page.slint:91
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "Aplicar sombreador CRT"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:100
|
||||
#: src/ui/gui/settings_page.slint:103
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "Overclock CPU N64 (puede causar errores)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:108
|
||||
#: src/ui/gui/settings_page.slint:111
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Desactivar Expansion Pak"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:116
|
||||
#: src/ui/gui/settings_page.slint:119
|
||||
msgctxt "Settings"
|
||||
msgid "Enable Rewind"
|
||||
msgstr "Activar rebobinado"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:127
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "Emular UNFLoader"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:124
|
||||
#: src/ui/gui/settings_page.slint:135
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "Emular VRU"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:144
|
||||
msgctxt "Settings"
|
||||
msgid "UI"
|
||||
msgstr "Interfaz"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:150
|
||||
msgctxt "Settings"
|
||||
msgid "Theme:"
|
||||
msgstr "Tema:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "System"
|
||||
msgstr "Sistema"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Dark"
|
||||
msgstr "Oscuro"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Light"
|
||||
msgstr "Claro"
|
||||
|
||||
#: 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 "Server:"
|
||||
#~ msgstr "Servidor:"
|
||||
|
||||
#~ msgctxt "CustomServerDialog"
|
||||
#~ msgid "Enter a custom server URL"
|
||||
#~ msgstr "Introduce la URL de servidor personalizado"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Custom Server URL (optional):"
|
||||
#~ msgstr "URL de servidor personalizado (opcional):"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Server"
|
||||
#~ msgstr "Servidor"
|
||||
|
||||
#~ msgctxt "NetplayWait"
|
||||
#~ msgid "Ping:"
|
||||
#~ msgstr "Latencia (Ping):"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr "Creando servidor, espera... Puede tardar unos 30 segundos."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-05-25 08:28+0000\n"
|
||||
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French (France)\n"
|
||||
@@ -47,37 +47,37 @@ msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Faire un don (via GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:151
|
||||
#: src/ui/gui/appwindow.slint:164
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Jeu local"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:155
|
||||
#: src/ui/gui/appwindow.slint:168
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Configuration manette"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:159
|
||||
#: src/ui/gui/appwindow.slint:172
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:163
|
||||
#: src/ui/gui/appwindow.slint:176
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Triche"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:167
|
||||
#: src/ui/gui/appwindow.slint:180
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:171
|
||||
#: src/ui/gui/appwindow.slint:184
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Paramètres"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:175
|
||||
#: src/ui/gui/appwindow.slint:188
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "À propos"
|
||||
@@ -97,7 +97,7 @@ msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Effacer la triche pour toutes les ROMs"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:88
|
||||
#: src/ui/gui/cheat_page.slint:83
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "Aucune triche disponible"
|
||||
@@ -192,47 +192,42 @@ msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Netplay : créer une session"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:23
|
||||
#: src/ui/gui/netplay_create.slint:22
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nom de la session :"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:27
|
||||
#: src/ui/gui/netplay_create.slint:26
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Saisir le nom de la session"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:37
|
||||
#: src/ui/gui/netplay_create.slint:36
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Mot de passe (facultatif) :"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:41
|
||||
#: src/ui/gui/netplay_create.slint:40
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Saisir le mot de passe de la session"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:52
|
||||
#: src/ui/gui/netplay_create.slint:51
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Nom du joueur :"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:56
|
||||
#: src/ui/gui/netplay_create.slint:55
|
||||
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
|
||||
#: src/ui/gui/netplay_create.slint:82
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
@@ -240,21 +235,16 @@ msgstr ""
|
||||
"La triche est activée pour ce jeu. Elle sera synchronisée avec les autres "
|
||||
"joueurs."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:112
|
||||
#: src/ui/gui/netplay_create.slint:91
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Créer la session"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:132
|
||||
#: src/ui/gui/netplay_create.slint:107
|
||||
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"
|
||||
@@ -275,153 +265,164 @@ 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
|
||||
#: src/ui/gui/netplay_join.slint:42
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "Sélectionner une ROM"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:68
|
||||
#: src/ui/gui/netplay_join.slint:57
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Actualisation de la liste des sessions, veuillez patienter…"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
#: src/ui/gui/netplay_join.slint:62
|
||||
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
|
||||
#: src/ui/gui/netplay_join.slint:67
|
||||
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
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Nom de la session"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:86
|
||||
#: src/ui/gui/netplay_join.slint:74
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Nom du jeu"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:87
|
||||
#: src/ui/gui/netplay_join.slint:75
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Mot de passe"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:88
|
||||
#: src/ui/gui/netplay_join.slint:76
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Triche"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:102
|
||||
#: src/ui/gui/netplay_join.slint:90
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Actualiser la liste"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:119
|
||||
#: src/ui/gui/netplay_join.slint:105
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Rejoindre la session"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:132
|
||||
#: src/ui/gui/netplay_join.slint:117
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:37
|
||||
#: src/ui/gui/netplay_page.slint:31
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:45
|
||||
#: src/ui/gui/netplay_page.slint:39
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Créer une session"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:56
|
||||
#: src/ui/gui/netplay_page.slint:50
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Rejoindre une session"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:70
|
||||
#: src/ui/gui/netplay_page.slint:64
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Salon Discord Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:77
|
||||
#: src/ui/gui/netplay_page.slint:71
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Donner votre avis"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:15
|
||||
#: src/ui/gui/netplay_wait.slint:17
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Netplay : salle d'attente"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:27
|
||||
#: src/ui/gui/netplay_wait.slint:29
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nom de la session :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:33
|
||||
#: src/ui/gui/netplay_wait.slint:35
|
||||
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
|
||||
#: src/ui/gui/netplay_wait.slint:45
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Joueur 1 :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:52
|
||||
#: src/ui/gui/netplay_wait.slint:49
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Joueur 2 :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:58
|
||||
#: src/ui/gui/netplay_wait.slint:55
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Joueur 3 :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:62
|
||||
#: src/ui/gui/netplay_wait.slint:59
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Joueur 4 :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:76
|
||||
#: src/ui/gui/netplay_wait.slint:73
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Saisir un message"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:87
|
||||
#: src/ui/gui/netplay_wait.slint:84
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Envoyer"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:105
|
||||
#: src/ui/gui/netplay_wait.slint:94
|
||||
msgctxt "NetplayWait"
|
||||
msgid ""
|
||||
"Setting the input delay too low will cause the game to lag.\n"
|
||||
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
|
||||
"Alt+]."
|
||||
msgstr ""
|
||||
"Régler le délai d'entrée trop bas provoquera des ralentissements.\n"
|
||||
"Vous pouvez diminuer le délai d'entrée en jeu avec Alt+[ et l'augmenter avec "
|
||||
"Alt+]."
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:101
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Local Input Delay:"
|
||||
msgstr "Délai d'entrée local :"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:118
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Recommended: "
|
||||
msgstr "Recommandé : "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Lancer la partie"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:113
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Waiting for P2P mesh "
|
||||
msgstr "En attente du maillage P2P "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:135
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
@@ -486,71 +487,126 @@ msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Voir les jeux pris en charge"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:23
|
||||
#: src/ui/gui/settings_page.slint:25
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Paramètres"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:34
|
||||
#: src/ui/gui/settings_page.slint:32
|
||||
msgctxt "Settings"
|
||||
msgid "Game"
|
||||
msgstr "Jeu"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:38
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Résolution :"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:48
|
||||
#: src/ui/gui/settings_page.slint:51
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "Réduction SSAA"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:56
|
||||
#: src/ui/gui/settings_page.slint:59
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Mise à l'échelle par entiers"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:64
|
||||
#: src/ui/gui/settings_page.slint:67
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Plein écran"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:72
|
||||
#: src/ui/gui/settings_page.slint:75
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "Écran large (étiré)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:80
|
||||
#: src/ui/gui/settings_page.slint:83
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "Synchro verticale"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:88
|
||||
#: src/ui/gui/settings_page.slint:91
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "Appliquer le filtre CRT"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:100
|
||||
#: src/ui/gui/settings_page.slint:103
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "Surcharger le CPU N64 (risque de bugs)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:108
|
||||
#: src/ui/gui/settings_page.slint:111
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Désactiver l'Expansion Pak"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:116
|
||||
#: src/ui/gui/settings_page.slint:119
|
||||
msgctxt "Settings"
|
||||
msgid "Enable Rewind"
|
||||
msgstr "Activer le rembobinage"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:127
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "Émuler UNFLoader"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:124
|
||||
#: src/ui/gui/settings_page.slint:135
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "Émuler le VRU"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:144
|
||||
msgctxt "Settings"
|
||||
msgid "UI"
|
||||
msgstr "Interface"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:150
|
||||
msgctxt "Settings"
|
||||
msgid "Theme:"
|
||||
msgstr "Thème :"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "System"
|
||||
msgstr "Système"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Dark"
|
||||
msgstr "Sombre"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Light"
|
||||
msgstr "Clair"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "Que souhaitez-vous dire ?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Server:"
|
||||
#~ msgstr "Serveur :"
|
||||
|
||||
#~ msgctxt "CustomServerDialog"
|
||||
#~ msgid "Enter a custom server URL"
|
||||
#~ msgstr "Saisir l'URL personnalisée du serveur"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Custom Server URL (optional):"
|
||||
#~ msgstr "URL personnalisée du serveur (facultatif) :"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Server"
|
||||
#~ msgstr "Serveur"
|
||||
|
||||
#~ msgctxt "NetplayWait"
|
||||
#~ msgid "Ping:"
|
||||
#~ msgstr "Ping :"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
|
||||
+114
-82
@@ -2,7 +2,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2026-05-25 08:28+0000\n"
|
||||
"POT-Creation-Date: 2026-06-30 09:43+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"
|
||||
@@ -47,37 +47,37 @@ msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:151
|
||||
#: src/ui/gui/appwindow.slint:164
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:155
|
||||
#: src/ui/gui/appwindow.slint:168
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:159
|
||||
#: src/ui/gui/appwindow.slint:172
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:163
|
||||
#: src/ui/gui/appwindow.slint:176
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:167
|
||||
#: src/ui/gui/appwindow.slint:180
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:171
|
||||
#: src/ui/gui/appwindow.slint:184
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/appwindow.slint:175
|
||||
#: src/ui/gui/appwindow.slint:188
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr ""
|
||||
@@ -97,7 +97,7 @@ msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:88
|
||||
#: src/ui/gui/cheat_page.slint:83
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr ""
|
||||
@@ -192,67 +192,57 @@ msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:23
|
||||
#: src/ui/gui/netplay_create.slint:22
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:27
|
||||
#: src/ui/gui/netplay_create.slint:26
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:37
|
||||
#: src/ui/gui/netplay_create.slint:36
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:41
|
||||
#: src/ui/gui/netplay_create.slint:40
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:52
|
||||
#: src/ui/gui/netplay_create.slint:51
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:56
|
||||
#: src/ui/gui/netplay_create.slint:55
|
||||
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
|
||||
#: src/ui/gui/netplay_create.slint:82
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:112
|
||||
#: src/ui/gui/netplay_create.slint:91
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:132
|
||||
#: src/ui/gui/netplay_create.slint:107
|
||||
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"
|
||||
@@ -273,152 +263,159 @@ 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
|
||||
#: src/ui/gui/netplay_join.slint:42
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:68
|
||||
#: src/ui/gui/netplay_join.slint:57
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
#: src/ui/gui/netplay_join.slint:62
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:78
|
||||
#: src/ui/gui/netplay_join.slint:67
|
||||
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
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:86
|
||||
#: src/ui/gui/netplay_join.slint:74
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:87
|
||||
#: src/ui/gui/netplay_join.slint:75
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:88
|
||||
#: src/ui/gui/netplay_join.slint:76
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:102
|
||||
#: src/ui/gui/netplay_join.slint:90
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:119
|
||||
#: src/ui/gui/netplay_join.slint:105
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:132
|
||||
#: src/ui/gui/netplay_join.slint:117
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:37
|
||||
#: src/ui/gui/netplay_page.slint:31
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:45
|
||||
#: src/ui/gui/netplay_page.slint:39
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:56
|
||||
#: src/ui/gui/netplay_page.slint:50
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:70
|
||||
#: src/ui/gui/netplay_page.slint:64
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:77
|
||||
#: src/ui/gui/netplay_page.slint:71
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:15
|
||||
#: src/ui/gui/netplay_wait.slint:17
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:27
|
||||
#: src/ui/gui/netplay_wait.slint:29
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:33
|
||||
#: src/ui/gui/netplay_wait.slint:35
|
||||
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
|
||||
#: src/ui/gui/netplay_wait.slint:45
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:52
|
||||
#: src/ui/gui/netplay_wait.slint:49
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:58
|
||||
#: src/ui/gui/netplay_wait.slint:55
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:62
|
||||
#: src/ui/gui/netplay_wait.slint:59
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:76
|
||||
#: src/ui/gui/netplay_wait.slint:73
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:87
|
||||
#: src/ui/gui/netplay_wait.slint:84
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:105
|
||||
#: src/ui/gui/netplay_wait.slint:94
|
||||
msgctxt "NetplayWait"
|
||||
msgid ""
|
||||
"Setting the input delay too low will cause the game to lag.\nYou can lower "
|
||||
"the input delay in-game by pressing Alt+[ and raise it with Alt+]."
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:101
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Local Input Delay:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:118
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Recommended: "
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:113
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Waiting for P2P mesh "
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:135
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
@@ -483,66 +480,101 @@ msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:23
|
||||
#: src/ui/gui/settings_page.slint:25
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:34
|
||||
#: src/ui/gui/settings_page.slint:32
|
||||
msgctxt "Settings"
|
||||
msgid "Game"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:38
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:48
|
||||
#: src/ui/gui/settings_page.slint:51
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:56
|
||||
#: src/ui/gui/settings_page.slint:59
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:64
|
||||
#: src/ui/gui/settings_page.slint:67
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:72
|
||||
#: src/ui/gui/settings_page.slint:75
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:80
|
||||
#: src/ui/gui/settings_page.slint:83
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:88
|
||||
#: src/ui/gui/settings_page.slint:91
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:100
|
||||
#: src/ui/gui/settings_page.slint:103
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:108
|
||||
#: src/ui/gui/settings_page.slint:111
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:116
|
||||
#: src/ui/gui/settings_page.slint:119
|
||||
msgctxt "Settings"
|
||||
msgid "Enable Rewind"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:127
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:124
|
||||
#: src/ui/gui/settings_page.slint:135
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:144
|
||||
msgctxt "Settings"
|
||||
msgid "UI"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:150
|
||||
msgctxt "Settings"
|
||||
msgid "Theme:"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "System"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Dark"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Light"
|
||||
msgstr ""
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-05-25 08:28+0000\n"
|
||||
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Japanese\n"
|
||||
@@ -47,37 +47,37 @@ msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "GitHubで寄付"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:151
|
||||
#: src/ui/gui/appwindow.slint:164
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "ローカルゲーム"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:155
|
||||
#: src/ui/gui/appwindow.slint:168
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "コントローラー設定"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:159
|
||||
#: src/ui/gui/appwindow.slint:172
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "ネットプレイ"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:163
|
||||
#: src/ui/gui/appwindow.slint:176
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "チート"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:167
|
||||
#: src/ui/gui/appwindow.slint:180
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:171
|
||||
#: src/ui/gui/appwindow.slint:184
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "設定"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:175
|
||||
#: src/ui/gui/appwindow.slint:188
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "バージョン情報"
|
||||
@@ -97,7 +97,7 @@ msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "すべてのROMのチートをクリア"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:88
|
||||
#: src/ui/gui/cheat_page.slint:83
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "利用できるチートはありません"
|
||||
@@ -192,67 +192,57 @@ msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "ネットプレイ:セッション作成"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:23
|
||||
#: src/ui/gui/netplay_create.slint:22
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "セッション名:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:27
|
||||
#: src/ui/gui/netplay_create.slint:26
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "セッション名を入力"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:37
|
||||
#: src/ui/gui/netplay_create.slint:36
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "パスワード(任意):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:41
|
||||
#: src/ui/gui/netplay_create.slint:40
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "セッションのパスワードを入力"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:52
|
||||
#: src/ui/gui/netplay_create.slint:51
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "プレイヤー名:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:56
|
||||
#: src/ui/gui/netplay_create.slint:55
|
||||
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
|
||||
#: src/ui/gui/netplay_create.slint:82
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr "このゲームではチートが有効です。チートは他のプレイヤーと同期されます。"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:112
|
||||
#: src/ui/gui/netplay_create.slint:91
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "セッションを作成"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:132
|
||||
#: src/ui/gui/netplay_create.slint:107
|
||||
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"
|
||||
@@ -273,152 +263,162 @@ 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
|
||||
#: src/ui/gui/netplay_join.slint:42
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROMを選択"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:68
|
||||
#: src/ui/gui/netplay_join.slint:57
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "セッション一覧を更新しています。しばらくお待ちください…"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
#: src/ui/gui/netplay_join.slint:62
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr "下の一覧からセッションをクリックして選択"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:78
|
||||
#: src/ui/gui/netplay_join.slint:67
|
||||
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
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "セッション名"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:86
|
||||
#: src/ui/gui/netplay_join.slint:74
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "ゲーム名"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:87
|
||||
#: src/ui/gui/netplay_join.slint:75
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "パスワード"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:88
|
||||
#: src/ui/gui/netplay_join.slint:76
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "チート"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:102
|
||||
#: src/ui/gui/netplay_join.slint:90
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "セッション一覧を更新"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:119
|
||||
#: src/ui/gui/netplay_join.slint:105
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "セッションに参加"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:132
|
||||
#: src/ui/gui/netplay_join.slint:117
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "キャンセル"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:37
|
||||
#: src/ui/gui/netplay_page.slint:31
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "ネットプレイ"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:45
|
||||
#: src/ui/gui/netplay_page.slint:39
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "セッションを作成"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:56
|
||||
#: src/ui/gui/netplay_page.slint:50
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "セッションに参加"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:70
|
||||
#: src/ui/gui/netplay_page.slint:64
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "ネットプレイ用Discordチャンネル"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:77
|
||||
#: src/ui/gui/netplay_page.slint:71
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "フィードバックを送る"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:15
|
||||
#: src/ui/gui/netplay_wait.slint:17
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "ネットプレイ:待機室"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:27
|
||||
#: src/ui/gui/netplay_wait.slint:29
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "セッション名:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:33
|
||||
#: src/ui/gui/netplay_wait.slint:35
|
||||
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
|
||||
#: src/ui/gui/netplay_wait.slint:45
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "プレイヤー1:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:52
|
||||
#: src/ui/gui/netplay_wait.slint:49
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "プレイヤー2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:58
|
||||
#: src/ui/gui/netplay_wait.slint:55
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "プレイヤー3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:62
|
||||
#: src/ui/gui/netplay_wait.slint:59
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "プレイヤー4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:76
|
||||
#: src/ui/gui/netplay_wait.slint:73
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "チャットメッセージを入力"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:87
|
||||
#: src/ui/gui/netplay_wait.slint:84
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "送信"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:105
|
||||
#: src/ui/gui/netplay_wait.slint:94
|
||||
msgctxt "NetplayWait"
|
||||
msgid ""
|
||||
"Setting the input delay too low will cause the game to lag.\n"
|
||||
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
|
||||
"Alt+]."
|
||||
msgstr ""
|
||||
"入力遅延を低く設定しすぎるとゲームが遅延します。\n"
|
||||
"Alt+[ でゲーム内の入力遅延を下げ、Alt+] で上げることができます。"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:101
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Local Input Delay:"
|
||||
msgstr "ローカル入力遅延:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:118
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Recommended: "
|
||||
msgstr "推奨: "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "ゲーム開始"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:113
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Waiting for P2P mesh "
|
||||
msgstr "P2Pメッシュ接続待機中 "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:135
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "キャンセル"
|
||||
@@ -483,71 +483,126 @@ msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "対応ゲームを表示"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:23
|
||||
#: src/ui/gui/settings_page.slint:25
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "設定"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:34
|
||||
#: src/ui/gui/settings_page.slint:32
|
||||
msgctxt "Settings"
|
||||
msgid "Game"
|
||||
msgstr "ゲーム"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:38
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "解像度:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:48
|
||||
#: src/ui/gui/settings_page.slint:51
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "SSAAダウンスケール"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:56
|
||||
#: src/ui/gui/settings_page.slint:59
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "整数倍スケーリング"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:64
|
||||
#: src/ui/gui/settings_page.slint:67
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "全画面表示"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:72
|
||||
#: src/ui/gui/settings_page.slint:75
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "ワイド画面(伸ばす)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:80
|
||||
#: src/ui/gui/settings_page.slint:83
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "垂直同期(VSync)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:88
|
||||
#: src/ui/gui/settings_page.slint:91
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "CRTシェーダーを適用"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:100
|
||||
#: src/ui/gui/settings_page.slint:103
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "N64のCPUをオーバークロック(不具合の原因になることがあります)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:108
|
||||
#: src/ui/gui/settings_page.slint:111
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "拡張パックを無効にする"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:116
|
||||
#: src/ui/gui/settings_page.slint:119
|
||||
msgctxt "Settings"
|
||||
msgid "Enable Rewind"
|
||||
msgstr "巻き戻しを有効にする"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:127
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "UNFLoaderをエミュレート"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:124
|
||||
#: src/ui/gui/settings_page.slint:135
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "VRUをエミュレート"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:144
|
||||
msgctxt "Settings"
|
||||
msgid "UI"
|
||||
msgstr "UI"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:150
|
||||
msgctxt "Settings"
|
||||
msgid "Theme:"
|
||||
msgstr "テーマ:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "System"
|
||||
msgstr "システム"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Dark"
|
||||
msgstr "ダーク"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Light"
|
||||
msgstr "ライト"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "何と言いますか?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Server:"
|
||||
#~ msgstr "サーバー:"
|
||||
|
||||
#~ msgctxt "CustomServerDialog"
|
||||
#~ msgid "Enter a custom server URL"
|
||||
#~ msgstr "カスタムサーバーURLを入力"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Custom Server URL (optional):"
|
||||
#~ msgstr "カスタムサーバーURL(任意):"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Server"
|
||||
#~ msgstr "サーバー"
|
||||
|
||||
#~ msgctxt "NetplayWait"
|
||||
#~ msgid "Ping:"
|
||||
#~ msgstr "Ping:"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-05-25 08:28+0000\n"
|
||||
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch (Netherlands)\n"
|
||||
@@ -47,37 +47,37 @@ msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Doneren (via GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:151
|
||||
#: src/ui/gui/appwindow.slint:164
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Lokaal spel"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:155
|
||||
#: src/ui/gui/appwindow.slint:168
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Controllerconfiguratie"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:159
|
||||
#: src/ui/gui/appwindow.slint:172
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:163
|
||||
#: src/ui/gui/appwindow.slint:176
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Cheats"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:167
|
||||
#: src/ui/gui/appwindow.slint:180
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:171
|
||||
#: src/ui/gui/appwindow.slint:184
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Instellingen"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:175
|
||||
#: src/ui/gui/appwindow.slint:188
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "Over"
|
||||
@@ -97,7 +97,7 @@ msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Cheats wissen voor alle ROM's"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:88
|
||||
#: src/ui/gui/cheat_page.slint:83
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "Geen cheats beschikbaar"
|
||||
@@ -192,47 +192,42 @@ msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Netplay: sessie aanmaken"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:23
|
||||
#: src/ui/gui/netplay_create.slint:22
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Sessienaam:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:27
|
||||
#: src/ui/gui/netplay_create.slint:26
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Sessienaam invoeren"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:37
|
||||
#: src/ui/gui/netplay_create.slint:36
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Wachtwoord (optioneel):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:41
|
||||
#: src/ui/gui/netplay_create.slint:40
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Sessiewachtwoord invoeren"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:52
|
||||
#: src/ui/gui/netplay_create.slint:51
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Spelernaam:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:56
|
||||
#: src/ui/gui/netplay_create.slint:55
|
||||
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
|
||||
#: src/ui/gui/netplay_create.slint:82
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
@@ -240,21 +235,16 @@ msgstr ""
|
||||
"Voor dit spel zijn cheats ingeschakeld. Cheats worden gesynchroniseerd met "
|
||||
"andere spelers."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:112
|
||||
#: src/ui/gui/netplay_create.slint:91
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Sessie aanmaken"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:132
|
||||
#: src/ui/gui/netplay_create.slint:107
|
||||
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"
|
||||
@@ -275,152 +265,163 @@ 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
|
||||
#: src/ui/gui/netplay_join.slint:42
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "ROM selecteren"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:68
|
||||
#: src/ui/gui/netplay_join.slint:57
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Sessielijst wordt vernieuwd, even geduld…"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
#: src/ui/gui/netplay_join.slint:62
|
||||
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
|
||||
#: src/ui/gui/netplay_join.slint:67
|
||||
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
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Sessienaam"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:86
|
||||
#: src/ui/gui/netplay_join.slint:74
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Spelnaam"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:87
|
||||
#: src/ui/gui/netplay_join.slint:75
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Wachtwoord"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:88
|
||||
#: src/ui/gui/netplay_join.slint:76
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Cheats"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:102
|
||||
#: src/ui/gui/netplay_join.slint:90
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Sessielijst vernieuwen"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:119
|
||||
#: src/ui/gui/netplay_join.slint:105
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Deelnemen aan sessie"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:132
|
||||
#: src/ui/gui/netplay_join.slint:117
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuleren"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:37
|
||||
#: src/ui/gui/netplay_page.slint:31
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:45
|
||||
#: src/ui/gui/netplay_page.slint:39
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Sessie aanmaken"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:56
|
||||
#: src/ui/gui/netplay_page.slint:50
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Deelnemen aan sessie"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:70
|
||||
#: src/ui/gui/netplay_page.slint:64
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Netplay-Discordkanaal"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:77
|
||||
#: src/ui/gui/netplay_page.slint:71
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Feedback geven"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:15
|
||||
#: src/ui/gui/netplay_wait.slint:17
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Netplay: wachtruimte"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:27
|
||||
#: src/ui/gui/netplay_wait.slint:29
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Sessienaam:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:33
|
||||
#: src/ui/gui/netplay_wait.slint:35
|
||||
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
|
||||
#: src/ui/gui/netplay_wait.slint:45
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Speler 1:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:52
|
||||
#: src/ui/gui/netplay_wait.slint:49
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Speler 2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:58
|
||||
#: src/ui/gui/netplay_wait.slint:55
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Speler 3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:62
|
||||
#: src/ui/gui/netplay_wait.slint:59
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Speler 4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:76
|
||||
#: src/ui/gui/netplay_wait.slint:73
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Chatbericht invoeren"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:87
|
||||
#: src/ui/gui/netplay_wait.slint:84
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Bericht verzenden"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:105
|
||||
#: src/ui/gui/netplay_wait.slint:94
|
||||
msgctxt "NetplayWait"
|
||||
msgid ""
|
||||
"Setting the input delay too low will cause the game to lag.\n"
|
||||
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
|
||||
"Alt+]."
|
||||
msgstr ""
|
||||
"Een te lage invoervertraging zorgt ervoor dat het spel gaat haperen.\n"
|
||||
"Je kunt de invoervertraging in het spel verlagen met Alt+[ en verhogen met "
|
||||
"Alt+]."
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:101
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Local Input Delay:"
|
||||
msgstr "Lokale invoervertraging:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:118
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Recommended: "
|
||||
msgstr "Aanbevolen: "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Spel starten"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:113
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Waiting for P2P mesh "
|
||||
msgstr "Wachten op P2P-mesh "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:135
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuleren"
|
||||
@@ -485,71 +486,126 @@ msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Ondersteunde spellen bekijken"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:23
|
||||
#: src/ui/gui/settings_page.slint:25
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Instellingen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:34
|
||||
#: src/ui/gui/settings_page.slint:32
|
||||
msgctxt "Settings"
|
||||
msgid "Game"
|
||||
msgstr "Spel"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:38
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Resolutie:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:48
|
||||
#: src/ui/gui/settings_page.slint:51
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "SSAA-downscaling"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:56
|
||||
#: src/ui/gui/settings_page.slint:59
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Schalen in hele getallen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:64
|
||||
#: src/ui/gui/settings_page.slint:67
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Volledig scherm"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:72
|
||||
#: src/ui/gui/settings_page.slint:75
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "Breedbeeld (uitgerekt)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:80
|
||||
#: src/ui/gui/settings_page.slint:83
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "VSync"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:88
|
||||
#: src/ui/gui/settings_page.slint:91
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "CRT-shader toepassen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:100
|
||||
#: src/ui/gui/settings_page.slint:103
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "N64-CPU overklokken (kan bugs veroorzaken)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:108
|
||||
#: src/ui/gui/settings_page.slint:111
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Expansion Pak uitschakelen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:116
|
||||
#: src/ui/gui/settings_page.slint:119
|
||||
msgctxt "Settings"
|
||||
msgid "Enable Rewind"
|
||||
msgstr "Terugspoelen inschakelen"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:127
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "UNFLoader emuleren"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:124
|
||||
#: src/ui/gui/settings_page.slint:135
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "VRU emuleren"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:144
|
||||
msgctxt "Settings"
|
||||
msgid "UI"
|
||||
msgstr "Interface"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:150
|
||||
msgctxt "Settings"
|
||||
msgid "Theme:"
|
||||
msgstr "Thema:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "System"
|
||||
msgstr "Systeem"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Dark"
|
||||
msgstr "Donker"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Light"
|
||||
msgstr "Licht"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "Wat wil je zeggen?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Server:"
|
||||
#~ msgstr "Server:"
|
||||
|
||||
#~ msgctxt "CustomServerDialog"
|
||||
#~ msgid "Enter a custom server URL"
|
||||
#~ msgstr "Aangepaste server-URL invoeren"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Custom Server URL (optional):"
|
||||
#~ msgstr "Aangepaste server-URL (optioneel):"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Server"
|
||||
#~ msgstr "Server"
|
||||
|
||||
#~ msgctxt "NetplayWait"
|
||||
#~ msgid "Ping:"
|
||||
#~ msgstr "Ping:"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2026-05-25 08:28+0000\n"
|
||||
"POT-Creation-Date: 2026-06-30 09:43+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"
|
||||
@@ -10,7 +10,7 @@ msgstr ""
|
||||
"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"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
@@ -47,37 +47,37 @@ msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Doações (via GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:151
|
||||
#: src/ui/gui/appwindow.slint:164
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Jogo Local"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:155
|
||||
#: src/ui/gui/appwindow.slint:168
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Configurar Controle"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:159
|
||||
#: src/ui/gui/appwindow.slint:172
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:163
|
||||
#: src/ui/gui/appwindow.slint:176
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Trapaças"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:167
|
||||
#: src/ui/gui/appwindow.slint:180
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:171
|
||||
#: src/ui/gui/appwindow.slint:184
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Configurações"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:175
|
||||
#: src/ui/gui/appwindow.slint:188
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "Sobre"
|
||||
@@ -97,7 +97,7 @@ msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Limpar Trapaças para todas as ROMs"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:88
|
||||
#: src/ui/gui/cheat_page.slint:83
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "Sem trapaças disponíveis"
|
||||
@@ -180,7 +180,7 @@ msgstr "Abrir ROM"
|
||||
#: src/ui/gui/localgame_page.slint:37
|
||||
msgctxt "LocalGame"
|
||||
msgid "Open Saves Folder"
|
||||
msgstr "Abrir Partes de Saves"
|
||||
msgstr "Abrir Pasta de Saves"
|
||||
|
||||
#: src/ui/gui/localgame_page.slint:54
|
||||
msgctxt "LocalGame"
|
||||
@@ -192,47 +192,42 @@ msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Netplay: Criar Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:23
|
||||
#: src/ui/gui/netplay_create.slint:22
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nome da Sessão:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:27
|
||||
#: src/ui/gui/netplay_create.slint:26
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Insira nome da sessão:"
|
||||
msgstr "Insira nome da sessão"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:37
|
||||
#: src/ui/gui/netplay_create.slint:36
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Senha: (Opcional):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:41
|
||||
#: src/ui/gui/netplay_create.slint:40
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Insira a senha da sessão"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:52
|
||||
#: src/ui/gui/netplay_create.slint:51
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Nome de Jogador:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:56
|
||||
#: src/ui/gui/netplay_create.slint:55
|
||||
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
|
||||
#: src/ui/gui/netplay_create.slint:82
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
@@ -240,21 +235,16 @@ msgstr ""
|
||||
"Este jogo tem trapaças habilitadas. As trapaças serão sincronizadas com "
|
||||
"outros jogadores."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:112
|
||||
#: src/ui/gui/netplay_create.slint:91
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Criar Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:132
|
||||
#: src/ui/gui/netplay_create.slint:107
|
||||
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"
|
||||
@@ -275,153 +265,164 @@ 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
|
||||
#: src/ui/gui/netplay_join.slint:42
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "Selecionar ROM"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:68
|
||||
#: src/ui/gui/netplay_join.slint:57
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Recarregando lista de sessões, por favor aguarde..."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
#: src/ui/gui/netplay_join.slint:62
|
||||
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
|
||||
#: src/ui/gui/netplay_join.slint:67
|
||||
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
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Nome da Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:86
|
||||
#: src/ui/gui/netplay_join.slint:74
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Nome do Jogo"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:87
|
||||
#: src/ui/gui/netplay_join.slint:75
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Senha"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:88
|
||||
#: src/ui/gui/netplay_join.slint:76
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Trapaças"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:102
|
||||
#: src/ui/gui/netplay_join.slint:90
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Recarregar Lista de Sessões"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:119
|
||||
#: src/ui/gui/netplay_join.slint:105
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Entrar na Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:132
|
||||
#: src/ui/gui/netplay_join.slint:117
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:37
|
||||
#: src/ui/gui/netplay_page.slint:31
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Netplay"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:45
|
||||
#: src/ui/gui/netplay_page.slint:39
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Criar Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:56
|
||||
#: src/ui/gui/netplay_page.slint:50
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Entrar na Sessão"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:70
|
||||
#: src/ui/gui/netplay_page.slint:64
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Canal de Netplay do Discord"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:77
|
||||
#: src/ui/gui/netplay_page.slint:71
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Fornecer Feedback"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:15
|
||||
#: src/ui/gui/netplay_wait.slint:17
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Netplay: Aguardando Sala"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:27
|
||||
#: src/ui/gui/netplay_wait.slint:29
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Nome da Sessão:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:33
|
||||
#: src/ui/gui/netplay_wait.slint:35
|
||||
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
|
||||
#: src/ui/gui/netplay_wait.slint:45
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Jogador 1"
|
||||
msgstr "Jogador 1:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:52
|
||||
#: src/ui/gui/netplay_wait.slint:49
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Jogador 2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:58
|
||||
#: src/ui/gui/netplay_wait.slint:55
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Jogador 3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:62
|
||||
#: src/ui/gui/netplay_wait.slint:59
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Jogador 4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:76
|
||||
#: src/ui/gui/netplay_wait.slint:73
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Escrever mensagem no chat"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:87
|
||||
#: src/ui/gui/netplay_wait.slint:84
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Enviar Mensagem"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:105
|
||||
#: src/ui/gui/netplay_wait.slint:94
|
||||
msgctxt "NetplayWait"
|
||||
msgid ""
|
||||
"Setting the input delay too low will cause the game to lag.\n"
|
||||
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
|
||||
"Alt+]."
|
||||
msgstr ""
|
||||
"Definir o atraso de entrada muito baixo causará lag no jogo.\n"
|
||||
"Você pode diminuir o atraso de entrada no jogo pressionando Alt+[ e aumentá-"
|
||||
"lo com Alt+]."
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:101
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Local Input Delay:"
|
||||
msgstr "Atraso de entrada local:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:118
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Recommended: "
|
||||
msgstr "Recomendado: "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Iniciar jogo"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:113
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Waiting for P2P mesh "
|
||||
msgstr "Aguardando malha P2P "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:135
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
@@ -486,71 +487,126 @@ msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Ver Jogos com Suporte"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:23
|
||||
#: src/ui/gui/settings_page.slint:25
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Configurações"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:34
|
||||
#: src/ui/gui/settings_page.slint:32
|
||||
msgctxt "Settings"
|
||||
msgid "Game"
|
||||
msgstr "Jogo"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:38
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Resolução:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:48
|
||||
#: src/ui/gui/settings_page.slint:51
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "Redução de SSAA"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:56
|
||||
#: src/ui/gui/settings_page.slint:59
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Proporção Íntegra"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:64
|
||||
#: src/ui/gui/settings_page.slint:67
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Tela Cheia"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:72
|
||||
#: src/ui/gui/settings_page.slint:75
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "Widescreen (esticar)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:80
|
||||
#: src/ui/gui/settings_page.slint:83
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "VSync"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:88
|
||||
#: src/ui/gui/settings_page.slint:91
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "Aplicar filtro de CRT"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:100
|
||||
#: src/ui/gui/settings_page.slint:103
|
||||
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
|
||||
#: src/ui/gui/settings_page.slint:111
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Desabilitar Expansion Pak"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:116
|
||||
#: src/ui/gui/settings_page.slint:119
|
||||
msgctxt "Settings"
|
||||
msgid "Enable Rewind"
|
||||
msgstr "Habilitar rebobinamento"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:127
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "Emular UNFLoader"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:124
|
||||
#: src/ui/gui/settings_page.slint:135
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "Emular VRU"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:144
|
||||
msgctxt "Settings"
|
||||
msgid "UI"
|
||||
msgstr "Interface"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:150
|
||||
msgctxt "Settings"
|
||||
msgid "Theme:"
|
||||
msgstr "Tema:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "System"
|
||||
msgstr "Sistema"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Dark"
|
||||
msgstr "Escuro"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Light"
|
||||
msgstr "Claro"
|
||||
|
||||
#: 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 "Server:"
|
||||
#~ msgstr "Servidor:"
|
||||
|
||||
#~ msgctxt "CustomServerDialog"
|
||||
#~ msgid "Enter a custom server URL"
|
||||
#~ msgstr "Insira a URL personalizada do servidor"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Custom Server URL (optional):"
|
||||
#~ msgstr "URL Personalizada de Servidor (opcional):"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Server"
|
||||
#~ msgstr "Servidor"
|
||||
|
||||
#~ msgctxt "NetplayWait"
|
||||
#~ msgid "Ping:"
|
||||
#~ msgstr "Ping:"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gopher64\n"
|
||||
"POT-Creation-Date: 2026-05-25 08:28+0000\n"
|
||||
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
|
||||
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Russian\n"
|
||||
@@ -10,7 +10,8 @@ msgstr ""
|
||||
"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"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
|
||||
#: src/ui/gui/about_page.slint:17
|
||||
msgctxt "NewVersionButton"
|
||||
@@ -47,37 +48,37 @@ msgctxt "About"
|
||||
msgid "Donate (via GitHub)"
|
||||
msgstr "Пожертвовать (через GitHub)"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:151
|
||||
#: src/ui/gui/appwindow.slint:164
|
||||
msgctxt "Menu"
|
||||
msgid "Local Game"
|
||||
msgstr "Локальная игра"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:155
|
||||
#: src/ui/gui/appwindow.slint:168
|
||||
msgctxt "Menu"
|
||||
msgid "Controller Config"
|
||||
msgstr "Настройка контроллера"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:159
|
||||
#: src/ui/gui/appwindow.slint:172
|
||||
msgctxt "Menu"
|
||||
msgid "Netplay"
|
||||
msgstr "Сетевая игра"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:163
|
||||
#: src/ui/gui/appwindow.slint:176
|
||||
msgctxt "Menu"
|
||||
msgid "Cheats"
|
||||
msgstr "Читы"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:167
|
||||
#: src/ui/gui/appwindow.slint:180
|
||||
msgctxt "Menu"
|
||||
msgid "RetroAchievements"
|
||||
msgstr "RetroAchievements"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:171
|
||||
#: src/ui/gui/appwindow.slint:184
|
||||
msgctxt "Menu"
|
||||
msgid "Settings"
|
||||
msgstr "Настройки"
|
||||
|
||||
#: src/ui/gui/appwindow.slint:175
|
||||
#: src/ui/gui/appwindow.slint:188
|
||||
msgctxt "Menu"
|
||||
msgid "About"
|
||||
msgstr "О программе"
|
||||
@@ -97,7 +98,7 @@ msgctxt "Cheats"
|
||||
msgid "Clear Cheats For All ROMs"
|
||||
msgstr "Очистить читы для всех ROMов"
|
||||
|
||||
#: src/ui/gui/cheat_page.slint:88
|
||||
#: src/ui/gui/cheat_page.slint:83
|
||||
msgctxt "Cheats"
|
||||
msgid "No Cheats Available"
|
||||
msgstr "Читы недоступны"
|
||||
@@ -192,68 +193,58 @@ msgctxt "NetplayCreate"
|
||||
msgid "Netplay: Create Session"
|
||||
msgstr "Сетевая игра: Создать сессию"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:23
|
||||
#: src/ui/gui/netplay_create.slint:22
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Session Name:"
|
||||
msgstr "Имя сессии:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:27
|
||||
#: src/ui/gui/netplay_create.slint:26
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session name"
|
||||
msgstr "Введите имя сессии"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:37
|
||||
#: src/ui/gui/netplay_create.slint:36
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Password (Optional):"
|
||||
msgstr "Пароль (необязательно):"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:41
|
||||
#: src/ui/gui/netplay_create.slint:40
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Enter session password"
|
||||
msgstr "Введите пароль сессии"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:52
|
||||
#: src/ui/gui/netplay_create.slint:51
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Player Name:"
|
||||
msgstr "Имя игрока:"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:56
|
||||
#: src/ui/gui/netplay_create.slint:55
|
||||
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
|
||||
#: src/ui/gui/netplay_create.slint:82
|
||||
msgctxt "NetplayCreate"
|
||||
msgid ""
|
||||
"This game has cheats enabled. Cheats will be synchronized with other players."
|
||||
msgstr ""
|
||||
"Для этой игры включены читы. Читы будут синхронизированы с другими игроками."
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:112
|
||||
#: src/ui/gui/netplay_create.slint:91
|
||||
msgctxt "NetplayCreate"
|
||||
msgid "Create Session"
|
||||
msgstr "Создать сессию"
|
||||
|
||||
#: src/ui/gui/netplay_create.slint:132
|
||||
#: src/ui/gui/netplay_create.slint:107
|
||||
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"
|
||||
@@ -274,152 +265,163 @@ 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
|
||||
#: src/ui/gui/netplay_join.slint:42
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Select ROM"
|
||||
msgstr "Выбрать ROM"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:68
|
||||
#: src/ui/gui/netplay_join.slint:57
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refreshing session list, please wait..."
|
||||
msgstr "Обновление списка сессий, пожалуйста, подождите..."
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
#: src/ui/gui/netplay_join.slint:62
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Click on a session below to select it"
|
||||
msgstr "Нажмите на сессию ниже, чтобы выбрать её"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:78
|
||||
#: src/ui/gui/netplay_join.slint:67
|
||||
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
|
||||
#: src/ui/gui/netplay_join.slint:73
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Session Name"
|
||||
msgstr "Имя сессии"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:86
|
||||
#: src/ui/gui/netplay_join.slint:74
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Game Name"
|
||||
msgstr "Название игры"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:87
|
||||
#: src/ui/gui/netplay_join.slint:75
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Password"
|
||||
msgstr "Пароль"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:88
|
||||
#: src/ui/gui/netplay_join.slint:76
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cheats"
|
||||
msgstr "Читы"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:102
|
||||
#: src/ui/gui/netplay_join.slint:90
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Refresh Session List"
|
||||
msgstr "Обновить список сессий"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:119
|
||||
#: src/ui/gui/netplay_join.slint:105
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Join Session"
|
||||
msgstr "Присоединиться к сессии"
|
||||
|
||||
#: src/ui/gui/netplay_join.slint:132
|
||||
#: src/ui/gui/netplay_join.slint:117
|
||||
msgctxt "NetplayJoin"
|
||||
msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:37
|
||||
#: src/ui/gui/netplay_page.slint:31
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay"
|
||||
msgstr "Сетевая игра"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:45
|
||||
#: src/ui/gui/netplay_page.slint:39
|
||||
msgctxt "Netplay"
|
||||
msgid "Create Session"
|
||||
msgstr "Создать сессию"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:56
|
||||
#: src/ui/gui/netplay_page.slint:50
|
||||
msgctxt "Netplay"
|
||||
msgid "Join Session"
|
||||
msgstr "Присоединиться к сессии"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:70
|
||||
#: src/ui/gui/netplay_page.slint:64
|
||||
msgctxt "Netplay"
|
||||
msgid "Netplay Discord Channel"
|
||||
msgstr "Канал Discord для сетевой игры"
|
||||
|
||||
#: src/ui/gui/netplay_page.slint:77
|
||||
#: src/ui/gui/netplay_page.slint:71
|
||||
msgctxt "Netplay"
|
||||
msgid "Provide Feedback"
|
||||
msgstr "Оставить отзыв"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:15
|
||||
#: src/ui/gui/netplay_wait.slint:17
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Netplay: Waiting Room"
|
||||
msgstr "Сетевая игра: Комната ожидания"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:27
|
||||
#: src/ui/gui/netplay_wait.slint:29
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Session Name:"
|
||||
msgstr "Имя сессии:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:33
|
||||
#: src/ui/gui/netplay_wait.slint:35
|
||||
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
|
||||
#: src/ui/gui/netplay_wait.slint:45
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 1:"
|
||||
msgstr "Игрок 1:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:52
|
||||
#: src/ui/gui/netplay_wait.slint:49
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 2:"
|
||||
msgstr "Игрок 2:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:58
|
||||
#: src/ui/gui/netplay_wait.slint:55
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 3:"
|
||||
msgstr "Игрок 3:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:62
|
||||
#: src/ui/gui/netplay_wait.slint:59
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Player 4:"
|
||||
msgstr "Игрок 4:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:76
|
||||
#: src/ui/gui/netplay_wait.slint:73
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Enter chat message"
|
||||
msgstr "Введите сообщение чата"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:87
|
||||
#: src/ui/gui/netplay_wait.slint:84
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Send Message"
|
||||
msgstr "Отправить сообщение"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:105
|
||||
#: src/ui/gui/netplay_wait.slint:94
|
||||
msgctxt "NetplayWait"
|
||||
msgid ""
|
||||
"Setting the input delay too low will cause the game to lag.\n"
|
||||
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
|
||||
"Alt+]."
|
||||
msgstr ""
|
||||
"Слишком низкая задержка ввода вызовет лаги в игре.\n"
|
||||
"Вы можете уменьшить задержку ввода в игре, нажав Alt+[, и увеличить её с "
|
||||
"помощью Alt+]."
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:101
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Local Input Delay:"
|
||||
msgstr "Локальная задержка ввода:"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:118
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Recommended: "
|
||||
msgstr "Рекомендуется: "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Start Game"
|
||||
msgstr "Начать игру"
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:113
|
||||
#: src/ui/gui/netplay_wait.slint:127
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Waiting for P2P mesh "
|
||||
msgstr "Ожидание P2P-сети "
|
||||
|
||||
#: src/ui/gui/netplay_wait.slint:135
|
||||
msgctxt "NetplayWait"
|
||||
msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
@@ -484,71 +486,126 @@ msgctxt "RetroAchievements"
|
||||
msgid "View Supported Games"
|
||||
msgstr "Просмотреть поддерживаемые игры"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:23
|
||||
#: src/ui/gui/settings_page.slint:25
|
||||
msgctxt "Settings"
|
||||
msgid "Settings"
|
||||
msgstr "Настройки"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:34
|
||||
#: src/ui/gui/settings_page.slint:32
|
||||
msgctxt "Settings"
|
||||
msgid "Game"
|
||||
msgstr "Игра"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:38
|
||||
msgctxt "Settings"
|
||||
msgid "Resolution:"
|
||||
msgstr "Разрешение:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:48
|
||||
#: src/ui/gui/settings_page.slint:51
|
||||
msgctxt "Settings"
|
||||
msgid "SSAA downscaling"
|
||||
msgstr "SSAA даунскейлинг"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:56
|
||||
#: src/ui/gui/settings_page.slint:59
|
||||
msgctxt "Settings"
|
||||
msgid "Integer Scaling"
|
||||
msgstr "Целочисленное масштабирование"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:64
|
||||
#: src/ui/gui/settings_page.slint:67
|
||||
msgctxt "Settings"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Полноэкранный режим"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:72
|
||||
#: src/ui/gui/settings_page.slint:75
|
||||
msgctxt "Settings"
|
||||
msgid "Widescreen (stretch)"
|
||||
msgstr "Широкоэкранный режим (растянуто)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:80
|
||||
#: src/ui/gui/settings_page.slint:83
|
||||
msgctxt "Settings"
|
||||
msgid "VSync"
|
||||
msgstr "Вертикальная синхронизация"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:88
|
||||
#: src/ui/gui/settings_page.slint:91
|
||||
msgctxt "Settings"
|
||||
msgid "Apply CRT shader"
|
||||
msgstr "Применить шейдер ЭЛТ"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:100
|
||||
#: src/ui/gui/settings_page.slint:103
|
||||
msgctxt "Settings"
|
||||
msgid "Overclock N64 CPU (may cause bugs)"
|
||||
msgstr "Разгон процессора N64 (может вызвать ошибки)"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:108
|
||||
#: src/ui/gui/settings_page.slint:111
|
||||
msgctxt "Settings"
|
||||
msgid "Disable Expansion Pak"
|
||||
msgstr "Отключить Expansion Pak"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:116
|
||||
#: src/ui/gui/settings_page.slint:119
|
||||
msgctxt "Settings"
|
||||
msgid "Enable Rewind"
|
||||
msgstr "Включить перемотку назад"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:127
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate UNFLoader"
|
||||
msgstr "Эмулировать UNFLoader"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:124
|
||||
#: src/ui/gui/settings_page.slint:135
|
||||
msgctxt "Settings"
|
||||
msgid "Emulate VRU"
|
||||
msgstr "Эмулировать VRU"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:144
|
||||
msgctxt "Settings"
|
||||
msgid "UI"
|
||||
msgstr "Интерфейс"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:150
|
||||
msgctxt "Settings"
|
||||
msgid "Theme:"
|
||||
msgstr "Тема:"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "System"
|
||||
msgstr "Система"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Dark"
|
||||
msgstr "Тёмная"
|
||||
|
||||
#: src/ui/gui/settings_page.slint:154
|
||||
msgctxt "Settings"
|
||||
msgid "Light"
|
||||
msgstr "Светлая"
|
||||
|
||||
#: src/ui/gui/vru_dialog.slint:7
|
||||
msgctxt "VruDialog"
|
||||
msgid "What Would You Like To Say?"
|
||||
msgstr "Что вы хотите сказать?"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Server:"
|
||||
#~ msgstr "Сервер:"
|
||||
|
||||
#~ msgctxt "CustomServerDialog"
|
||||
#~ msgid "Enter a custom server URL"
|
||||
#~ msgstr "Введите URL пользовательского сервера"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Custom Server URL (optional):"
|
||||
#~ msgstr "URL пользовательского сервера (необязательно):"
|
||||
|
||||
#~ msgctxt "NetplayJoin"
|
||||
#~ msgid "Server"
|
||||
#~ msgstr "Сервер"
|
||||
|
||||
#~ msgctxt "NetplayWait"
|
||||
#~ msgid "Ping:"
|
||||
#~ msgstr "Пинг:"
|
||||
|
||||
#~ msgctxt "NetplayCreate"
|
||||
#~ msgid "Creating server, please wait...This may take about 30 seconds."
|
||||
#~ msgstr ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 MiB |
+102
-40
@@ -59,6 +59,7 @@ enum user_event_codes {
|
||||
USER_EVENT_LOAD_STATE = 2,
|
||||
USER_EVENT_EXIT_GAME = 3,
|
||||
USER_EVENT_FAST_FORWARD = 4,
|
||||
USER_EVENT_LOAD_REWIND = 5,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
@@ -82,6 +83,9 @@ typedef struct {
|
||||
FrameBufferInfo frame_buffer_info;
|
||||
} RDP_DEVICE;
|
||||
|
||||
static void *g_tmem = nullptr;
|
||||
static void *g_hidden_rdram = nullptr;
|
||||
|
||||
typedef struct {
|
||||
uint32_t fps;
|
||||
uint32_t vis;
|
||||
@@ -189,6 +193,12 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
|
||||
user_event.user.code = USER_EVENT_SAVE_STATE;
|
||||
SDL_PushEvent(&user_event);
|
||||
break;
|
||||
case SDL_SCANCODE_F6:
|
||||
SDL_zero(user_event);
|
||||
user_event.type = SDL_EVENT_USER;
|
||||
user_event.user.code = USER_EVENT_LOAD_REWIND;
|
||||
SDL_PushEvent(&user_event);
|
||||
break;
|
||||
case SDL_SCANCODE_F7:
|
||||
SDL_zero(user_event);
|
||||
user_event.type = SDL_EVENT_USER;
|
||||
@@ -211,10 +221,18 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
|
||||
callback.reset_game = true;
|
||||
break;
|
||||
case SDL_SCANCODE_LEFTBRACKET:
|
||||
callback.lower_volume = true;
|
||||
if (event->key.mod & SDL_KMOD_ALT) {
|
||||
callback.decrease_input_delay = true;
|
||||
} else {
|
||||
callback.lower_volume = true;
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_RIGHTBRACKET:
|
||||
callback.raise_volume = true;
|
||||
if (event->key.mod & SDL_KMOD_ALT) {
|
||||
callback.increase_input_delay = true;
|
||||
} else {
|
||||
callback.raise_volume = true;
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_SLASH:
|
||||
callback.frame_advance = true;
|
||||
@@ -244,6 +262,9 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
|
||||
case USER_EVENT_SAVE_STATE:
|
||||
callback.save_state = true;
|
||||
break;
|
||||
case USER_EVENT_LOAD_REWIND:
|
||||
callback.load_rewind = true;
|
||||
break;
|
||||
case USER_EVENT_LOAD_STATE:
|
||||
callback.load_state = true;
|
||||
break;
|
||||
@@ -285,8 +306,16 @@ JoystickEvent get_joystick_event() {
|
||||
return joystick_event;
|
||||
}
|
||||
|
||||
void rdp_idle() {
|
||||
processor->idle();
|
||||
sync_signal = 0;
|
||||
rdram_dirty.assign(gfx_info.RDRAM_SIZE >> 3, false);
|
||||
}
|
||||
|
||||
static void rdp_new_processor() {
|
||||
RDP::CommandProcessorFlags flags = 0;
|
||||
RDP::CommandProcessorFlags flags =
|
||||
RDP::COMMAND_PROCESSOR_FLAG_HOST_VISIBLE_HIDDEN_RDRAM_BIT |
|
||||
RDP::COMMAND_PROCESSOR_FLAG_HOST_VISIBLE_TMEM_BIT;
|
||||
|
||||
if (gfx_info.upscale == 2) {
|
||||
flags |= RDP::COMMAND_PROCESSOR_FLAG_SUPER_SAMPLED_DITHER_BIT;
|
||||
@@ -304,6 +333,17 @@ static void rdp_new_processor() {
|
||||
processor = new RDP::CommandProcessor(wsi->get_device(), gfx_info.RDRAM, 0,
|
||||
gfx_info.RDRAM_SIZE,
|
||||
gfx_info.RDRAM_SIZE / 2, flags);
|
||||
g_tmem = processor->get_tmem();
|
||||
if (!g_tmem) {
|
||||
LOGE("Failed to get tmem\n");
|
||||
}
|
||||
g_hidden_rdram = processor->begin_read_hidden_rdram();
|
||||
if (!g_hidden_rdram) {
|
||||
LOGE("Failed to get hidden_rdram\n");
|
||||
}
|
||||
|
||||
sync_signal = 0;
|
||||
rdram_dirty.assign(gfx_info.RDRAM_SIZE >> 3, false);
|
||||
}
|
||||
|
||||
static ImageHandle create_message_image(Vulkan::Device &device, int width,
|
||||
@@ -330,12 +370,13 @@ static ImageHandle create_message_image(Vulkan::Device &device, int width,
|
||||
void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
|
||||
size_t font_size, uint32_t save_state_slot) {
|
||||
memset(&rdp_device, 0, sizeof(RDP_DEVICE));
|
||||
memset(&callback, 0, sizeof(CALL_BACK));
|
||||
|
||||
window = (SDL_Window *)_window;
|
||||
SDL_SyncWindow(window);
|
||||
bool result = SDL_AddEventWatch(sdl_event_filter, nullptr);
|
||||
if (!result) {
|
||||
printf("Could not add event watch.\n");
|
||||
LOGE("Could not add event watch.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -405,14 +446,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;
|
||||
|
||||
g_tmem = nullptr;
|
||||
g_hidden_rdram = nullptr;
|
||||
|
||||
messages = std::queue<Message>();
|
||||
achievement_challenge_indicator_image = Vulkan::ImageHandle();
|
||||
achievement_progress_indicator_image = Vulkan::ImageHandle();
|
||||
@@ -581,10 +622,10 @@ static void render_frame(Vulkan::Device &device) {
|
||||
cmd->set_depth_test(false, false);
|
||||
cmd->set_cull_mode(VK_CULL_MODE_NONE);
|
||||
|
||||
VkViewport vp = cmd->get_viewport();
|
||||
// If we don't have an image, we just get a cleared screen in the render
|
||||
// pass.
|
||||
if (image) {
|
||||
VkViewport vp = cmd->get_viewport();
|
||||
calculate_viewport(&vp.x, &vp.y, &vp.width, &vp.height,
|
||||
image->get_height() / gfx_info.upscale);
|
||||
|
||||
@@ -605,33 +646,32 @@ static void render_frame(Vulkan::Device &device) {
|
||||
// The vertices are constants in the shader.
|
||||
// Draws fullscreen quad using oversized triangle.
|
||||
cmd->draw(3);
|
||||
|
||||
if (!messages.empty()) {
|
||||
Message *message = &messages.front();
|
||||
if (!message->image) {
|
||||
message->image = create_message_image(device, vp.width, message_font,
|
||||
message->message.c_str());
|
||||
SDL_AddTimer(message->milliseconds, pop_message_callback, NULL);
|
||||
}
|
||||
cmd->set_texture(0, 0, message->image->get_view(),
|
||||
Vulkan::StockSampler::NearestClamp);
|
||||
vp.x = floor(vp.x + (vp.width - message->image->get_width()) / 2);
|
||||
vp.y = vp.y + vp.height - message->image->get_height();
|
||||
vp.height = message->image->get_height();
|
||||
vp.width = message->image->get_width();
|
||||
cmd->set_viewport(vp);
|
||||
|
||||
cmd->draw(3);
|
||||
} else if (achievement_progress_indicator_image) {
|
||||
draw_indicator(cmd, achievement_progress_indicator_image, vp);
|
||||
} else if (achievement_challenge_indicator_image &&
|
||||
display_challenge_indicator) {
|
||||
draw_indicator(cmd, achievement_challenge_indicator_image, vp);
|
||||
}
|
||||
if (!messages.empty()) {
|
||||
Message *message = &messages.front();
|
||||
if (!message->image) {
|
||||
message->image = create_message_image(device, vp.width, message_font,
|
||||
message->message.c_str());
|
||||
SDL_AddTimer(message->milliseconds, pop_message_callback, NULL);
|
||||
}
|
||||
cmd->set_texture(0, 0, message->image->get_view(),
|
||||
Vulkan::StockSampler::NearestClamp);
|
||||
vp.x = floor(vp.x + (vp.width - message->image->get_width()) / 2);
|
||||
vp.y = vp.y + vp.height - message->image->get_height();
|
||||
vp.height = message->image->get_height();
|
||||
vp.width = message->image->get_width();
|
||||
cmd->set_viewport(vp);
|
||||
|
||||
if (messages.empty() && display_fps && fps_image) {
|
||||
draw_fps(cmd, vp);
|
||||
}
|
||||
cmd->draw(3);
|
||||
} else if (achievement_progress_indicator_image) {
|
||||
draw_indicator(cmd, achievement_progress_indicator_image, vp);
|
||||
} else if (achievement_challenge_indicator_image &&
|
||||
display_challenge_indicator) {
|
||||
draw_indicator(cmd, achievement_challenge_indicator_image, vp);
|
||||
}
|
||||
|
||||
if (messages.empty() && display_fps && fps_image) {
|
||||
draw_fps(cmd, vp);
|
||||
}
|
||||
|
||||
cmd->end_render_pass();
|
||||
@@ -652,17 +692,23 @@ void rdp_update_screen() {
|
||||
if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED) {
|
||||
return;
|
||||
}
|
||||
wsi->end_frame();
|
||||
if (!wsi->end_frame()) {
|
||||
LOGE("End frame failed\n");
|
||||
SDL_PumpEvents(); // For Android to trigger pause event
|
||||
}
|
||||
wsi->begin_frame();
|
||||
}
|
||||
|
||||
CALL_BACK rdp_check_callback() {
|
||||
CALL_BACK return_value = callback;
|
||||
callback.save_state = false;
|
||||
callback.load_rewind = false;
|
||||
callback.load_state = false;
|
||||
callback.reset_game = false;
|
||||
callback.lower_volume = false;
|
||||
callback.raise_volume = false;
|
||||
callback.decrease_input_delay = false;
|
||||
callback.increase_input_delay = false;
|
||||
callback.frame_advance = false;
|
||||
return return_value;
|
||||
}
|
||||
@@ -688,19 +734,35 @@ void rdp_check_framebuffers(uint32_t address, uint32_t length) {
|
||||
}
|
||||
}
|
||||
|
||||
size_t rdp_state_size() { return sizeof(RDP_DEVICE); }
|
||||
size_t rdp_state_size() {
|
||||
return sizeof(RDP_DEVICE) + 0x1000 + processor->get_hidden_rdram_size();
|
||||
}
|
||||
|
||||
void rdp_save_state(uint8_t *state) {
|
||||
processor->wait_for_timeline(processor->signal_timeline());
|
||||
memcpy(state, &rdp_device, sizeof(RDP_DEVICE));
|
||||
|
||||
if (g_tmem) {
|
||||
memcpy(state + sizeof(RDP_DEVICE), g_tmem, 0x1000);
|
||||
}
|
||||
|
||||
if (g_hidden_rdram) {
|
||||
memcpy(state + sizeof(RDP_DEVICE) + 0x1000, g_hidden_rdram,
|
||||
processor->get_hidden_rdram_size());
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
if (g_tmem) {
|
||||
memcpy(g_tmem, state + sizeof(RDP_DEVICE), 0x1000);
|
||||
}
|
||||
|
||||
if (g_hidden_rdram) {
|
||||
memcpy(g_hidden_rdram, state + sizeof(RDP_DEVICE) + 0x1000,
|
||||
processor->get_hidden_rdram_size());
|
||||
}
|
||||
}
|
||||
|
||||
static void push_onscreen_message(void *data) {
|
||||
@@ -728,7 +790,7 @@ uint32_t pixel_size(uint32_t pixel_type, uint32_t area) {
|
||||
case 3:
|
||||
return area * 4;
|
||||
default:
|
||||
printf("Invalid pixel size: %u\n", pixel_type);
|
||||
LOGE("Invalid pixel size: %u\n", pixel_type);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,13 @@ typedef struct {
|
||||
bool emu_running;
|
||||
bool save_state;
|
||||
bool load_state;
|
||||
bool load_rewind;
|
||||
bool reset_game;
|
||||
bool enable_speedlimiter;
|
||||
bool lower_volume;
|
||||
bool raise_volume;
|
||||
bool decrease_input_delay;
|
||||
bool increase_input_delay;
|
||||
bool paused;
|
||||
bool frame_advance;
|
||||
uint32_t save_state_slot;
|
||||
@@ -57,6 +60,7 @@ void rdp_update_screen();
|
||||
void rdp_render_frame();
|
||||
CALL_BACK rdp_check_callback();
|
||||
uint64_t rdp_process_commands();
|
||||
void rdp_idle();
|
||||
void rdp_onscreen_message(const char *message, MESSAGE_LENGTH milliseconds);
|
||||
void rdp_check_framebuffers(uint32_t address, uint32_t length);
|
||||
size_t rdp_state_size();
|
||||
|
||||
@@ -46,7 +46,7 @@ uint32_t SDL_WSIPlatform::get_surface_height() {
|
||||
|
||||
bool SDL_WSIPlatform::alive(Vulkan::WSI &wsi) { return true; }
|
||||
|
||||
void SDL_WSIPlatform::poll_input() {}
|
||||
void SDL_WSIPlatform::poll_input() { SDL_PumpEvents(); }
|
||||
|
||||
void SDL_WSIPlatform::poll_input_async(Granite::InputTrackerHandler *handler) {}
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ void ra_welcome(const char **game_title, const char **game_image_url) {
|
||||
|
||||
int message_length = snprintf(
|
||||
buffer, sizeof(buffer), "%s\nMode: %s\n", game->title,
|
||||
rc_client_get_hardcore_enabled(g_client) ? "Hardcore" : "Softcore");
|
||||
rc_client_get_hardcore_enabled(g_client) ? "Hardcore" : "Casual");
|
||||
|
||||
if (summary.num_core_achievements != 0) {
|
||||
snprintf(buffer + message_length, sizeof(buffer) - message_length,
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "1.96.0"
|
||||
channel = "1.96.1"
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ pub struct DecodedCheat {
|
||||
data: u16,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Cheats {
|
||||
pub cheats: Vec<Vec<DecodedCheat>>,
|
||||
pub boot: bool,
|
||||
@@ -26,7 +26,7 @@ pub type Cheat = std::collections::BTreeMap<String, std::collections::BTreeMap<S
|
||||
|
||||
pub fn init(
|
||||
device: &mut device::Device,
|
||||
cheat_settings: std::collections::HashMap<String, Option<String>>,
|
||||
cheat_settings: rustc_hash::FxHashMap<String, Option<String>>,
|
||||
) {
|
||||
let cheats = serde_json::from_slice::<Cheat>(include_bytes!("../data/cheats.json"))
|
||||
.unwrap()
|
||||
|
||||
+106
-41
@@ -15,8 +15,8 @@ const N64_EXTENSIONS_UNCOMPRESSED: [&str; 8] =
|
||||
|
||||
use rand::{Rng, SeedableRng};
|
||||
|
||||
use crate::{cheats, netplay, retroachievements, ui};
|
||||
use std::{collections::HashMap, io::Read};
|
||||
use crate::{cheats, netplay, retroachievements, savestates, ui};
|
||||
use std::io::Read;
|
||||
|
||||
pub mod ai;
|
||||
pub mod cache;
|
||||
@@ -47,11 +47,12 @@ pub mod tlb;
|
||||
pub mod unmapped;
|
||||
pub mod vi;
|
||||
|
||||
pub async fn run_game(
|
||||
pub fn run_game(
|
||||
device: &mut Device,
|
||||
rom_contents: &[u8],
|
||||
game_settings: ui::GameSettings,
|
||||
ra_config: retroachievements::RAConfig,
|
||||
netplay_config: Option<netplay::NetplayConfig>,
|
||||
) {
|
||||
device.cpu.overclock = game_settings.overclock;
|
||||
if game_settings.disable_expansion_pak {
|
||||
@@ -59,24 +60,38 @@ pub async fn run_game(
|
||||
}
|
||||
if let Some(slot) = game_settings.load_savestate_slot {
|
||||
device.ui.storage.save_state_slot = slot;
|
||||
device.load_state = true;
|
||||
device.savestate.load_state = true;
|
||||
}
|
||||
|
||||
init_rng(device);
|
||||
|
||||
cart::rom::init(device, rom_contents); // cart needs to come before rdram
|
||||
|
||||
// rdram pointer is shared with parallel-rdp and retroachievements
|
||||
rdram::init(device);
|
||||
|
||||
ui::video::init(device);
|
||||
ui::video::init(device, netplay_config.is_some());
|
||||
ui::audio::init(device);
|
||||
ui::input::init(&mut device.ui);
|
||||
|
||||
// must be after video init
|
||||
if let Some(netplay_config) = &netplay_config {
|
||||
device.netplay = netplay::init(
|
||||
device,
|
||||
netplay_config,
|
||||
cart::rom::is_system_pal(rom_contents),
|
||||
);
|
||||
if device.netplay.is_none() {
|
||||
ui::input::close(&mut device.ui);
|
||||
ui::audio::close(&mut device.ui);
|
||||
ui::video::close(&device.ui);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
init_rng_rtc(device);
|
||||
|
||||
// must be after video init
|
||||
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)
|
||||
};
|
||||
@@ -103,9 +118,14 @@ pub async fn run_game(
|
||||
|
||||
cpu::run(device);
|
||||
|
||||
retroachievements::unload_game(discord_watch_tx, discord_handle).await;
|
||||
retroachievements::unload_game(discord_watch_tx, discord_handle);
|
||||
|
||||
ui::storage::write_saves(device);
|
||||
|
||||
if let Some(netplay) = &mut device.netplay {
|
||||
netplay::close(netplay);
|
||||
}
|
||||
|
||||
ui::input::close(&mut device.ui);
|
||||
ui::audio::close(&mut device.ui);
|
||||
ui::video::close(&device.ui);
|
||||
@@ -115,7 +135,7 @@ fn set_rng() -> rand::rngs::Xoshiro256PlusPlus {
|
||||
rand::rngs::Xoshiro256PlusPlus::try_from_rng(&mut rand::rngs::SysRng).unwrap()
|
||||
}
|
||||
|
||||
fn init_rng(device: &mut Device) {
|
||||
fn init_rng_rtc(device: &mut Device) {
|
||||
let mut rng_seed = set_rng().next_u64();
|
||||
if let Some(netplay) = &mut device.netplay {
|
||||
if netplay.player_number == 0 {
|
||||
@@ -125,6 +145,18 @@ fn init_rng(device: &mut Device) {
|
||||
}
|
||||
}
|
||||
device.rng = rand::rngs::Xoshiro256PlusPlus::seed_from_u64(rng_seed);
|
||||
|
||||
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);
|
||||
} else {
|
||||
device.cart.rtc_timestamp = netplay::receive_rtc(netplay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_rom(contents: Vec<u8>) -> Option<Vec<u8>> {
|
||||
@@ -222,15 +254,28 @@ pub fn get_rom_contents(file_path: &std::path::PathBuf) -> Option<Vec<u8>> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct SpeedLimiter {
|
||||
pub limit_freq: u64,
|
||||
#[serde(skip)]
|
||||
#[serde(default = "std::time::Instant::now")]
|
||||
pub limit_freq_check: std::time::Instant,
|
||||
pub frame_counter: u64,
|
||||
pub enabled: bool,
|
||||
pub min_wait_time: std::time::Duration,
|
||||
#[serde(skip)]
|
||||
pub next_pace_deadline: Option<std::time::Instant>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Device {
|
||||
#[serde(skip)]
|
||||
pub netplay: Option<netplay::Netplay>,
|
||||
#[serde(skip, default = "ui::Ui::new")]
|
||||
#[serde(skip)]
|
||||
pub ui: ui::Ui,
|
||||
pub speed_limiter: SpeedLimiter,
|
||||
pub byte_swap: usize,
|
||||
pub save_state: bool,
|
||||
pub load_state: bool,
|
||||
pub savestate: savestates::Savestate,
|
||||
pub cpu: cpu::Cpu,
|
||||
pub pif: pif::Pif,
|
||||
pub cart: cart::Cart,
|
||||
@@ -244,7 +289,6 @@ pub struct Device {
|
||||
pub ai: ai::Ai,
|
||||
pub si: si::Si,
|
||||
pub ri: ri::Ri,
|
||||
#[serde(skip, default = "set_rng")]
|
||||
pub rng: rand::rngs::Xoshiro256PlusPlus,
|
||||
pub vru: controller::vru::Vru,
|
||||
pub transferpaks: [controller::transferpak::TransferPak; 4],
|
||||
@@ -256,7 +300,27 @@ pub fn zero_m128i() -> __m128i {
|
||||
}
|
||||
|
||||
impl Device {
|
||||
pub fn new() -> Device {
|
||||
pub fn clone_state(&mut self, device: &Device) {
|
||||
self.rng.clone_from(&device.rng);
|
||||
self.cpu.clone_from(&device.cpu);
|
||||
self.pif.clone_from(&device.pif);
|
||||
self.cart.clone_from(&device.cart);
|
||||
self.memory.clone_from(&device.memory);
|
||||
self.rsp.clone_from(&device.rsp);
|
||||
self.rdp.clone_from(&device.rdp);
|
||||
self.rdram.clone_from(&device.rdram);
|
||||
self.mi.clone_from(&device.mi);
|
||||
self.pi.clone_from(&device.pi);
|
||||
self.vi.clone_from(&device.vi);
|
||||
self.ai.clone_from(&device.ai);
|
||||
self.si.clone_from(&device.si);
|
||||
self.ri.clone_from(&device.ri);
|
||||
self.vru.clone_from(&device.vru);
|
||||
self.transferpaks.clone_from(&device.transferpaks);
|
||||
self.cheats.clone_from(&device.cheats);
|
||||
}
|
||||
|
||||
pub fn new(with_ui: bool) -> Box<Device> {
|
||||
let mut byte_swap: usize = 0;
|
||||
let test: [u8; 4] = [1, 2, 3, 4];
|
||||
// if the host computer is little endian, that means the RDRAM will be stored as little endian
|
||||
@@ -264,12 +328,31 @@ impl Device {
|
||||
if u32::from_le_bytes(test) == u32::from_ne_bytes(test) {
|
||||
byte_swap = 3;
|
||||
}
|
||||
Device {
|
||||
Box::new(Device {
|
||||
netplay: None,
|
||||
ui: ui::Ui::new(),
|
||||
ui: if with_ui {
|
||||
ui::Ui::new()
|
||||
} else {
|
||||
ui::Ui::default()
|
||||
},
|
||||
speed_limiter: SpeedLimiter {
|
||||
limit_freq: 2,
|
||||
limit_freq_check: std::time::Instant::now(),
|
||||
frame_counter: 0,
|
||||
enabled: true,
|
||||
min_wait_time: std::time::Duration::from_secs(1),
|
||||
next_pace_deadline: None,
|
||||
},
|
||||
byte_swap,
|
||||
save_state: false,
|
||||
load_state: false,
|
||||
savestate: savestates::Savestate {
|
||||
save_state: false,
|
||||
load_state: false,
|
||||
load_rewind: false,
|
||||
last_rewind_saved: 0.0,
|
||||
rewind_pool: std::sync::Arc::new(tokio::sync::Mutex::new(
|
||||
std::collections::BTreeMap::new(),
|
||||
)),
|
||||
},
|
||||
cpu: cpu::Cpu {
|
||||
cop0: cop0::Cop0 {
|
||||
regs: [0; cop0::COP0_REGS_COUNT],
|
||||
@@ -278,20 +361,8 @@ impl Device {
|
||||
is_event: false,
|
||||
instrs: [cop0::reserved; 32],
|
||||
instrs2: [cop0::reserved; 32],
|
||||
tlb_lut_w: vec![
|
||||
tlb::TlbLut {
|
||||
address: 0,
|
||||
cached: false,
|
||||
};
|
||||
0x100000
|
||||
],
|
||||
tlb_lut_r: vec![
|
||||
tlb::TlbLut {
|
||||
address: 0,
|
||||
cached: false,
|
||||
};
|
||||
0x100000
|
||||
],
|
||||
tlb_lut_w: rustc_hash::FxHashMap::default(),
|
||||
tlb_lut_r: rustc_hash::FxHashMap::default(),
|
||||
tlb_entries: [tlb::TlbEntry {
|
||||
mask: 0,
|
||||
vpn2: 0,
|
||||
@@ -509,14 +580,8 @@ impl Device {
|
||||
field: 0,
|
||||
delay: 0,
|
||||
count_per_scanline: 0,
|
||||
enable_speed_limiter: true,
|
||||
next_pace_deadline: None,
|
||||
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(),
|
||||
},
|
||||
vru: controller::vru::Vru {
|
||||
status: 0,
|
||||
@@ -526,7 +591,7 @@ impl Device {
|
||||
word_buffer: [0; 40],
|
||||
words: Vec::new(),
|
||||
talking: false,
|
||||
word_mappings: HashMap::new(),
|
||||
word_mappings: rustc_hash::FxHashMap::default(),
|
||||
},
|
||||
rng: set_rng(),
|
||||
transferpaks: [
|
||||
@@ -540,6 +605,6 @@ impl Device {
|
||||
boot: true,
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -11,7 +11,7 @@ pub const AI_REGS_COUNT: usize = 6;
|
||||
pub const AI_STATUS_BUSY: u32 = 0x40000000;
|
||||
const AI_STATUS_FULL: u32 = 0x80000000;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Ai {
|
||||
pub regs: [u32; AI_REGS_COUNT],
|
||||
pub fifo: [AiDma; 2],
|
||||
@@ -147,8 +147,7 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
AI_DACRATE_REG => {
|
||||
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(device)
|
||||
ui::audio::update_freq(device);
|
||||
}
|
||||
device::memory::masked_write_32(&mut device.ai.regs[reg as usize], value, mask)
|
||||
}
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ const JDT_EEPROM_16K: u16 = 0xc000; /* 16k EEPROM */
|
||||
const EEPROM_BLOCK_SIZE: usize = 8;
|
||||
pub const EEPROM_MAX_SIZE: usize = 0x800;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AfRtc {
|
||||
pub control: u16,
|
||||
}
|
||||
@@ -31,7 +31,7 @@ fn byte2bcd(mut n: u32) -> u8 {
|
||||
(((n / 10) << 4) | (n % 10)) as u8
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Cart {
|
||||
#[serde(skip)]
|
||||
pub rom: Vec<u8>,
|
||||
|
||||
+3
-16
@@ -1,5 +1,4 @@
|
||||
use crate::device;
|
||||
use crate::netplay;
|
||||
use crate::ui;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
@@ -122,22 +121,10 @@ pub fn dma_write(
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device, rom_file: &[u8]) {
|
||||
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);
|
||||
} else {
|
||||
device.cart.rtc_timestamp = netplay::receive_rtc(netplay);
|
||||
}
|
||||
}
|
||||
|
||||
device.cart.sc64.cfg[device::cart::sc64::SC64_BOOTLOADER_SWITCH as usize] = 1;
|
||||
|
||||
device.cart.rom = rom_file.to_vec();
|
||||
device.cart.pal = is_system_pal(device.cart.rom[0x3E]);
|
||||
device.cart.pal = is_system_pal(&device.cart.rom);
|
||||
set_cic(device);
|
||||
|
||||
device.ui.game_hash = calculate_hash(&device.cart.rom);
|
||||
@@ -148,10 +135,10 @@ pub fn init(device: &mut device::Device, rom_file: &[u8]) {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_system_pal(country: u8) -> bool {
|
||||
pub fn is_system_pal(rom_contents: &[u8]) -> bool {
|
||||
let pal_codes: [u8; 8] = *b"DFIPSUXY";
|
||||
for i in pal_codes {
|
||||
if country == i {
|
||||
if rom_contents[0x3E] == i {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-11
@@ -20,7 +20,7 @@ pub const SC64_CFG_COUNT: usize = 15;
|
||||
const SC64_BUFFER_MASK: usize = 0x1FFF;
|
||||
const SC64_EEPROM_MASK: usize = 0xFFF;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Sc64 {
|
||||
pub buffer: Vec<u8>,
|
||||
pub regs: [u32; SC64_REGS_COUNT],
|
||||
@@ -219,16 +219,9 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
data.data_size & 0xFFFFFF; // length
|
||||
device.cart.sc64.usb_buffer = data.data; // store the data to be read
|
||||
}
|
||||
Err(err) => {
|
||||
match err {
|
||||
tokio::sync::broadcast::error::TryRecvError::Lagged(_) => {
|
||||
panic!("cart_rx lagged: {err}");
|
||||
}
|
||||
_ => {
|
||||
device.cart.sc64.regs[SC64_DATA0_REG] = 0; // read_status/type
|
||||
device.cart.sc64.regs[SC64_DATA1_REG] = 0; // length
|
||||
}
|
||||
}
|
||||
Err(_err) => {
|
||||
device.cart.sc64.regs[SC64_DATA0_REG] = 0; // read_status/type
|
||||
device.cart.sc64.regs[SC64_DATA1_REG] = 0; // length
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,7 @@ pub const MX29L1100_ID: u32 = 0x00c2001e;
|
||||
const MX29L0000_ID: u32 = 0x00c20000;
|
||||
const MX29L0001_ID: u32 = 0x00c20001;
|
||||
|
||||
#[derive(PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum FlashramMode {
|
||||
ReadArray,
|
||||
ReadSiliconId,
|
||||
@@ -18,7 +18,7 @@ pub enum FlashramMode {
|
||||
PageProgram,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Flashram {
|
||||
pub status: u32,
|
||||
pub mode: FlashramMode,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{device, netplay, savestates, ui};
|
||||
use crate::{device, savestates, ui};
|
||||
|
||||
pub mod gbcart;
|
||||
pub mod mempak;
|
||||
@@ -57,11 +57,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
|
||||
JCMD_CONTROLLER_READ => {
|
||||
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(&mut device.ui, 0);
|
||||
netplay::send_input(netplay, local_input);
|
||||
}
|
||||
netplay::get_input(device, channel)
|
||||
netplay.inputs[channel].0
|
||||
} else {
|
||||
ui::input::get(&mut device.ui, channel)
|
||||
};
|
||||
@@ -162,7 +158,7 @@ pub fn pak_switch_event(device: &mut device::Device) {
|
||||
if channel.change_pak != PakType::None {
|
||||
//stop rumble if it is on
|
||||
if let Some(netplay) = &device.netplay {
|
||||
if netplay.player_number as usize == i {
|
||||
if netplay.player_number == i {
|
||||
device::ui::input::set_rumble(&device.ui, 0, 0);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,7 @@ 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)]
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct GbCart {
|
||||
pub enabled: bool,
|
||||
#[serde(skip)]
|
||||
@@ -19,7 +19,6 @@ pub struct GbCart {
|
||||
pub mbc1_mode: bool,
|
||||
pub ram_bank: u16,
|
||||
pub rom_bank: u32,
|
||||
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,
|
||||
@@ -255,12 +254,9 @@ fn write_mbc3(
|
||||
} else if address < 0x6000 {
|
||||
cart.ram_bank = (value & 0xf) as u16;
|
||||
} else if address < 0x8000 {
|
||||
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;
|
||||
if cart.cart_type == CartType::MBC3RamBattRtc && (value & 0x1) != 0 {
|
||||
update_rtc_regs(cart, elapsed_time);
|
||||
cart.rtc_regs_latch = cart.rtc_regs;
|
||||
}
|
||||
} else if (0xa000..0xc000).contains(&address) {
|
||||
if !cart.ram_enabled {
|
||||
@@ -284,7 +280,6 @@ fn read_mbc3(
|
||||
address: u16,
|
||||
data: usize,
|
||||
size: usize,
|
||||
elapsed_time: i64,
|
||||
) {
|
||||
if address < 0x4000 {
|
||||
let banked_address = address & 0x3FFF;
|
||||
@@ -309,15 +304,8 @@ fn read_mbc3(
|
||||
.unwrap_or(&vec![0; size]),
|
||||
);
|
||||
} 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] = cart.rtc_regs_latch[cart.ram_bank as usize - 0x8];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -395,7 +383,6 @@ pub fn read(
|
||||
address: u16,
|
||||
data: usize,
|
||||
size: usize,
|
||||
elapsed_time: i64,
|
||||
) {
|
||||
if !cart.enabled {
|
||||
for i in 0..size {
|
||||
@@ -405,8 +392,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, elapsed_time),
|
||||
CartType::MBC3RamBattRtc => read_mbc3(pif_ram, cart, address, data, size, elapsed_time),
|
||||
CartType::MBC3RamBatt => read_mbc3(pif_ram, cart, address, data, size),
|
||||
CartType::MBC3RamBattRtc => read_mbc3(pif_ram, cart, address, data, size),
|
||||
CartType::MBC5RamBatt => read_mbc5(pif_ram, cart, address, data, size),
|
||||
_ => panic!("Unsupported cart type"),
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ pub fn write(device: &mut device::Device, channel: usize, address: u16, data: us
|
||||
let rumble = device.pif.ram[data + size - 1];
|
||||
|
||||
if let Some(netplay) = &device.netplay {
|
||||
if netplay.player_number as usize == channel {
|
||||
if netplay.player_number == channel {
|
||||
device::ui::input::set_rumble(&device.ui, 0, rumble);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::device;
|
||||
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TransferPak {
|
||||
pub enabled: bool,
|
||||
pub reset_state: u8,
|
||||
@@ -58,7 +58,6 @@ pub fn read(device: &mut device::Device, channel: usize, address: u16, data: usi
|
||||
0x4000 * pak.bank + (address & 0x7fff) - 0x4000,
|
||||
data,
|
||||
size,
|
||||
device.vi.elapsed_time as i64,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::device;
|
||||
#[cfg(all(feature = "gui", not(target_os = "android")))]
|
||||
use crate::ui;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const JCMD_VRU_READ: u8 = 0x09;
|
||||
const JCMD_VRU_WRITE: u8 = 0x0A;
|
||||
@@ -18,7 +17,7 @@ const VOICE_STATUS_BUSY: u8 = 0x05;
|
||||
const JDT_VRU: u16 = 0x0100; /* VRU */
|
||||
const CONT_FLAVOR: u16 = JDT_VRU;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Vru {
|
||||
pub status: u8,
|
||||
pub voice_state: u8,
|
||||
@@ -28,7 +27,7 @@ pub struct Vru {
|
||||
pub word_buffer: [u16; 40],
|
||||
pub words: Vec<String>,
|
||||
pub talking: bool,
|
||||
pub word_mappings: HashMap<String, String>,
|
||||
pub word_mappings: rustc_hash::FxHashMap<String, String>,
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
@@ -186,7 +185,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
|
||||
#[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([
|
||||
let data: rustc_hash::FxHashMap<usize, u16> = rustc_hash::FxHashMap::from_iter([
|
||||
(0, 0x8000),
|
||||
(2, 0x0F00),
|
||||
(4, 0), // error flags
|
||||
@@ -247,7 +246,7 @@ pub fn vru_talking_event(device: &mut device::Device) {
|
||||
}
|
||||
|
||||
fn create_word_mappings(device: &mut device::Device) {
|
||||
device.vru.word_mappings = HashMap::from([
|
||||
device.vru.word_mappings = rustc_hash::FxHashMap::from_iter([
|
||||
(
|
||||
String::from("03A50024000303CF00A80003036000EA"),
|
||||
String::from("pikachu"),
|
||||
|
||||
+3
-3
@@ -91,7 +91,7 @@ const COP0_XCONTEXT_REG_MASK: u64 =
|
||||
const COP0_PARITYERR_REG_MASK: u64 = 0b00000000000000000000000011111111;
|
||||
const COP0_TAGLO_REG_MASK: u64 = 0b00001111111111111111111111000000;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Cop0 {
|
||||
pub reg_latch: u64,
|
||||
pub regs: [u64; COP0_REGS_COUNT],
|
||||
@@ -100,8 +100,8 @@ pub struct Cop0 {
|
||||
pub instrs: [fn(&mut device::Device, u32); 32],
|
||||
#[serde(skip, default = "savestates::default_instructions")]
|
||||
pub instrs2: [fn(&mut device::Device, u32); 32],
|
||||
pub tlb_lut_r: Vec<device::tlb::TlbLut>,
|
||||
pub tlb_lut_w: Vec<device::tlb::TlbLut>,
|
||||
pub tlb_lut_r: rustc_hash::FxHashMap<u64, device::tlb::TlbLut>,
|
||||
pub tlb_lut_w: rustc_hash::FxHashMap<u64, device::tlb::TlbLut>,
|
||||
pub tlb_entries: [device::tlb::TlbEntry; 32],
|
||||
pub is_event: bool,
|
||||
}
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ const FCR31_CAUSE_MASK: u32 = 0b00000000000000111111000000000000;
|
||||
const FCR31_ENABLE_MASK: u32 = 0b00000000000000000000111110000000;
|
||||
const FCR31_WRITE_MASK: u32 = 0b00000001100000111111111111111111;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Cop1 {
|
||||
pub fcr0: u32,
|
||||
pub fcr31: u32,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
use crate::{device, savestates};
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Cop2 {
|
||||
#[serde(skip, default = "savestates::default_instructions")]
|
||||
pub instrs: [fn(&mut device::Device, u32); 32],
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
use crate::{device, savestates, ui};
|
||||
|
||||
#[derive(PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum State {
|
||||
Step,
|
||||
Take,
|
||||
@@ -11,13 +11,13 @@ pub enum State {
|
||||
Exception,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct BranchState {
|
||||
pub state: State,
|
||||
pub pc: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Cpu {
|
||||
pub cop0: device::cop0::Cop0,
|
||||
pub cop1: device::cop1::Cop1,
|
||||
|
||||
@@ -15,7 +15,8 @@ pub const EVENT_TYPE_VRU: usize = 10;
|
||||
pub const EVENT_TYPE_PAK: usize = 11;
|
||||
pub const EVENT_TYPE_SAVE: usize = 12;
|
||||
pub const EVENT_TYPE_NMI: usize = 13;
|
||||
pub const EVENT_TYPE_COUNT: usize = 14;
|
||||
pub const EVENT_TYPE_UPDATE_SCREEN: usize = 14;
|
||||
pub const EVENT_TYPE_COUNT: usize = 15;
|
||||
|
||||
#[derive(PartialEq, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Event {
|
||||
@@ -65,6 +66,7 @@ fn get_event_handler(name: usize) -> fn(&mut device::Device) {
|
||||
EVENT_TYPE_PAK => device::controller::pak_switch_event,
|
||||
EVENT_TYPE_SAVE => ui::storage::save_event,
|
||||
EVENT_TYPE_NMI => device::exceptions::reset_event,
|
||||
EVENT_TYPE_UPDATE_SCREEN => device::vi::update_screen,
|
||||
_ => dummy_event,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ pub enum AccessSize {
|
||||
Icache = 32,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Memory {
|
||||
#[serde(skip, default = "savestates::default_memory_read_fast")]
|
||||
pub fast_read: [fn(&device::Device, u64, AccessSize) -> u32; 0x2000], // fast_read is used for lookups that try to detect idle loops
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ const MI_SET_RDRAM: u32 = 1 << 13;
|
||||
|
||||
pub const MI_INIT_LENGTH_MASK: u32 = 0b1111111;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Mi {
|
||||
pub regs: [u32; MI_REGS_COUNT],
|
||||
}
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ const PI_STATUS_INTERRUPT: u32 = 1 << 3;
|
||||
const PI_STATUS_RESET: u32 = 1 << 0;
|
||||
const PI_STATUS_CLR_INTR: u32 = 1 << 1;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Pi {
|
||||
pub regs: [u32; PI_REGS_COUNT],
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
mod rom;
|
||||
use crate::device;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Pif {
|
||||
#[serde(with = "serde_big_array::BigArray")]
|
||||
pub rom: [u8; 1984],
|
||||
@@ -219,7 +219,7 @@ pub fn process_ram(device: &mut device::Device) {
|
||||
pub fn connect_pif_channels(device: &mut device::Device) {
|
||||
for i in 0..4 {
|
||||
if let Some(netplay) = &device.netplay {
|
||||
if netplay.player_data[i].reg_id != 0 {
|
||||
if netplay.connected[i] {
|
||||
device.pif.channels[i].process = Some(device::controller::process);
|
||||
}
|
||||
} else if device.ui.config.input.controller_enabled[i] {
|
||||
@@ -278,7 +278,7 @@ pub fn init(device: &mut device::Device) {
|
||||
|
||||
for i in 0..4 {
|
||||
if let Some(netplay) = &device.netplay {
|
||||
if netplay.player_data[i].reg_id != 0 {
|
||||
if netplay.connected[i] {
|
||||
device.pif.channels[i].pak_handler = Some(default_handler);
|
||||
}
|
||||
} else if device.ui.config.input.controller_enabled[i] {
|
||||
|
||||
+2
-11
@@ -1,4 +1,4 @@
|
||||
use crate::{device, savestates, ui};
|
||||
use crate::{device, ui};
|
||||
|
||||
pub const DPC_START_REG: usize = 0;
|
||||
pub const DPC_END_REG: usize = 1;
|
||||
@@ -40,7 +40,7 @@ const DPC_CLR_PIPE_CTR: u32 = 1 << 7;
|
||||
const DPC_CLR_CMD_CTR: u32 = 1 << 8;
|
||||
const DPC_CLR_CLOCK_CTR: u32 = 1 << 9;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Rdp {
|
||||
pub regs_dpc: [u32; DPC_REGS_COUNT],
|
||||
pub regs_dps: [u32; DPS_REGS_COUNT],
|
||||
@@ -120,15 +120,6 @@ fn run_rdp(device: &mut device::Device) {
|
||||
device.rdp.regs_dpc[DPC_PIPEBUSY_REG] = 0xFFFFFF;
|
||||
|
||||
if timer != 0 {
|
||||
if device.save_state {
|
||||
// Right after full sync, good time for save state
|
||||
device.save_state = false;
|
||||
savestates::create_savestate(device);
|
||||
} else if device.load_state {
|
||||
// Right after full sync, good time for save state
|
||||
device.load_state = false;
|
||||
savestates::load_savestate(device);
|
||||
}
|
||||
device::events::create_event(device, device::events::EVENT_TYPE_DP, timer)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ pub const RDRAM_REGS_COUNT: usize = 10;
|
||||
|
||||
pub const RDRAM_MASK: usize = 0xFFFFFF;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Rdram {
|
||||
pub mem: Vec<u8>,
|
||||
pub size: u32,
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ pub const RI_REFRESH_REG: usize = 4;
|
||||
//const RI_WERROR_REG: usize = 7;
|
||||
pub const RI_REGS_COUNT: usize = 8;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Ri {
|
||||
pub regs: [u32; RI_REGS_COUNT],
|
||||
pub ram_init: bool,
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{device, savestates};
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
use std::arch::x86_64::*;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct BranchState {
|
||||
pub state: device::cpu::State,
|
||||
pub pc: u32,
|
||||
@@ -25,7 +25,7 @@ pub enum InstructionType {
|
||||
Vu,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Cpu {
|
||||
#[serde(with = "serde_big_array::BigArray")]
|
||||
pub instructions: [Instructions; 0x1000 / 4],
|
||||
|
||||
@@ -76,7 +76,7 @@ pub struct RspDma {
|
||||
pub dramaddr: u32,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Rsp {
|
||||
pub cpu: device::rsp_cpu::Cpu,
|
||||
pub regs: [u32; SP_REGS_COUNT],
|
||||
|
||||
+2
-2
@@ -16,14 +16,14 @@ pub const SI_STATUS_IO_BUSY: u32 = 1 << 1;
|
||||
//const SI_STATUS_DMA_ERROR: u32 = 1 << 3;
|
||||
const SI_STATUS_INTERRUPT: u32 = 1 << 12;
|
||||
|
||||
#[derive(PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DmaDir {
|
||||
None,
|
||||
Write,
|
||||
Read,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Si {
|
||||
pub regs: [u32; SI_REGS_COUNT],
|
||||
pub dma_dir: DmaDir,
|
||||
|
||||
+38
-28
@@ -149,15 +149,13 @@ fn tlb_unmap(device: &mut device::Device, index: u64) {
|
||||
if e.v_even != 0 {
|
||||
let mut i = e.start_even;
|
||||
while i < e.end_even {
|
||||
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].address = 0;
|
||||
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].cached = false;
|
||||
device.cpu.cop0.tlb_lut_r.remove(&(i >> 12));
|
||||
i += 0x1000
|
||||
}
|
||||
if e.d_even != 0 {
|
||||
let mut i = e.start_even;
|
||||
while i < e.end_even {
|
||||
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].address = 0;
|
||||
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].cached = false;
|
||||
device.cpu.cop0.tlb_lut_w.remove(&(i >> 12));
|
||||
i += 0x1000
|
||||
}
|
||||
}
|
||||
@@ -166,15 +164,13 @@ fn tlb_unmap(device: &mut device::Device, index: u64) {
|
||||
if e.v_odd != 0 {
|
||||
let mut i = e.start_odd;
|
||||
while i < e.end_odd {
|
||||
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].address = 0;
|
||||
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].cached = false;
|
||||
device.cpu.cop0.tlb_lut_r.remove(&(i >> 12));
|
||||
i += 0x1000
|
||||
}
|
||||
if e.d_odd != 0 {
|
||||
let mut i = e.start_odd;
|
||||
while i < e.end_odd {
|
||||
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].address = 0;
|
||||
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].cached = false;
|
||||
device.cpu.cop0.tlb_lut_w.remove(&(i >> 12));
|
||||
i += 0x1000
|
||||
}
|
||||
}
|
||||
@@ -191,17 +187,25 @@ fn tlb_map(device: &mut device::Device, index: u64) {
|
||||
{
|
||||
let mut i = e.start_even;
|
||||
while i < e.end_even {
|
||||
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].address =
|
||||
0x80000000 | (e.phys_even + (i - e.start_even) + 0xFFF);
|
||||
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].cached = e.c_even != 2;
|
||||
device.cpu.cop0.tlb_lut_r.insert(
|
||||
i >> 12,
|
||||
device::tlb::TlbLut {
|
||||
address: 0x80000000 | (e.phys_even + (i - e.start_even) + 0xFFF),
|
||||
cached: e.c_even != 2,
|
||||
},
|
||||
);
|
||||
i += 0x1000
|
||||
}
|
||||
if e.d_even != 0 {
|
||||
let mut i = e.start_even;
|
||||
while i < e.end_even {
|
||||
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].address =
|
||||
0x80000000 | (e.phys_even + (i - e.start_even) + 0xFFF);
|
||||
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].cached = e.c_even != 2;
|
||||
device.cpu.cop0.tlb_lut_w.insert(
|
||||
i >> 12,
|
||||
device::tlb::TlbLut {
|
||||
address: 0x80000000 | (e.phys_even + (i - e.start_even) + 0xFFF),
|
||||
cached: e.c_even != 2,
|
||||
},
|
||||
);
|
||||
i += 0x1000
|
||||
}
|
||||
}
|
||||
@@ -214,17 +218,25 @@ fn tlb_map(device: &mut device::Device, index: u64) {
|
||||
{
|
||||
let mut i = e.start_odd;
|
||||
while i < e.end_odd {
|
||||
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].address =
|
||||
0x80000000 | (e.phys_odd + (i - e.start_odd) + 0xFFF);
|
||||
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].cached = e.c_odd != 2;
|
||||
device.cpu.cop0.tlb_lut_r.insert(
|
||||
i >> 12,
|
||||
device::tlb::TlbLut {
|
||||
address: 0x80000000 | (e.phys_odd + (i - e.start_odd) + 0xFFF),
|
||||
cached: e.c_odd != 2,
|
||||
},
|
||||
);
|
||||
i += 0x1000
|
||||
}
|
||||
if e.d_odd != 0 {
|
||||
let mut i = e.start_odd;
|
||||
while i < e.end_odd {
|
||||
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].address =
|
||||
0x80000000 | (e.phys_odd + (i - e.start_odd) + 0xFFF);
|
||||
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].cached = e.c_odd != 2;
|
||||
device.cpu.cop0.tlb_lut_w.insert(
|
||||
i >> 12,
|
||||
device::tlb::TlbLut {
|
||||
address: 0x80000000 | (e.phys_odd + (i - e.start_odd) + 0xFFF),
|
||||
cached: e.c_odd != 2,
|
||||
},
|
||||
);
|
||||
i += 0x1000
|
||||
}
|
||||
}
|
||||
@@ -239,19 +251,17 @@ pub fn get_physical_address(
|
||||
address &= 0xffffffff;
|
||||
|
||||
if access_type == device::memory::AccessType::Write {
|
||||
if device.cpu.cop0.tlb_lut_w[(address >> 12) as usize].address != 0 {
|
||||
if let Some(lut) = device.cpu.cop0.tlb_lut_w.get(&(address >> 12)) {
|
||||
return (
|
||||
(device.cpu.cop0.tlb_lut_w[(address >> 12) as usize].address & 0x1FFFF000)
|
||||
| (address & 0xFFF),
|
||||
device.cpu.cop0.tlb_lut_w[(address >> 12) as usize].cached,
|
||||
(lut.address & 0x1FFFF000) | (address & 0xFFF),
|
||||
lut.cached,
|
||||
false,
|
||||
);
|
||||
}
|
||||
} else if device.cpu.cop0.tlb_lut_r[(address >> 12) as usize].address != 0 {
|
||||
} else if let Some(lut) = device.cpu.cop0.tlb_lut_r.get(&(address >> 12)) {
|
||||
return (
|
||||
(device.cpu.cop0.tlb_lut_r[(address >> 12) as usize].address & 0x1FFFF000)
|
||||
| (address & 0xFFF),
|
||||
device.cpu.cop0.tlb_lut_r[(address >> 12) as usize].cached,
|
||||
(lut.address & 0x1FFFF000) | (address & 0xFFF),
|
||||
lut.cached,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
+80
-60
@@ -1,39 +1,30 @@
|
||||
use crate::{cheats, device, netplay, retroachievements, ui};
|
||||
use crate::{cheats, device, netplay, retroachievements, savestates, ui};
|
||||
|
||||
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_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_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)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Vi {
|
||||
pub regs: [u32; VI_REGS_COUNT],
|
||||
pub clock: u64,
|
||||
pub delay: u64,
|
||||
pub field: u32,
|
||||
#[serde(skip)]
|
||||
pub next_pace_deadline: Option<std::time::Instant>,
|
||||
pub count_per_scanline: u64,
|
||||
pub enable_speed_limiter: bool,
|
||||
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")]
|
||||
pub limit_freq_check: std::time::Instant,
|
||||
}
|
||||
|
||||
const MAX_LIMIT_FREQ: u64 = 3;
|
||||
@@ -46,12 +37,10 @@ pub fn set_expected_refresh_rate(device: &mut device::Device) {
|
||||
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] + 1) as u64;
|
||||
|
||||
reset_pace_deadline(device);
|
||||
}
|
||||
|
||||
fn reset_pace_deadline(device: &mut device::Device) {
|
||||
device.vi.next_pace_deadline = None;
|
||||
device.speed_limiter.next_pace_deadline = None;
|
||||
}
|
||||
|
||||
fn set_vertical_interrupt(device: &mut device::Device) {
|
||||
@@ -111,7 +100,12 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
let current_origin = device.vi.regs[reg as usize];
|
||||
device::memory::masked_write_32(&mut device.vi.regs[reg as usize], value, mask);
|
||||
if current_origin != device.vi.regs[reg as usize] {
|
||||
let _ = device.ui.video.fps_tx.try_send(true);
|
||||
if device.netplay.is_none() {
|
||||
savestates::process_savestates(device);
|
||||
}
|
||||
if !netplay::in_rollback(device.netplay.as_ref()) {
|
||||
let _ = device.ui.video.fps_tx.as_ref().unwrap().send(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -121,50 +115,55 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
|
||||
ui::video::set_register(reg as u32, device.vi.regs[reg as usize])
|
||||
}
|
||||
|
||||
pub fn vertical_interrupt_event(device: &mut device::Device) {
|
||||
if device.cheats.enabled {
|
||||
cheats::execute_cheats(device, device.cheats.cheats.clone());
|
||||
pub fn update_screen(device: &mut device::Device) {
|
||||
if !netplay::in_rollback(device.netplay.as_ref()) {
|
||||
ui::video::render_frame();
|
||||
}
|
||||
|
||||
ui::video::render_frame();
|
||||
let _ = device.ui.video.vis_tx.try_send(true);
|
||||
|
||||
retroachievements::do_frame();
|
||||
|
||||
let (mut speed_limiter_toggled, paused) = ui::video::check_callback(device);
|
||||
|
||||
if let Some(netplay) = &mut device.netplay {
|
||||
netplay::send_sync_check(netplay, device.cpu.cop0.regs.as_ref());
|
||||
if device.vi.enable_speed_limiter == netplay.fast_forward {
|
||||
speed_limiter_toggled = true;
|
||||
device.vi.enable_speed_limiter = !netplay.fast_forward;
|
||||
}
|
||||
}
|
||||
let (speed_limiter_toggled, paused) = ui::video::check_callback(device);
|
||||
|
||||
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 {
|
||||
if !netplay::in_rollback(device.netplay.as_ref())
|
||||
&& device
|
||||
.speed_limiter
|
||||
.frame_counter
|
||||
.is_multiple_of(device.speed_limiter.limit_freq)
|
||||
&& device.speed_limiter.enabled
|
||||
{
|
||||
speed_limiter(device);
|
||||
}
|
||||
|
||||
unsafe { sdl3_sys::events::SDL_PumpEvents() };
|
||||
if !netplay::in_rollback(device.netplay.as_ref()) {
|
||||
ui::video::update_screen();
|
||||
device.speed_limiter.frame_counter += 1;
|
||||
let _ = device.ui.video.vis_tx.as_ref().unwrap().send(true);
|
||||
}
|
||||
|
||||
ui::video::update_screen();
|
||||
device.vi.vi_counter += 1;
|
||||
device.vi.elapsed_time += device.vi.frame_time;
|
||||
|
||||
if device.netplay.is_none() && paused {
|
||||
if device.netplay.is_some() {
|
||||
device.netplay.as_mut().unwrap().inputs = netplay::process_requests(device);
|
||||
} else if paused {
|
||||
if retroachievements::get_hardcore() {
|
||||
ui::video::onscreen_message(
|
||||
"Cannot pause in RA hardcore mode",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
} else {
|
||||
ui::video::pause_loop(device.vi.frame_time);
|
||||
ui::video::pause_loop(&mut device.ui, device.vi.frame_time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical_interrupt_event(device: &mut device::Device) {
|
||||
device.vi.elapsed_time += device.vi.frame_time;
|
||||
|
||||
if device.cheats.enabled {
|
||||
cheats::execute_cheats(device, device.cheats.cheats.clone());
|
||||
}
|
||||
|
||||
retroachievements::do_frame();
|
||||
|
||||
/* toggle vi field if in interlaced mode */
|
||||
device.vi.field ^= (device.vi.regs[VI_STATUS_REG] >> 6) & 0x1;
|
||||
@@ -175,7 +174,17 @@ pub fn vertical_interrupt_event(device: &mut device::Device) {
|
||||
device,
|
||||
device::events::EVENT_TYPE_VI,
|
||||
device.cpu.next_event_count + device.vi.delay,
|
||||
)
|
||||
);
|
||||
|
||||
let v_intr = device.vi.regs[VI_V_INTR_REG] & 0x3FF;
|
||||
let v_start = (device.vi.regs[VI_V_START_REG] >> 16) & 0x3FF;
|
||||
if v_start > v_intr {
|
||||
device::events::create_event(
|
||||
device,
|
||||
device::events::EVENT_TYPE_UPDATE_SCREEN,
|
||||
(v_start - v_intr) as u64 * device.vi.count_per_scanline,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
@@ -188,55 +197,66 @@ pub fn init(device: &mut device::Device) {
|
||||
|
||||
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);
|
||||
let mut interval = std::time::Duration::from_secs_f64(
|
||||
device.vi.frame_time * device.speed_limiter.limit_freq as f64,
|
||||
);
|
||||
|
||||
if let Some(netplay) = &device.netplay {
|
||||
let ahead = netplay.session.frames_ahead();
|
||||
if ahead > 0 {
|
||||
interval = interval.mul_f64(1.0 + 0.05 * ahead.min(2) as f64);
|
||||
} else if ahead < 0 {
|
||||
interval = interval.mul_f64(1.0 - 0.05 * (-ahead).min(2) as f64);
|
||||
}
|
||||
}
|
||||
|
||||
let now = std::time::Instant::now();
|
||||
match device.vi.next_pace_deadline {
|
||||
match device.speed_limiter.next_pace_deadline {
|
||||
None => {
|
||||
device.vi.next_pace_deadline = Some(now + interval);
|
||||
device.speed_limiter.next_pace_deadline = Some(now + interval);
|
||||
speed_limiter_toggled = true;
|
||||
}
|
||||
Some(deadline) => {
|
||||
if now < deadline {
|
||||
let dur = deadline - now;
|
||||
spin_sleep::sleep(dur);
|
||||
if dur < device.vi.min_wait_time {
|
||||
device.vi.min_wait_time = dur;
|
||||
|
||||
if dur < device.speed_limiter.min_wait_time {
|
||||
device.speed_limiter.min_wait_time = dur;
|
||||
}
|
||||
} else {
|
||||
//println!("did not sleep");
|
||||
device.vi.min_wait_time = std::time::Duration::from_secs(0);
|
||||
device.speed_limiter.min_wait_time = std::time::Duration::from_secs(0);
|
||||
}
|
||||
let mut next = deadline + interval;
|
||||
let t = std::time::Instant::now();
|
||||
while next <= t {
|
||||
next += interval;
|
||||
}
|
||||
device.vi.next_pace_deadline = Some(next);
|
||||
device.speed_limiter.next_pace_deadline = Some(next);
|
||||
}
|
||||
}
|
||||
|
||||
if std::time::Instant::now().duration_since(device.vi.limit_freq_check)
|
||||
if std::time::Instant::now().duration_since(device.speed_limiter.limit_freq_check)
|
||||
> std::time::Duration::from_secs(1)
|
||||
{
|
||||
if !speed_limiter_toggled {
|
||||
if device.vi.min_wait_time == std::time::Duration::from_secs(0)
|
||||
&& device.vi.limit_freq < MAX_LIMIT_FREQ
|
||||
if device.speed_limiter.min_wait_time == std::time::Duration::from_secs(0)
|
||||
&& device.speed_limiter.limit_freq < MAX_LIMIT_FREQ
|
||||
{
|
||||
device.vi.limit_freq += 1;
|
||||
device.speed_limiter.limit_freq += 1;
|
||||
reset_pace_deadline(device);
|
||||
} else if device.vi.min_wait_time
|
||||
} else if device.speed_limiter.min_wait_time
|
||||
> std::time::Duration::from_secs_f64(device.vi.frame_time)
|
||||
&& device.vi.limit_freq > 1
|
||||
&& device.speed_limiter.limit_freq > 1
|
||||
{
|
||||
device.vi.limit_freq -= 1;
|
||||
device.speed_limiter.limit_freq -= 1;
|
||||
reset_pace_deadline(device);
|
||||
}
|
||||
}
|
||||
|
||||
//println!("limit freq: {}", device.vi.limit_freq);
|
||||
device.vi.min_wait_time = std::time::Duration::from_secs(1);
|
||||
device.vi.limit_freq_check = std::time::Instant::now();
|
||||
device.speed_limiter.min_wait_time = std::time::Duration::from_secs(1);
|
||||
device.speed_limiter.limit_freq_check = std::time::Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
+83
-28
@@ -22,15 +22,21 @@ pub struct Args {
|
||||
#[arg(short, long)]
|
||||
pub fullscreen: bool,
|
||||
#[arg(long)]
|
||||
pub widescreen: 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_SERVER_ADDR", hide = true)]
|
||||
pub netplay_server_addr: Option<String>,
|
||||
#[arg(long, value_name = "NETPLAY_PLAYER_NUMBER", hide = true)]
|
||||
pub netplay_player_number: Option<u8>,
|
||||
pub netplay_player_number: Option<usize>,
|
||||
#[arg(long, value_name = "NETPLAY_NUMBER_OF_PLAYERS", hide = true)]
|
||||
pub netplay_number_of_players: Option<usize>,
|
||||
#[arg(long, value_name = "NETPLAY_INPUT_DELAY", hide = true)]
|
||||
pub netplay_input_delay: Option<usize>,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PROFILE_NAME",
|
||||
@@ -93,7 +99,27 @@ pub struct Args {
|
||||
pub ra_password: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
|
||||
#[cfg(feature = "gui")]
|
||||
fn set_app_id() {
|
||||
if let Err(e) = slint::set_xdg_app_id(ui::APP_ID) {
|
||||
eprintln!("Could not set application ID: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_runtime() -> (tokio::sync::oneshot::Sender<()>, tokio::runtime::Handle) {
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let (close_tx, close_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().expect("Failed to build runtime");
|
||||
tx.send(rt.handle().clone()).unwrap();
|
||||
rt.block_on(close_rx).unwrap();
|
||||
});
|
||||
|
||||
(close_tx, rx.blocking_recv().unwrap())
|
||||
}
|
||||
|
||||
pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
|
||||
let dirs = ui::get_dirs();
|
||||
|
||||
std::fs::create_dir_all(&dirs.config_dir)?;
|
||||
@@ -135,17 +161,24 @@ pub async fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
|
||||
return Err(Error::other("Savestate slot must be between 0 and 9"));
|
||||
}
|
||||
|
||||
let mut device = device::Device::new();
|
||||
let mut device = device::Device::new(true);
|
||||
|
||||
device.ui.config.recent_roms.retain(|x| *x != game);
|
||||
device.ui.config.recent_roms.insert(0, game);
|
||||
device.ui.config.recent_roms.truncate(5);
|
||||
device.ui.config.ui.recent_roms.retain(|x| *x != game);
|
||||
device.ui.config.ui.recent_roms.insert(0, game);
|
||||
device.ui.config.ui.recent_roms.truncate(5);
|
||||
|
||||
if args.fullscreen {
|
||||
device.ui.video.fullscreen = true;
|
||||
} else {
|
||||
device.ui.video.fullscreen = device.ui.config.video.fullscreen;
|
||||
}
|
||||
|
||||
if args.widescreen {
|
||||
device.ui.video.widescreen = true;
|
||||
} else {
|
||||
device.ui.video.widescreen = device.ui.config.video.widescreen;
|
||||
}
|
||||
|
||||
let overclock = args
|
||||
.overclock
|
||||
.unwrap_or(device.ui.config.emulation.overclock);
|
||||
@@ -156,11 +189,7 @@ pub async fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
|
||||
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 {
|
||||
if args.netplay_server_addr.is_none() {
|
||||
for i in 0..4 {
|
||||
if device.ui.config.input.transfer_pak[i]
|
||||
&& !device.ui.config.input.gb_rom_path[i].is_empty()
|
||||
@@ -186,7 +215,7 @@ pub async fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
|
||||
{
|
||||
let username = args.ra_username.unwrap_or(ra_config.username.clone());
|
||||
retroachievements::init_client(
|
||||
if cfg!(ra_hardcore_enabled) {
|
||||
if cfg!(ra_hardcore_enabled) && args.netplay_server_addr.is_none() {
|
||||
ra_config.hardcore
|
||||
} else {
|
||||
false
|
||||
@@ -204,12 +233,29 @@ pub async fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
|
||||
tx.send(false).unwrap();
|
||||
}
|
||||
|
||||
rx.await.unwrap();
|
||||
rx.blocking_recv().unwrap();
|
||||
|
||||
ra_config
|
||||
} else {
|
||||
retroachievements::RAConfig::default()
|
||||
};
|
||||
|
||||
let netplay_config = if let Some(server_addr) = args.netplay_server_addr
|
||||
&& let Some(player_number) = args.netplay_player_number
|
||||
&& let Some(number_of_players) = args.netplay_number_of_players
|
||||
&& let Some(input_delay) = args.netplay_input_delay
|
||||
{
|
||||
Some(netplay::NetplayConfig {
|
||||
server_addr,
|
||||
player_number,
|
||||
number_of_players,
|
||||
input_delay,
|
||||
ice_config_path: dirs.cache_dir.join("ice_config.json"),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
device::run_game(
|
||||
&mut device,
|
||||
&rom_contents,
|
||||
@@ -220,16 +266,14 @@ pub async fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
|
||||
load_savestate_slot: args.load_state,
|
||||
},
|
||||
ra_config,
|
||||
)
|
||||
.await;
|
||||
netplay_config,
|
||||
);
|
||||
|
||||
// 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 {
|
||||
if device.netplay.is_none() {
|
||||
for i in 0..4 {
|
||||
if device.ui.config.input.transfer_pak[i]
|
||||
&& !device.ui.config.input.gb_ram_path[i].is_empty()
|
||||
@@ -244,16 +288,16 @@ pub async fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
ui::usb::close(shutdown_tx, usb_handle).await;
|
||||
ui::usb::close(shutdown_tx, usb_handle);
|
||||
} 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(
|
||||
ui::input_profile::configure_input_profile(
|
||||
&mut config,
|
||||
profile,
|
||||
args.use_dinput,
|
||||
args.deadzone.unwrap_or(ui::input::DEADZONE_DEFAULT),
|
||||
args.deadzone.unwrap_or(ui::input_profile::DEADZONE_DEFAULT),
|
||||
);
|
||||
|
||||
ui::sdl_close();
|
||||
@@ -297,7 +341,10 @@ pub async fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
|
||||
#[cfg(feature = "gui")]
|
||||
{
|
||||
let app = ui::gui::AppWindow::new().unwrap();
|
||||
ui::gui::app_window(&app, false);
|
||||
let no_intro_map =
|
||||
std::sync::Arc::new(tokio::sync::Mutex::new(rustc_hash::FxHashMap::default()));
|
||||
set_app_id();
|
||||
ui::gui::app_window(&app, false, no_intro_map);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,8 +354,10 @@ pub async fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[unsafe(no_mangle)]
|
||||
#[tokio::main(worker_threads = 4)]
|
||||
async fn android_main(app: slint::android::AndroidApp) {
|
||||
fn android_main(app: slint::android::AndroidApp) {
|
||||
let (close_tx, handle) = create_runtime();
|
||||
let _guard = handle.enter();
|
||||
|
||||
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 { .. } => {
|
||||
@@ -316,7 +365,10 @@ async fn android_main(app: slint::android::AndroidApp) {
|
||||
&& let Some(weak_app_window) = weak_app_window.as_ref()
|
||||
{
|
||||
weak_app_window
|
||||
.upgrade_in_event_loop(move |handle| ui::gui::save_settings(&handle))
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
ui::gui::save_settings(&handle);
|
||||
handle.invoke_netplay_close();
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -337,7 +389,10 @@ async fn android_main(app: slint::android::AndroidApp) {
|
||||
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);
|
||||
let no_intro_map =
|
||||
std::sync::Arc::new(tokio::sync::Mutex::new(rustc_hash::FxHashMap::default()));
|
||||
ui::gui::app_window(&app_window, true, no_intro_map);
|
||||
close_tx.send(()).unwrap();
|
||||
*android::WEAK_SLINT_WINDOW.lock().unwrap() = None;
|
||||
*android::ANDROID_APP.lock().unwrap() = None;
|
||||
}
|
||||
|
||||
+7
-3
@@ -5,8 +5,12 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
#[tokio::main(worker_threads = 4)]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
fn main() -> std::io::Result<()> {
|
||||
let (close_tx, handle) = gopher64::create_runtime();
|
||||
let _guard = handle.enter();
|
||||
|
||||
let args = gopher64::Args::parse();
|
||||
gopher64::run(args, std::env::args().count()).await
|
||||
let result = gopher64::run(args, std::env::args().count());
|
||||
close_tx.send(()).unwrap();
|
||||
result
|
||||
}
|
||||
|
||||
+528
-303
@@ -1,349 +1,574 @@
|
||||
use crate::{device, ui};
|
||||
use std::io::{Read, Write};
|
||||
//UDP packet formats
|
||||
const UDP_SEND_KEY_INFO: u8 = 0;
|
||||
const UDP_RECEIVE_KEY_INFO: u8 = 1;
|
||||
const UDP_REQUEST_KEY_INFO: u8 = 2;
|
||||
const UDP_RECEIVE_KEY_INFO_GRATUITOUS: u8 = 3;
|
||||
const UDP_SYNC_DATA: u8 = 4;
|
||||
use crate::device;
|
||||
use crate::savestates;
|
||||
use crate::ui;
|
||||
use sha2::digest::Digest;
|
||||
|
||||
//TCP packet formats
|
||||
const TCP_SEND_SAVE: u8 = 1;
|
||||
//const TCP_RECEIVE_SAVE: u8 = 2;
|
||||
//const TCP_SEND_SETTINGS: u8 = 3;
|
||||
//const TCP_RECEIVE_SETTINGS: u8 = 4;
|
||||
const TCP_REGISTER_PLAYER: u8 = 5;
|
||||
const TCP_GET_REGISTRATION: u8 = 6;
|
||||
const TCP_DISCONNECT_NOTICE: u8 = 7;
|
||||
const TCP_RECEIVE_SAVE_WITH_SIZE: u8 = 8;
|
||||
const TCP_SEND_RTC: u8 = 64;
|
||||
const TCP_RECEIVE_RTC: u8 = 128;
|
||||
const TCP_SEND_RNG: u8 = 65;
|
||||
const TCP_RECEIVE_RNG: u8 = 129;
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RtcIceServerConfig {
|
||||
pub urls: Vec<String>,
|
||||
pub username: Option<String>,
|
||||
pub credential: Option<String>,
|
||||
}
|
||||
|
||||
const CS4: u32 = 32;
|
||||
pub struct GgrsConfig;
|
||||
impl ggrs::Config for GgrsConfig {
|
||||
type Input = ui::input::InputData;
|
||||
type InputPredictor = ggrs::PredictRepeatLast;
|
||||
type State = i32;
|
||||
type Address = matchbox_socket::PeerId;
|
||||
}
|
||||
|
||||
pub struct MatchboxChannel {
|
||||
channel: matchbox_socket::WebRtcChannel,
|
||||
}
|
||||
|
||||
impl ggrs::NonBlockingSocket<matchbox_socket::PeerId> for MatchboxChannel {
|
||||
fn send_to(&mut self, msg: &ggrs::Message, addr: &matchbox_socket::PeerId) {
|
||||
let encoded = postcard::to_stdvec(msg).expect("serialization failed");
|
||||
let _ = self.channel.try_send(encoded.into(), *addr);
|
||||
}
|
||||
|
||||
fn receive_all_messages(&mut self) -> Vec<(matchbox_socket::PeerId, ggrs::Message)> {
|
||||
self.channel
|
||||
.receive()
|
||||
.iter()
|
||||
.filter_map(|(peer, packet)| {
|
||||
let msg = postcard::from_bytes::<ggrs::Message>(packet).ok()?;
|
||||
Some((*peer, msg))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NetplayConfig {
|
||||
pub server_addr: String,
|
||||
pub player_number: usize,
|
||||
pub number_of_players: usize,
|
||||
pub input_delay: usize,
|
||||
pub ice_config_path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
pub struct Netplay {
|
||||
udp_socket: std::net::UdpSocket,
|
||||
tcp_stream: std::net::TcpStream,
|
||||
pub player_number: u8,
|
||||
pub player_data: [PlayerData; 4],
|
||||
vi_counter: u32,
|
||||
status: u8,
|
||||
buffer_target: u8,
|
||||
pub fast_forward: bool,
|
||||
pub disconnected: bool,
|
||||
pub session: ggrs::P2PSession<GgrsConfig>,
|
||||
pub reliable_channel: matchbox_socket::WebRtcChannel,
|
||||
pub peers: Vec<matchbox_socket::PeerId>,
|
||||
pub player_number: usize,
|
||||
pub connected: [bool; 4],
|
||||
pub input_delay: usize,
|
||||
pub messages: std::collections::HashMap<String, Vec<u8>>,
|
||||
pub received_data: std::collections::VecDeque<Vec<u8>>,
|
||||
pub inputs: Vec<(ui::input::InputData, ggrs::InputStatus)>,
|
||||
pub requests: std::collections::VecDeque<ggrs::GgrsRequest<GgrsConfig>>,
|
||||
pub incoming_message: Vec<u8>,
|
||||
pub ice_config_path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
pub struct PlayerData {
|
||||
lag: u8,
|
||||
count: u32,
|
||||
pub reg_id: u32,
|
||||
input_events: std::collections::HashMap<u32, InputEvent>,
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct NetplayMessage {
|
||||
name: String,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
struct InputEvent {
|
||||
input: u32,
|
||||
plugin: u8,
|
||||
fn send_message(netplay: &mut Netplay, message: NetplayMessage) {
|
||||
let data = postcard::to_stdvec(&message).unwrap();
|
||||
let chunks = data.chunks(16384).collect::<Vec<&[u8]>>();
|
||||
for peer in netplay.peers.iter() {
|
||||
for chunk in chunks.iter() {
|
||||
if let Err(e) = netplay
|
||||
.reliable_channel
|
||||
.try_send(chunk.to_vec().into(), *peer)
|
||||
{
|
||||
eprintln!("Failed to send message: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_reliable_messages(netplay: &mut Netplay) {
|
||||
netplay.received_data.extend(
|
||||
netplay
|
||||
.reliable_channel
|
||||
.receive()
|
||||
.iter()
|
||||
.map(|(_, data)| data.to_vec()),
|
||||
);
|
||||
|
||||
while !netplay.received_data.is_empty() {
|
||||
if let Some(data) = netplay.received_data.pop_front() {
|
||||
netplay.incoming_message.extend(data);
|
||||
|
||||
if let Ok(decoded_message) =
|
||||
postcard::from_bytes::<NetplayMessage>(&netplay.incoming_message)
|
||||
{
|
||||
netplay
|
||||
.messages
|
||||
.insert(decoded_message.name, decoded_message.data);
|
||||
netplay.incoming_message.clear();
|
||||
check_input_delay(netplay);
|
||||
check_disconnect(netplay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn receive_message(netplay: &mut Netplay, name: &str) -> Vec<u8> {
|
||||
let timeout = std::time::Duration::from_secs(10);
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
loop {
|
||||
process_reliable_messages(netplay);
|
||||
if let Some(data) = netplay.messages.remove(name) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if now.elapsed() > timeout {
|
||||
panic!("Could not receive message for {name}");
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
}
|
||||
}
|
||||
|
||||
fn send_player_number(
|
||||
channel: &mut matchbox_socket::WebRtcChannel,
|
||||
peers: &[matchbox_socket::PeerId],
|
||||
player_number: usize,
|
||||
) {
|
||||
let message = NetplayMessage {
|
||||
name: "player_number".to_string(),
|
||||
data: (player_number as u64).to_be_bytes().to_vec(),
|
||||
};
|
||||
let data = postcard::to_stdvec(&message).unwrap();
|
||||
for peer in peers {
|
||||
if let Err(e) = channel.try_send(data.clone().into(), *peer) {
|
||||
eprintln!("Failed to send message: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_player_numbers(
|
||||
channel: &mut matchbox_socket::WebRtcChannel,
|
||||
player_numbers: &mut std::collections::BTreeMap<usize, Option<matchbox_socket::PeerId>>,
|
||||
) {
|
||||
for (peer, data) in channel.receive() {
|
||||
let message = postcard::from_bytes::<NetplayMessage>(&data).unwrap();
|
||||
if message.name == "player_number" {
|
||||
player_numbers.insert(
|
||||
u64::from_be_bytes(message.data.try_into().unwrap()) as usize,
|
||||
Some(peer),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_rtc(netplay: &mut Netplay, rtc: i64) {
|
||||
let mut request: Vec<u8> = [TCP_SEND_RTC].to_vec();
|
||||
let size: u32 = 8;
|
||||
request.extend_from_slice(&size.to_be_bytes());
|
||||
request.extend_from_slice(&rtc.to_be_bytes());
|
||||
netplay.tcp_stream.write_all(&request).unwrap();
|
||||
let message = NetplayMessage {
|
||||
name: "rtc".to_string(),
|
||||
data: rtc.to_be_bytes().to_vec(),
|
||||
};
|
||||
send_message(netplay, message);
|
||||
}
|
||||
|
||||
pub fn receive_rtc(netplay: &mut Netplay) -> i64 {
|
||||
let request: Vec<u8> = [TCP_RECEIVE_RTC].to_vec();
|
||||
netplay.tcp_stream.write_all(&request).unwrap();
|
||||
let mut rtc: [u8; 8] = [0; 8];
|
||||
netplay.tcp_stream.read_exact(&mut rtc).unwrap();
|
||||
i64::from_be_bytes(rtc)
|
||||
let message = receive_message(netplay, "rtc");
|
||||
|
||||
i64::from_be_bytes(message.try_into().unwrap())
|
||||
}
|
||||
|
||||
pub fn send_rng(netplay: &mut Netplay, seed: u64) {
|
||||
let mut request: Vec<u8> = [TCP_SEND_RNG].to_vec();
|
||||
let size: u32 = 8;
|
||||
request.extend_from_slice(&size.to_be_bytes());
|
||||
request.extend_from_slice(&seed.to_be_bytes());
|
||||
netplay.tcp_stream.write_all(&request).unwrap();
|
||||
let message = NetplayMessage {
|
||||
name: "rng".to_string(),
|
||||
data: seed.to_be_bytes().to_vec(),
|
||||
};
|
||||
send_message(netplay, message);
|
||||
}
|
||||
|
||||
pub fn receive_rng(netplay: &mut Netplay) -> u64 {
|
||||
let request: Vec<u8> = [TCP_RECEIVE_RNG].to_vec();
|
||||
netplay.tcp_stream.write_all(&request).unwrap();
|
||||
let mut seed: [u8; 8] = [0; 8];
|
||||
netplay.tcp_stream.read_exact(&mut seed).unwrap();
|
||||
u64::from_be_bytes(seed)
|
||||
let message = receive_message(netplay, "rng");
|
||||
u64::from_be_bytes(message.try_into().unwrap())
|
||||
}
|
||||
|
||||
pub fn send_save(netplay: &mut Netplay, save_type: &str, save_data: &[u8], size: usize) {
|
||||
let mut request: Vec<u8> = [TCP_SEND_SAVE].to_vec();
|
||||
request.extend_from_slice(save_type.as_bytes());
|
||||
request.push(0); // null terminate string
|
||||
request.extend_from_slice(&(size as u32).to_be_bytes());
|
||||
|
||||
if size > 0 {
|
||||
request.extend_from_slice(save_data);
|
||||
}
|
||||
netplay.tcp_stream.write_all(&request).unwrap();
|
||||
pub fn send_save(netplay: &mut Netplay, save_type: &str, save_data: &[u8]) {
|
||||
let message = NetplayMessage {
|
||||
name: save_type.to_string(),
|
||||
data: save_data.to_vec(),
|
||||
};
|
||||
send_message(netplay, message);
|
||||
}
|
||||
|
||||
pub fn receive_save(netplay: &mut Netplay, save_type: &str, save_data: &mut Vec<u8>) {
|
||||
let mut request: Vec<u8> = [TCP_RECEIVE_SAVE_WITH_SIZE].to_vec();
|
||||
request.extend_from_slice(save_type.as_bytes());
|
||||
request.push(0); // null terminate string
|
||||
netplay.tcp_stream.write_all(&request).unwrap();
|
||||
|
||||
let mut size: [u8; 4] = [0; 4];
|
||||
netplay.tcp_stream.read_exact(&mut size).unwrap();
|
||||
let mut response: Vec<u8> = vec![0; u32::from_be_bytes(size) as usize];
|
||||
netplay.tcp_stream.read_exact(&mut response).unwrap();
|
||||
*save_data = response;
|
||||
let message = receive_message(netplay, save_type);
|
||||
*save_data = message;
|
||||
}
|
||||
|
||||
pub fn send_sync_check(netplay: &mut Netplay, regs: &[u64]) {
|
||||
if netplay.vi_counter.is_multiple_of(600) {
|
||||
let mut request: Vec<u8> = [UDP_SYNC_DATA].to_vec();
|
||||
request.extend_from_slice(&(netplay.vi_counter).to_be_bytes());
|
||||
|
||||
for item in regs.iter() {
|
||||
request.extend_from_slice(&(*item as u32).to_be_bytes());
|
||||
}
|
||||
|
||||
netplay.udp_socket.send(&request).unwrap();
|
||||
pub fn send_input_delay(netplay: &mut Netplay, input_delay: usize) {
|
||||
if input_delay < 1 {
|
||||
return;
|
||||
}
|
||||
netplay.vi_counter = netplay.vi_counter.wrapping_add(1);
|
||||
|
||||
let message = NetplayMessage {
|
||||
name: "input_delay".to_string(),
|
||||
data: (input_delay as u64).to_be_bytes().to_vec(),
|
||||
};
|
||||
send_message(netplay, message);
|
||||
change_input_delay(netplay, input_delay);
|
||||
}
|
||||
|
||||
pub fn send_input(netplay: &Netplay, input: ui::input::InputData) {
|
||||
let mut request: Vec<u8> = [UDP_SEND_KEY_INFO].to_vec();
|
||||
request.push(netplay.player_number);
|
||||
request.extend_from_slice(
|
||||
&(netplay.player_data[netplay.player_number as usize].count).to_be_bytes(),
|
||||
);
|
||||
request.extend_from_slice(&(input.data).to_be_bytes());
|
||||
request.push(input.pak_change_pressed as u8);
|
||||
netplay.udp_socket.send(&request).unwrap();
|
||||
}
|
||||
|
||||
pub fn get_input(device: &mut device::Device, channel: usize) -> ui::input::InputData {
|
||||
let netplay = device.netplay.as_mut().unwrap();
|
||||
let mut input = None;
|
||||
|
||||
let timeout = std::time::Instant::now() + std::time::Duration::from_secs(10);
|
||||
let mut request_timer = std::time::Instant::now() - std::time::Duration::from_millis(5);
|
||||
while input.is_none() {
|
||||
process_incoming(netplay); // we execute process_incoming before request_input so that we send an accurate buffer count
|
||||
if std::time::Instant::now() > request_timer {
|
||||
// sends a request packet every 5ms
|
||||
request_input(netplay, channel);
|
||||
request_timer = std::time::Instant::now() + std::time::Duration::from_millis(5);
|
||||
}
|
||||
input = netplay.player_data[channel]
|
||||
.input_events
|
||||
.remove(&netplay.player_data[channel].count);
|
||||
|
||||
if std::time::Instant::now() > timeout {
|
||||
fn change_input_delay(netplay: &mut Netplay, input_delay: usize) {
|
||||
netplay.input_delay = input_delay;
|
||||
for handle in netplay.session.local_player_handles() {
|
||||
if let Err(e) = netplay.session.set_input_delay(handle, input_delay) {
|
||||
eprintln!("Error setting input delay: {}", e);
|
||||
} else {
|
||||
ui::video::onscreen_message(
|
||||
"Lost connection to netplay server",
|
||||
&format!("Input delay: {}", input_delay),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_input_delay(netplay: &mut Netplay) {
|
||||
if let Some(data) = netplay.messages.remove("input_delay") {
|
||||
let input_delay = u64::from_be_bytes(data.try_into().unwrap()) as usize;
|
||||
if input_delay != netplay.input_delay {
|
||||
change_input_delay(netplay, input_delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_disconnect(netplay: &mut Netplay) {
|
||||
if !netplay.disconnected && netplay.messages.remove("disconnect").is_some() {
|
||||
netplay.disconnected = true;
|
||||
ui::video::onscreen_message(
|
||||
"Player disconnected, session has ended",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_LONG,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_frames(netplay: &Netplay) -> usize {
|
||||
netplay
|
||||
.requests
|
||||
.iter()
|
||||
.filter(|r| matches!(r, ggrs::GgrsRequest::AdvanceFrame { .. }))
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn in_rollback(netplay: Option<&Netplay>) -> bool {
|
||||
if let Some(netplay) = netplay {
|
||||
pending_frames(netplay) != 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_requests(
|
||||
device: &mut device::Device,
|
||||
) -> Vec<(ui::input::InputData, ggrs::InputStatus)> {
|
||||
loop {
|
||||
if let Some(request) = device.netplay.as_mut().unwrap().requests.pop_front() {
|
||||
match request {
|
||||
ggrs::GgrsRequest::SaveGameState { cell, frame } => {
|
||||
savestates::create_savestate(device, true, Some(frame));
|
||||
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
for reg in device.cpu.cop0.regs.as_ref() {
|
||||
hasher.update(reg.to_be_bytes());
|
||||
}
|
||||
let hash = u128::from_be_bytes(hasher.finalize()[..16].try_into().unwrap());
|
||||
cell.save(frame, Some(frame), Some(hash));
|
||||
}
|
||||
ggrs::GgrsRequest::LoadGameState { cell, frame: _ } => {
|
||||
eprintln!("attempting to load game state");
|
||||
if let Some(frame) = cell.load() {
|
||||
savestates::load_savestate(device, true, Some(frame));
|
||||
}
|
||||
}
|
||||
ggrs::GgrsRequest::AdvanceFrame { inputs } => {
|
||||
return inputs;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
process_netplay(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_clients(netplay: &mut Netplay) {
|
||||
netplay.session.poll_remote_clients();
|
||||
for event in netplay.session.events() {
|
||||
match event {
|
||||
ggrs::GgrsEvent::Synchronizing { .. } => {}
|
||||
ggrs::GgrsEvent::Synchronized { .. } => {}
|
||||
ggrs::GgrsEvent::Disconnected { .. } => {
|
||||
if !netplay.disconnected {
|
||||
netplay.disconnected = true;
|
||||
ui::video::onscreen_message(
|
||||
"Lost connection to peer(s)",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_LONG,
|
||||
);
|
||||
}
|
||||
}
|
||||
ggrs::GgrsEvent::NetworkInterrupted { .. } => {
|
||||
println!("network interrupted");
|
||||
}
|
||||
ggrs::GgrsEvent::NetworkResumed { .. } => {
|
||||
println!("network resumed");
|
||||
}
|
||||
ggrs::GgrsEvent::WaitRecommendation { skip_frames } => {
|
||||
println!("wait recommendation: skip_frames={}", skip_frames);
|
||||
}
|
||||
ggrs::GgrsEvent::DesyncDetected { .. } => {
|
||||
eprintln!("desync detected");
|
||||
ui::video::onscreen_message(
|
||||
"Desync detected",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_LONG,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_netplay(device: &mut device::Device) {
|
||||
let netplay = device.netplay.as_mut().unwrap();
|
||||
|
||||
poll_clients(netplay);
|
||||
process_reliable_messages(netplay);
|
||||
advance_frame(device);
|
||||
}
|
||||
|
||||
fn advance_frame(device: &mut device::Device) {
|
||||
let netplay = device.netplay.as_mut().unwrap();
|
||||
let local_input = ui::input::get(&mut device.ui, 0);
|
||||
let local_handle = *netplay.session.local_player_handles().first().unwrap();
|
||||
netplay
|
||||
.session
|
||||
.add_local_input(local_handle, local_input)
|
||||
.unwrap();
|
||||
|
||||
if netplay.disconnected {
|
||||
netplay.requests.push_back(ggrs::GgrsRequest::AdvanceFrame {
|
||||
inputs: vec![
|
||||
(
|
||||
ui::input::InputData::default(),
|
||||
ggrs::InputStatus::Disconnected
|
||||
);
|
||||
4
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
match netplay.session.advance_frame() {
|
||||
Ok(requests) => {
|
||||
netplay.requests.extend(requests);
|
||||
}
|
||||
Err(ggrs::GgrsError::PredictionThreshold) => {
|
||||
println!("prediction threshold reached");
|
||||
}
|
||||
Err(e) => panic!("{e}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_peers(
|
||||
peers: &[matchbox_socket::PeerId],
|
||||
player_numbers: &std::collections::BTreeMap<usize, Option<matchbox_socket::PeerId>>,
|
||||
) -> bool {
|
||||
for peer in player_numbers.values() {
|
||||
if let Some(peer) = peer
|
||||
&& !peers.contains(peer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn create_socket(builder: matchbox_socket::WebRtcSocketBuilder) -> matchbox_socket::WebRtcSocket {
|
||||
let (socket, loop_fut) = builder.build();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = loop_fut.await {
|
||||
eprintln!("WebRTC loop failed: {}", e);
|
||||
}
|
||||
});
|
||||
socket
|
||||
}
|
||||
|
||||
pub fn init(
|
||||
device: &mut device::Device,
|
||||
netplay_config: &NetplayConfig,
|
||||
pal: bool,
|
||||
) -> Option<Netplay> {
|
||||
let mut builder = matchbox_socket::WebRtcSocketBuilder::new(&netplay_config.server_addr)
|
||||
.add_unreliable_channel()
|
||||
.add_reliable_channel();
|
||||
|
||||
if let Ok(ice_config) = std::fs::read(&netplay_config.ice_config_path)
|
||||
&& let Ok(ice_config) = serde_json::from_slice::<RtcIceServerConfig>(&ice_config)
|
||||
{
|
||||
builder = builder.ice_server(matchbox_socket::RtcIceServerConfig {
|
||||
urls: ice_config.urls,
|
||||
username: ice_config.username,
|
||||
credential: ice_config.credential,
|
||||
});
|
||||
} else {
|
||||
eprintln!("Using default ICE config");
|
||||
}
|
||||
|
||||
let mut socket = create_socket(builder.clone());
|
||||
|
||||
let mut now = std::time::Instant::now();
|
||||
let mut message_timer = now;
|
||||
let socket_timeout = std::time::Duration::from_secs_f64(rand::random_range(8.0..10.0));
|
||||
let mut player_numbers =
|
||||
std::collections::BTreeMap::from([(netplay_config.player_number, None)]);
|
||||
|
||||
ui::video::onscreen_message(
|
||||
"Connecting to netplay peers...\nPlease wait...",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
|
||||
device.cpu.running = true;
|
||||
while device.cpu.running {
|
||||
if socket
|
||||
.update_peers()
|
||||
.iter()
|
||||
.any(|(_, peer_state)| *peer_state == matchbox_socket::PeerState::Disconnected)
|
||||
{
|
||||
// if someone has disconnected, reset the timeout
|
||||
now = std::time::Instant::now();
|
||||
}
|
||||
|
||||
let connected_peers = socket
|
||||
.connected_peers()
|
||||
.collect::<Vec<matchbox_socket::PeerId>>();
|
||||
|
||||
if connected_peers.len() == netplay_config.number_of_players - 1 {
|
||||
send_player_number(
|
||||
socket.channel_mut(1),
|
||||
&connected_peers,
|
||||
netplay_config.player_number,
|
||||
);
|
||||
get_player_numbers(socket.channel_mut(1), &mut player_numbers);
|
||||
if player_numbers.len() == netplay_config.number_of_players
|
||||
&& verify_peers(&connected_peers, &player_numbers)
|
||||
{
|
||||
break;
|
||||
}
|
||||
} else if now.elapsed() > socket_timeout {
|
||||
socket.close();
|
||||
player_numbers.retain(|_, peer| peer.is_none());
|
||||
socket = create_socket(builder.clone());
|
||||
now = std::time::Instant::now();
|
||||
}
|
||||
|
||||
if message_timer.elapsed() > std::time::Duration::from_secs(4) {
|
||||
ui::video::onscreen_message(
|
||||
&format!(
|
||||
"Still connecting to {} netplay peer(s)...\nPlease wait...",
|
||||
netplay_config.number_of_players - player_numbers.len()
|
||||
),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
input = Some(InputEvent {
|
||||
input: 0,
|
||||
plugin: 0,
|
||||
});
|
||||
device.cpu.running = false;
|
||||
message_timer = std::time::Instant::now();
|
||||
}
|
||||
|
||||
ui::video::render_frame();
|
||||
ui::video::update_screen();
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
ui::video::check_callback(device);
|
||||
}
|
||||
if !device.cpu.running {
|
||||
// user closed the window
|
||||
return None;
|
||||
}
|
||||
device.cpu.running = false;
|
||||
|
||||
let mut session_builder = ggrs::SessionBuilder::<GgrsConfig>::new()
|
||||
.with_num_players(netplay_config.number_of_players)
|
||||
.unwrap()
|
||||
.with_input_delay(netplay_config.input_delay)
|
||||
.with_fps(if pal { 50 } else { 60 })
|
||||
.unwrap()
|
||||
.with_desync_detection_mode(ggrs::DesyncDetection::On { interval: 60 })
|
||||
.with_max_prediction_window(0)
|
||||
.with_disconnect_timeout(std::time::Duration::from_secs(if cfg!(debug_assertions) {
|
||||
10
|
||||
} else {
|
||||
5
|
||||
}));
|
||||
|
||||
let mut peers = vec![];
|
||||
for (i, peer) in player_numbers.iter() {
|
||||
if let Some(peer) = peer {
|
||||
session_builder = session_builder
|
||||
.add_player(ggrs::PlayerType::Remote(*peer), *i)
|
||||
.unwrap();
|
||||
peers.push(*peer);
|
||||
} else {
|
||||
session_builder = session_builder
|
||||
.add_player(ggrs::PlayerType::Local, *i)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
netplay.fast_forward = netplay.player_data[channel].lag > 0
|
||||
&& netplay.player_data[channel].input_events.len() as u8 > netplay.buffer_target;
|
||||
|
||||
netplay.player_data[channel].count = netplay.player_data[channel].count.wrapping_add(1);
|
||||
ui::input::InputData {
|
||||
data: input.as_ref().unwrap().input,
|
||||
pak_change_pressed: input.as_ref().unwrap().plugin != 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn request_input(netplay: &Netplay, channel: usize) {
|
||||
let mut request: Vec<u8> = [UDP_REQUEST_KEY_INFO].to_vec();
|
||||
request.push(channel as u8); //The player we need input for
|
||||
request.extend_from_slice(
|
||||
&(netplay.player_data[netplay.player_number as usize].reg_id).to_be_bytes(),
|
||||
);
|
||||
request.extend_from_slice(&(netplay.player_data[channel].count).to_be_bytes());
|
||||
request.push(0); //spectator mode
|
||||
request.push(netplay.player_data[channel].input_events.len() as u8);
|
||||
netplay.udp_socket.send(&request).unwrap();
|
||||
}
|
||||
|
||||
fn process_incoming(netplay: &mut Netplay) {
|
||||
let mut buf: [u8; 1024] = [0; 1024];
|
||||
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;
|
||||
//current_status is a status update from the server
|
||||
//it will let us know if another player has disconnected, or the games have desynced
|
||||
let current_status = buf[2];
|
||||
if buf[0] == UDP_RECEIVE_KEY_INFO {
|
||||
netplay.player_data[player].lag = buf[3];
|
||||
}
|
||||
if current_status != netplay.status {
|
||||
if ((current_status & 0x1) ^ (netplay.status & 0x1)) != 0 {
|
||||
ui::video::onscreen_message(
|
||||
"Netplay desync detected",
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_LONG,
|
||||
);
|
||||
}
|
||||
for dis in 1..5 {
|
||||
if ((current_status & (0x1 << dis)) ^ (netplay.status & (0x1 << dis))) != 0
|
||||
{
|
||||
ui::video::onscreen_message(
|
||||
&format!("Player {dis} disconnected"),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
}
|
||||
}
|
||||
netplay.status = current_status;
|
||||
}
|
||||
|
||||
let mut buffer_offset = 5;
|
||||
for _i in 0..buf[4] {
|
||||
let count = u32::from_be_bytes(
|
||||
buf[buffer_offset..buffer_offset + 4].try_into().unwrap(),
|
||||
);
|
||||
buffer_offset += 4;
|
||||
|
||||
if (count.wrapping_sub(netplay.player_data[player].count)) > (u32::MAX / 2) {
|
||||
//event doesn't need to be recorded
|
||||
buffer_offset += 5;
|
||||
continue;
|
||||
}
|
||||
|
||||
let input = u32::from_be_bytes(
|
||||
buf[buffer_offset..buffer_offset + 4].try_into().unwrap(),
|
||||
);
|
||||
buffer_offset += 4;
|
||||
let plugin = buf[buffer_offset];
|
||||
buffer_offset += 1;
|
||||
let input_event = InputEvent { input, plugin };
|
||||
netplay.player_data[player]
|
||||
.input_events
|
||||
.insert(count, input_event);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
panic!("unknown UDP packet")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(peer_addr: std::net::SocketAddr, player_number: u8) -> Netplay {
|
||||
let udp_socket;
|
||||
if peer_addr.is_ipv4() {
|
||||
udp_socket = std::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0))
|
||||
.expect("couldn't bind to address");
|
||||
socket2::SockRef::from(&udp_socket)
|
||||
.set_tos_v4(CS4 << 2)
|
||||
.unwrap();
|
||||
} else {
|
||||
udp_socket = std::net::UdpSocket::bind((std::net::Ipv6Addr::UNSPECIFIED, 0))
|
||||
.expect("couldn't bind to address");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
socket2::SockRef::from(&udp_socket)
|
||||
.set_tclass_v6(CS4 << 2)
|
||||
.unwrap();
|
||||
let matchbox_channel = MatchboxChannel {
|
||||
channel: socket.take_channel(0).unwrap(),
|
||||
};
|
||||
let reliable_channel = socket.take_channel(1).unwrap();
|
||||
|
||||
udp_socket.connect(peer_addr).unwrap();
|
||||
udp_socket.set_nonblocking(true).unwrap();
|
||||
|
||||
let mut stream = std::net::TcpStream::connect(peer_addr).unwrap();
|
||||
|
||||
let regid = (player_number + 1) as u32;
|
||||
let mut request: [u8; 8] = [
|
||||
TCP_REGISTER_PLAYER,
|
||||
player_number,
|
||||
0, //plugin/pak
|
||||
0, //rawdata
|
||||
0, //regid (u32)
|
||||
0, //regid (u32)
|
||||
0, //regid (u32)
|
||||
0, //regid (u32)
|
||||
];
|
||||
|
||||
request[4..8].copy_from_slice(®id.to_be_bytes());
|
||||
stream.write_all(&request).unwrap();
|
||||
|
||||
let mut response: [u8; 2] = [0, 0];
|
||||
stream.read_exact(&mut response).unwrap();
|
||||
if response[0] != 1 {
|
||||
panic!("Failed to register player");
|
||||
if matchbox_channel.channel.config().max_retransmits != Some(0)
|
||||
|| matchbox_channel.channel.config().ordered
|
||||
{
|
||||
eprintln!("Sending GGRS traffic over reliable channel");
|
||||
}
|
||||
let buffer_target = response[1];
|
||||
|
||||
let request: [u8; 1] = [TCP_GET_REGISTRATION];
|
||||
stream.write_all(&request).unwrap();
|
||||
let mut response: [u8; 24] = [0; 24];
|
||||
stream.read_exact(&mut response).unwrap();
|
||||
let mut session = session_builder.start_p2p_session(matchbox_channel).unwrap();
|
||||
|
||||
let mut reg_id: [u32; 4] = [0; 4];
|
||||
for i in 0..4 {
|
||||
// reg_id of 0 means no player connected
|
||||
reg_id[i] = u32::from_be_bytes(response[(i * 6)..(i * 6) + 4].try_into().unwrap());
|
||||
let now = std::time::Instant::now();
|
||||
let timeout = std::time::Duration::from_secs(10);
|
||||
while session.current_state() != ggrs::SessionState::Running {
|
||||
session.poll_remote_clients();
|
||||
if now.elapsed() > timeout {
|
||||
eprintln!("Could not start netplay session");
|
||||
return None;
|
||||
}
|
||||
unsafe { sdl3_sys::events::SDL_PumpEvents() };
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
Netplay {
|
||||
udp_socket,
|
||||
tcp_stream: stream,
|
||||
player_number,
|
||||
vi_counter: 0,
|
||||
status: 0,
|
||||
buffer_target,
|
||||
fast_forward: false,
|
||||
player_data: [
|
||||
PlayerData {
|
||||
lag: 0,
|
||||
count: 0,
|
||||
reg_id: reg_id[0],
|
||||
input_events: std::collections::HashMap::new(),
|
||||
},
|
||||
PlayerData {
|
||||
lag: 0,
|
||||
count: 0,
|
||||
reg_id: reg_id[1],
|
||||
input_events: std::collections::HashMap::new(),
|
||||
},
|
||||
PlayerData {
|
||||
lag: 0,
|
||||
count: 0,
|
||||
reg_id: reg_id[2],
|
||||
input_events: std::collections::HashMap::new(),
|
||||
},
|
||||
PlayerData {
|
||||
lag: 0,
|
||||
count: 0,
|
||||
reg_id: reg_id[3],
|
||||
input_events: std::collections::HashMap::new(),
|
||||
},
|
||||
|
||||
Some(Netplay {
|
||||
disconnected: false,
|
||||
incoming_message: vec![],
|
||||
input_delay: netplay_config.input_delay,
|
||||
session,
|
||||
reliable_channel,
|
||||
peers,
|
||||
player_number: netplay_config.player_number,
|
||||
connected: [
|
||||
netplay_config.number_of_players > 0,
|
||||
netplay_config.number_of_players > 1,
|
||||
netplay_config.number_of_players > 2,
|
||||
netplay_config.number_of_players > 3,
|
||||
],
|
||||
inputs: Vec::new(),
|
||||
requests: std::collections::VecDeque::new(),
|
||||
received_data: std::collections::VecDeque::new(),
|
||||
messages: std::collections::HashMap::new(),
|
||||
ice_config_path: netplay_config.ice_config_path.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn close(netplay: &mut Netplay) {
|
||||
if !netplay.disconnected {
|
||||
let message = NetplayMessage {
|
||||
name: "disconnect".to_string(),
|
||||
data: vec![],
|
||||
};
|
||||
send_message(netplay, message);
|
||||
std::thread::sleep(std::time::Duration::from_millis(200)); // give the message time to be sent
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(device: &mut device::Device) {
|
||||
let netplay = device.netplay.as_mut().unwrap();
|
||||
let regid = (netplay.player_number + 1) as u32;
|
||||
let mut request: [u8; 5] = [TCP_DISCONNECT_NOTICE, 0, 0, 0, 0];
|
||||
request[1..5].copy_from_slice(®id.to_be_bytes());
|
||||
|
||||
netplay.tcp_stream.write_all(&request).unwrap();
|
||||
netplay.tcp_stream.flush().unwrap();
|
||||
netplay
|
||||
.tcp_stream
|
||||
.shutdown(std::net::Shutdown::Both)
|
||||
.unwrap();
|
||||
let _ = std::fs::remove_file(&netplay.ice_config_path);
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ pub extern "C" fn rust_server_call(
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn load_game(
|
||||
pub fn load_game(
|
||||
rom: &[u8],
|
||||
rom_size: usize,
|
||||
discord_rich_presence: bool,
|
||||
@@ -150,7 +150,8 @@ pub async fn load_game(
|
||||
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();
|
||||
|
||||
rx.blocking_recv().unwrap();
|
||||
|
||||
let mut c_title = std::ptr::null();
|
||||
let mut c_image_url = std::ptr::null();
|
||||
@@ -177,7 +178,7 @@ pub async fn load_game(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn unload_game(
|
||||
pub fn unload_game(
|
||||
discord_watch_tx: Option<tokio::sync::watch::Sender<()>>,
|
||||
discord_handle: Option<tokio::task::JoinHandle<()>>,
|
||||
) {
|
||||
@@ -185,12 +186,14 @@ pub async fn unload_game(
|
||||
&& 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();
|
||||
}
|
||||
tokio::task::spawn(async move {
|
||||
if tokio::time::timeout(std::time::Duration::from_secs(1), &mut discord_handle)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
discord_handle.abort();
|
||||
}
|
||||
});
|
||||
}
|
||||
unsafe { ra_unload_game() };
|
||||
}
|
||||
|
||||
+278
-144
@@ -5,6 +5,7 @@ use serde::de::{Deserialize, Deserializer, SeqAccess, Visitor};
|
||||
use serde::ser::{Serialize, SerializeSeq, Serializer};
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
use std::arch::x86_64::*;
|
||||
use std::ops::Deref;
|
||||
|
||||
struct M128iArrayVisitor<const N: usize>;
|
||||
|
||||
@@ -69,48 +70,137 @@ where
|
||||
seq.end()
|
||||
}
|
||||
|
||||
pub fn create_savestate(device: &device::Device) {
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Savestate {
|
||||
pub save_state: bool,
|
||||
pub load_state: bool,
|
||||
pub load_rewind: bool,
|
||||
pub last_rewind_saved: f64,
|
||||
#[serde(skip)]
|
||||
pub rewind_pool:
|
||||
std::sync::Arc<tokio::sync::Mutex<std::collections::BTreeMap<i32, SavestateData>>>,
|
||||
}
|
||||
|
||||
pub struct SavestateData {
|
||||
device: Box<device::Device>,
|
||||
saves: ui::storage::Saves,
|
||||
rdp_state: Vec<u8>,
|
||||
ra_state: Vec<u8>,
|
||||
}
|
||||
|
||||
static DEVICE_CLONE: std::sync::LazyLock<tokio::sync::Mutex<Box<device::Device>>> =
|
||||
std::sync::LazyLock::new(|| tokio::sync::Mutex::new(device::Device::new(false)));
|
||||
|
||||
static SAVES_CLONE: std::sync::LazyLock<tokio::sync::Mutex<ui::storage::Saves>> =
|
||||
std::sync::LazyLock::new(|| tokio::sync::Mutex::new(ui::storage::Saves::default()));
|
||||
|
||||
pub fn process_savestates(device: &mut device::Device) {
|
||||
let save_rewind = if device.netplay.is_none()
|
||||
&& device.ui.config.emulation.rewind
|
||||
&& device.vi.elapsed_time - device.savestate.last_rewind_saved > 1.0
|
||||
{
|
||||
device.savestate.last_rewind_saved = device.vi.elapsed_time;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if device.savestate.save_state || save_rewind {
|
||||
create_savestate(device, !device.savestate.save_state, None);
|
||||
device.savestate.save_state = false;
|
||||
} else if device.savestate.load_state || device.savestate.load_rewind {
|
||||
load_savestate(device, device.savestate.load_rewind, None);
|
||||
device.savestate.load_state = false;
|
||||
device.savestate.load_rewind = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_savestate(device: &mut device::Device, rewind: bool, rewind_frame: Option<i32>) {
|
||||
if !rewind {
|
||||
ui::video::idle();
|
||||
}
|
||||
|
||||
let mut rdp_state: Vec<u8> = vec![0; ui::video::state_size()];
|
||||
ui::video::save_state(rdp_state.as_mut_ptr());
|
||||
|
||||
let mut ra_state: Vec<u8> = vec![0; retroachievements::state_size()];
|
||||
retroachievements::save_state(ra_state.as_mut_ptr(), ra_state.len());
|
||||
|
||||
let save_path = device.ui.storage.paths.savestate_file_path.clone();
|
||||
DEVICE_CLONE.blocking_lock().clone_state(device);
|
||||
SAVES_CLONE
|
||||
.blocking_lock()
|
||||
.clone_from(&device.ui.storage.saves);
|
||||
|
||||
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);
|
||||
let save_path = device.ui.storage.paths.savestate_file_path.clone();
|
||||
let save_state_slot = device.ui.storage.save_state_slot;
|
||||
let rewind_pool = device.savestate.rewind_pool.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut error = false;
|
||||
|
||||
if rewind {
|
||||
let mut pool = rewind_pool.lock().await;
|
||||
let mut state = device::Device::new(false);
|
||||
state.clone_state(DEVICE_CLONE.lock().await.deref());
|
||||
let key = if let Some(key) = rewind_frame {
|
||||
key
|
||||
} else if let Some(key) = pool.keys().last() {
|
||||
key + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
pool.insert(
|
||||
key,
|
||||
SavestateData {
|
||||
device: state,
|
||||
saves: SAVES_CLONE.lock().await.clone(),
|
||||
rdp_state,
|
||||
ra_state,
|
||||
},
|
||||
);
|
||||
if pool.len() > 30 {
|
||||
pool.pop_first();
|
||||
}
|
||||
});
|
||||
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,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let compressed_file = if let Ok(device_data) =
|
||||
postcard::to_stdvec(DEVICE_CLONE.lock().await.deref())
|
||||
&& let Ok(saves_data) = postcard::to_stdvec(SAVES_CLONE.lock().await.deref())
|
||||
&& let Ok(compressed_file) = ui::storage::compress_file(&[
|
||||
(&device_data, "device"),
|
||||
(&saves_data, "saves"),
|
||||
(&rdp_state, "rdp_state"),
|
||||
(&ra_state, "ra_state"),
|
||||
]) {
|
||||
Some(compressed_file)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(compressed_file) = compressed_file {
|
||||
if let Err(e) = tokio::fs::write(save_path, compressed_file).await {
|
||||
eprintln!("Error writing savestate: {}", e);
|
||||
error = true;
|
||||
}
|
||||
} else {
|
||||
error = true;
|
||||
eprintln!("Error compressing savestate");
|
||||
}
|
||||
}
|
||||
|
||||
if error {
|
||||
ui::video::onscreen_message(
|
||||
&format!("Failed to create savestate in slot {}", save_state_slot),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
} else if !rewind {
|
||||
ui::video::onscreen_message(
|
||||
&format!("Savestate created in slot {}", save_state_slot),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn load_savestate(device: &mut device::Device) {
|
||||
pub fn load_savestate(device: &mut device::Device, rewind: bool, rewind_frame: Option<i32>) {
|
||||
if retroachievements::get_hardcore() {
|
||||
ui::video::onscreen_message(
|
||||
"Cannot load savestate in RA hardcore mode",
|
||||
@@ -118,120 +208,159 @@ pub fn load_savestate(device: &mut device::Device) {
|
||||
);
|
||||
return;
|
||||
}
|
||||
let savestate = std::fs::read(&device.ui.storage.paths.savestate_file_path);
|
||||
if let Ok(savestate) = &savestate {
|
||||
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
|
||||
|
||||
let state_data = if rewind {
|
||||
if let Some(rewind_frame) = rewind_frame {
|
||||
let timeout = std::time::Duration::from_secs(1);
|
||||
let now = std::time::Instant::now();
|
||||
loop {
|
||||
if let Some(state) = device
|
||||
.savestate
|
||||
.rewind_pool
|
||||
.blocking_lock()
|
||||
.remove(&rewind_frame)
|
||||
{
|
||||
break Some(state);
|
||||
}
|
||||
if now.elapsed() > timeout {
|
||||
break None;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
}
|
||||
} else if let Some((_key, state)) = device.savestate.rewind_pool.blocking_lock().pop_last()
|
||||
{
|
||||
device.ui.storage.saves = saves;
|
||||
Some(state)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if let Ok(savestate) = std::fs::read(&device.ui.storage.paths.savestate_file_path)
|
||||
&& let Ok(device_data) = ui::storage::decompress_file(&savestate, "device")
|
||||
&& let Ok(saves_data) = 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(state) = postcard::from_bytes::<Box<device::Device>>(&device_data)
|
||||
&& let Ok(saves) = postcard::from_bytes(&saves_data)
|
||||
{
|
||||
Some(SavestateData {
|
||||
device: state,
|
||||
saves,
|
||||
rdp_state,
|
||||
ra_state,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
std::mem::swap(&mut device.cpu, &mut state.cpu);
|
||||
std::mem::swap(&mut device.pif, &mut state.pif);
|
||||
if let Some(mut state) = state_data
|
||||
&& device.rdram.size == state.device.rdram.size
|
||||
{
|
||||
if device.netplay.is_none() {
|
||||
ui::video::idle();
|
||||
}
|
||||
|
||||
let rom = device.cart.rom.clone();
|
||||
std::mem::swap(&mut device.cart, &mut state.cart);
|
||||
device.cart.rom = rom;
|
||||
device.savestate.last_rewind_saved = state.device.vi.elapsed_time;
|
||||
|
||||
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);
|
||||
std::mem::swap(&mut device.rng, &mut state.device.rng);
|
||||
|
||||
device.rdram.mem.clone_from(&state.rdram.mem);
|
||||
device.rdram.regs = state.rdram.regs;
|
||||
std::mem::swap(&mut device.ui.storage.saves, &mut state.saves);
|
||||
|
||||
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);
|
||||
std::mem::swap(&mut device.cpu, &mut state.device.cpu);
|
||||
std::mem::swap(&mut device.pif, &mut state.device.pif);
|
||||
|
||||
let mut tpak_rom = [vec![], vec![], vec![], vec![]];
|
||||
for (i, item) in tpak_rom.iter_mut().enumerate() {
|
||||
*item = device.transferpaks[i].cart.rom.clone();
|
||||
}
|
||||
std::mem::swap(&mut device.transferpaks, &mut state.transferpaks);
|
||||
for (i, item) in tpak_rom.iter().enumerate() {
|
||||
device.transferpaks[i].cart.rom = item.clone();
|
||||
std::mem::swap(&mut device.cart, &mut state.device.cart);
|
||||
std::mem::swap(&mut device.cart.rom, &mut state.device.cart.rom); // ROM is not included in the savestate
|
||||
|
||||
std::mem::swap(&mut device.memory, &mut state.device.memory);
|
||||
std::mem::swap(&mut device.rsp, &mut state.device.rsp);
|
||||
std::mem::swap(&mut device.rdp, &mut state.device.rdp);
|
||||
|
||||
device.rdram.mem.copy_from_slice(&state.device.rdram.mem); // RDRAM address should not change
|
||||
std::mem::swap(&mut device.rdram.regs, &mut state.device.rdram.regs);
|
||||
|
||||
std::mem::swap(&mut device.mi, &mut state.device.mi);
|
||||
std::mem::swap(&mut device.pi, &mut state.device.pi);
|
||||
std::mem::swap(&mut device.vi, &mut state.device.vi);
|
||||
std::mem::swap(&mut device.ai, &mut state.device.ai);
|
||||
std::mem::swap(&mut device.si, &mut state.device.si);
|
||||
std::mem::swap(&mut device.ri, &mut state.device.ri);
|
||||
std::mem::swap(&mut device.vru, &mut state.device.vru);
|
||||
std::mem::swap(&mut device.cheats, &mut state.device.cheats);
|
||||
|
||||
std::mem::swap(&mut device.transferpaks, &mut state.device.transferpaks);
|
||||
for (i, item) in device.transferpaks.iter_mut().enumerate() {
|
||||
std::mem::swap(
|
||||
&mut item.cart.rom,
|
||||
&mut state.device.transferpaks[i].cart.rom,
|
||||
); // ROM is not included in the savestate
|
||||
}
|
||||
|
||||
device::memory::init(device);
|
||||
device::vi::set_expected_refresh_rate(device);
|
||||
device::cpu::map_instructions(device);
|
||||
device::cop0::map_instructions(device);
|
||||
device::cop1::map_instructions(device);
|
||||
device::cop2::map_instructions(device);
|
||||
device::rsp_cpu::map_instructions(device);
|
||||
|
||||
let mut mem_addr = 0x1000;
|
||||
while mem_addr < 0x2000 {
|
||||
let data =
|
||||
u32::from_be_bytes(device.rsp.mem[mem_addr..mem_addr + 4].try_into().unwrap());
|
||||
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].func =
|
||||
device::rsp_cpu::decode_opcode(device, data);
|
||||
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].opcode = data;
|
||||
mem_addr += 4;
|
||||
}
|
||||
|
||||
for line_index in 0..512 {
|
||||
for i in 0..8 {
|
||||
device.memory.icache[line_index].instruction[i] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[i]);
|
||||
}
|
||||
}
|
||||
|
||||
device::memory::init(device);
|
||||
device::vi::set_expected_refresh_rate(device);
|
||||
device::cpu::map_instructions(device);
|
||||
device::cop0::map_instructions(device);
|
||||
device::cop1::map_instructions(device);
|
||||
device::cop2::map_instructions(device);
|
||||
device::rsp_cpu::map_instructions(device);
|
||||
|
||||
let mut mem_addr = 0x1000;
|
||||
while mem_addr < 0x2000 {
|
||||
let data =
|
||||
u32::from_be_bytes(device.rsp.mem[mem_addr..mem_addr + 4].try_into().unwrap());
|
||||
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].func =
|
||||
device::rsp_cpu::decode_opcode(device, data);
|
||||
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].opcode = data;
|
||||
mem_addr += 4;
|
||||
}
|
||||
|
||||
for line_index in 0..512 {
|
||||
for i in 0..8 {
|
||||
device.memory.icache[line_index].instruction[i] = device::cpu::decode_opcode(
|
||||
device,
|
||||
device.memory.icache[line_index].words[i],
|
||||
);
|
||||
device::pif::connect_pif_channels(device);
|
||||
for i in 0..4 {
|
||||
if let Some(handler) = device.pif.channels[i].pak_handler {
|
||||
if handler.pak_type == device::controller::PakType::RumblePak {
|
||||
let rumblepak_handler = device::controller::PakHandler {
|
||||
read: device::controller::rumble::read,
|
||||
write: device::controller::rumble::write,
|
||||
pak_type: device::controller::PakType::RumblePak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(rumblepak_handler);
|
||||
} else if handler.pak_type == device::controller::PakType::MemPak {
|
||||
let mempak_handler = device::controller::PakHandler {
|
||||
read: device::controller::mempak::read,
|
||||
write: device::controller::mempak::write,
|
||||
pak_type: device::controller::PakType::MemPak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(mempak_handler);
|
||||
} else if handler.pak_type == device::controller::PakType::TransferPak {
|
||||
let tpak_handler = device::controller::PakHandler {
|
||||
read: device::controller::transferpak::read,
|
||||
write: device::controller::transferpak::write,
|
||||
pak_type: device::controller::PakType::TransferPak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(tpak_handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
device::pif::connect_pif_channels(device);
|
||||
for i in 0..4 {
|
||||
if let Some(handler) = device.pif.channels[i].pak_handler {
|
||||
if handler.pak_type == device::controller::PakType::RumblePak {
|
||||
let rumblepak_handler = device::controller::PakHandler {
|
||||
read: device::controller::rumble::read,
|
||||
write: device::controller::rumble::write,
|
||||
pak_type: device::controller::PakType::RumblePak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(rumblepak_handler);
|
||||
} else if handler.pak_type == device::controller::PakType::MemPak {
|
||||
let mempak_handler = device::controller::PakHandler {
|
||||
read: device::controller::mempak::read,
|
||||
write: device::controller::mempak::write,
|
||||
pak_type: device::controller::PakType::MemPak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(mempak_handler);
|
||||
} else if handler.pak_type == device::controller::PakType::TransferPak {
|
||||
let tpak_handler = device::controller::PakHandler {
|
||||
read: device::controller::transferpak::read,
|
||||
write: device::controller::transferpak::write,
|
||||
pak_type: device::controller::PakType::TransferPak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(tpak_handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
ui::audio::update_freq(device);
|
||||
ui::video::load_state(device, state.rdp_state.as_ptr());
|
||||
|
||||
ui::audio::close_game_audio(&mut device.ui);
|
||||
ui::audio::init_game_audio(device);
|
||||
ui::video::load_state(device, rdp_state.as_ptr());
|
||||
if !state.ra_state.is_empty() {
|
||||
retroachievements::load_state(state.ra_state.as_ptr(), state.ra_state.len());
|
||||
} else {
|
||||
retroachievements::load_state(std::ptr::null(), 0);
|
||||
}
|
||||
|
||||
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 {
|
||||
retroachievements::load_state(std::ptr::null(), 0);
|
||||
}
|
||||
|
||||
if device.cheats.enabled {
|
||||
cheats::execute_cheats(device, device.cheats.cheats.clone());
|
||||
}
|
||||
if device.cheats.enabled {
|
||||
cheats::execute_cheats(device, device.cheats.cheats.clone());
|
||||
}
|
||||
|
||||
if !rewind {
|
||||
ui::video::onscreen_message(
|
||||
&format!(
|
||||
"Savestate loaded from slot {}",
|
||||
@@ -239,23 +368,28 @@ pub fn load_savestate(device: &mut device::Device) {
|
||||
),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
|
||||
);
|
||||
} else {
|
||||
ui::video::onscreen_message(
|
||||
&format!(
|
||||
}
|
||||
} else {
|
||||
let (message, length) = if !rewind {
|
||||
(
|
||||
format!(
|
||||
"Failed to load savestate from slot {}",
|
||||
device.ui.storage.save_state_slot
|
||||
),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
|
||||
)
|
||||
} else if device.netplay.is_none() {
|
||||
(
|
||||
"Failed to rewind".to_string(),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"Failed to rollback".to_string(),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ui::video::onscreen_message(
|
||||
&format!(
|
||||
"Could not find savestate in slot {}",
|
||||
device.ui.storage.save_state_slot
|
||||
),
|
||||
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
|
||||
);
|
||||
)
|
||||
};
|
||||
ui::video::onscreen_message(&message, length);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod config;
|
||||
#[cfg(feature = "gui")]
|
||||
pub mod gui;
|
||||
pub mod input;
|
||||
pub mod input_profile;
|
||||
#[cfg(feature = "gui")]
|
||||
pub mod netplay;
|
||||
#[cfg(feature = "gui")]
|
||||
@@ -17,6 +18,8 @@ pub mod video;
|
||||
#[cfg(all(feature = "gui", not(target_os = "android")))]
|
||||
pub mod vru;
|
||||
|
||||
pub const APP_ID: &str = "io.github.gopher64.gopher64";
|
||||
|
||||
pub static WEB_CLIENT: std::sync::LazyLock<reqwest::Client> = std::sync::LazyLock::new(|| {
|
||||
reqwest::Client::builder()
|
||||
.user_agent(format!(
|
||||
@@ -28,23 +31,32 @@ pub static WEB_CLIENT: std::sync::LazyLock<reqwest::Client> = std::sync::LazyLoc
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Default, Clone)]
|
||||
pub struct Dirs {
|
||||
pub config_dir: std::path::PathBuf,
|
||||
pub data_dir: std::path::PathBuf,
|
||||
pub cache_dir: std::path::PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Audio {
|
||||
pub audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
|
||||
pub gain: f32,
|
||||
}
|
||||
|
||||
unsafe impl Send for Audio {}
|
||||
unsafe impl Sync for Audio {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Input {
|
||||
pub keyboard_state: *const bool,
|
||||
pub controllers: [input::Controllers; 4],
|
||||
}
|
||||
|
||||
unsafe impl Send for Input {}
|
||||
unsafe impl Sync for Input {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Storage {
|
||||
pub save_type: Vec<storage::SaveTypes>,
|
||||
pub paths: storage::Paths,
|
||||
@@ -52,28 +64,35 @@ pub struct Storage {
|
||||
pub save_state_slot: u32,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Video {
|
||||
pub window: *mut sdl3_sys::video::SDL_Window,
|
||||
pub fullscreen: bool,
|
||||
pub fps_tx: tokio::sync::mpsc::Sender<bool>,
|
||||
pub fps_rx: Option<tokio::sync::mpsc::Receiver<bool>>,
|
||||
pub vis_tx: tokio::sync::mpsc::Sender<bool>,
|
||||
pub vis_rx: Option<tokio::sync::mpsc::Receiver<bool>>,
|
||||
pub widescreen: bool,
|
||||
pub fps_tx: Option<tokio::sync::mpsc::UnboundedSender<bool>>,
|
||||
pub fps_rx: Option<tokio::sync::mpsc::UnboundedReceiver<bool>>,
|
||||
pub vis_tx: Option<tokio::sync::mpsc::UnboundedSender<bool>>,
|
||||
pub vis_rx: Option<tokio::sync::mpsc::UnboundedReceiver<bool>>,
|
||||
}
|
||||
|
||||
unsafe impl Send for Video {}
|
||||
unsafe impl Sync for Video {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Usb {
|
||||
pub usb_tx: Option<tokio::sync::broadcast::Sender<usb::UsbData>>,
|
||||
pub cart_rx: Option<tokio::sync::broadcast::Receiver<usb::UsbData>>,
|
||||
pub usb_tx: Option<tokio::sync::mpsc::UnboundedSender<usb::UsbData>>,
|
||||
pub cart_rx: Option<tokio::sync::mpsc::UnboundedReceiver<usb::UsbData>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GameSettings {
|
||||
pub overclock: bool,
|
||||
pub disable_expansion_pak: bool,
|
||||
pub cheats: std::collections::HashMap<String, Option<String>>,
|
||||
pub cheats: rustc_hash::FxHashMap<String, Option<String>>,
|
||||
pub load_savestate_slot: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Ui {
|
||||
pub dirs: Dirs,
|
||||
pub config: config::Config,
|
||||
@@ -97,6 +116,8 @@ pub fn sdl_hints() {
|
||||
sdl3_sys::everything::SDL_HINT_ANDROID_ALLOW_RECREATE_ACTIVITY,
|
||||
hint.as_ptr(),
|
||||
);
|
||||
let app_id = std::ffi::CString::new(APP_ID).unwrap();
|
||||
sdl3_sys::everything::SDL_SetHint(sdl3_sys::everything::SDL_HINT_APP_ID, app_id.as_ptr());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,8 +182,8 @@ impl Ui {
|
||||
pub fn new() -> Ui {
|
||||
let dirs = get_dirs();
|
||||
|
||||
let (fps_tx, fps_rx) = tokio::sync::mpsc::channel(1000);
|
||||
let (vis_tx, vis_rx) = tokio::sync::mpsc::channel(1000);
|
||||
let (fps_tx, fps_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let (vis_tx, vis_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
Ui {
|
||||
input: Input {
|
||||
controllers: [
|
||||
@@ -232,7 +253,7 @@ impl Ui {
|
||||
write_pending: false,
|
||||
},
|
||||
romsave: storage::RomSave {
|
||||
data: std::collections::HashMap::new(),
|
||||
data: rustc_hash::FxHashMap::default(),
|
||||
write_pending: false,
|
||||
},
|
||||
},
|
||||
@@ -247,9 +268,10 @@ impl Ui {
|
||||
video: Video {
|
||||
window: std::ptr::null_mut(),
|
||||
fullscreen: false,
|
||||
fps_tx,
|
||||
widescreen: false,
|
||||
fps_tx: Some(fps_tx),
|
||||
fps_rx: Some(fps_rx),
|
||||
vis_tx,
|
||||
vis_tx: Some(vis_tx),
|
||||
vis_rx: Some(vis_rx),
|
||||
},
|
||||
usb: Usb {
|
||||
|
||||
+193
-190
@@ -1,4 +1,5 @@
|
||||
use crate::Args;
|
||||
use crate::create_runtime;
|
||||
use crate::run;
|
||||
use crate::ui;
|
||||
use clap::Parser;
|
||||
@@ -15,9 +16,9 @@ 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<
|
||||
pub static SELECT_ROM_TX: tokio::sync::Mutex<
|
||||
Option<tokio::sync::oneshot::Sender<Option<std::path::PathBuf>>>,
|
||||
> = std::sync::Mutex::new(None);
|
||||
> = tokio::sync::Mutex::const_new(None);
|
||||
|
||||
pub static WEAK_SLINT_WINDOW: std::sync::Mutex<Option<slint::Weak<ui::gui::AppWindow>>> =
|
||||
std::sync::Mutex::new(None);
|
||||
@@ -158,17 +159,21 @@ fn argv_to_strings(argc: std::ffi::c_int, argv: *mut *mut std::ffi::c_char) -> V
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
#[tokio::main(worker_threads = 4)]
|
||||
pub async extern "C" fn gopher64_sdl_main(
|
||||
pub extern "C" fn gopher64_sdl_main(
|
||||
argc: std::ffi::c_int,
|
||||
argv: *mut *mut std::ffi::c_char,
|
||||
) -> std::ffi::c_int {
|
||||
let (close_tx, handle) = create_runtime();
|
||||
let _guard = handle.enter();
|
||||
|
||||
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 {
|
||||
if let Err(err) = run(args, argc as usize) {
|
||||
close_tx.send(()).unwrap();
|
||||
eprintln!("Error running game: {err:?}");
|
||||
return 1;
|
||||
}
|
||||
close_tx.send(()).unwrap();
|
||||
0
|
||||
}
|
||||
|
||||
@@ -177,9 +182,17 @@ pub fn spawn_configure_input_profile(
|
||||
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)
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
if let Err(err) = get_vm(app).attach_current_thread(|env| {
|
||||
start_configure_input_profile_on_jvm(
|
||||
env,
|
||||
app,
|
||||
profile_name.to_string(),
|
||||
dinput,
|
||||
deadzone,
|
||||
)
|
||||
}) {
|
||||
eprintln!("JNI error while starting N64Activity: {err:?}");
|
||||
}
|
||||
@@ -188,45 +201,40 @@ pub fn spawn_configure_input_profile(
|
||||
|
||||
fn start_configure_input_profile_on_jvm(
|
||||
env: &mut Env<'_>,
|
||||
app: &slint::android::AndroidApp,
|
||||
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 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 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)
|
||||
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(())
|
||||
}
|
||||
|
||||
pub fn run_rom(
|
||||
@@ -235,9 +243,11 @@ pub fn run_rom(
|
||||
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)
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
if let Err(err) = get_vm(app).attach_current_thread(|env| {
|
||||
start_run_rom_on_jvm(env, app, file_path, game_settings, netplay, weak)
|
||||
}) {
|
||||
eprintln!("JNI error while starting N64Activity: {err:?}");
|
||||
}
|
||||
@@ -246,90 +256,88 @@ pub fn run_rom(
|
||||
|
||||
fn start_run_rom_on_jvm(
|
||||
env: &mut Env<'_>,
|
||||
app: &slint::android::AndroidApp,
|
||||
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 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 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 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())?,
|
||||
];
|
||||
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())?);
|
||||
if let Some(netplay) = netplay {
|
||||
args.push(JString::from_str(env, "--netplay-server-addr")?);
|
||||
args.push(JString::from_str(env, &netplay.server_addr)?);
|
||||
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, "--netplay-number-of-players")?);
|
||||
args.push(JString::from_str(
|
||||
env,
|
||||
&netplay.number_of_players.to_string(),
|
||||
)?);
|
||||
args.push(JString::from_str(env, "--netplay-input-delay")?);
|
||||
args.push(JString::from_str(env, &netplay.input_delay.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)
|
||||
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(())
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
fn get_vm(app: &slint::android::AndroidApp) -> JavaVM {
|
||||
unsafe { JavaVM::from_raw(app.vm_as_ptr().cast()) }
|
||||
}
|
||||
|
||||
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)) {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
match get_vm(app).attach_current_thread(|env| decode_path_on_jvm(env, path)) {
|
||||
Ok(decoded_path) => decoded_path,
|
||||
Err(err) => {
|
||||
eprintln!("JNI error while decoding path: {err:?}");
|
||||
@@ -350,8 +358,10 @@ fn decode_path_on_jvm(env: &mut Env<'_>, path: &str) -> jni::errors::Result<Stri
|
||||
|
||||
/// 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) {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
match get_vm(app).attach_current_thread(list_controllers_on_jvm) {
|
||||
Ok(controllers) => controllers,
|
||||
Err(err) => {
|
||||
eprintln!("JNI error while listing controllers: {err:?}");
|
||||
@@ -410,54 +420,54 @@ fn list_controllers_on_jvm(env: &mut Env<'_>) -> jni::errors::Result<Vec<Control
|
||||
|
||||
/// 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() {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
let path = path.to_string();
|
||||
if let Err(err) = vm.attach_current_thread(|env| open_uri_on_jvm(env, &path)) {
|
||||
if let Err(err) = get_vm(app).attach_current_thread(|env| open_uri_on_jvm(env, app, &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)? };
|
||||
fn open_uri_on_jvm(
|
||||
env: &mut Env<'_>,
|
||||
app: &slint::android::AndroidApp,
|
||||
path: &str,
|
||||
) -> jni::errors::Result<()> {
|
||||
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 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)?;
|
||||
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)
|
||||
}
|
||||
activity.as_ref().start_activity(env, &intent)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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()))
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<Option<std::path::PathBuf>>();
|
||||
SELECT_ROM_TX.lock().await.replace(tx);
|
||||
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
if let Err(err) = get_vm(app)
|
||||
.attach_current_thread(|env| select_rom_on_jvm(env, app, 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
|
||||
return None;
|
||||
}
|
||||
|
||||
rx.await.unwrap_or(None)
|
||||
}
|
||||
|
||||
pub async fn select_gb_rom(_player: i32) -> Option<std::path::PathBuf> {
|
||||
@@ -468,30 +478,28 @@ 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)? };
|
||||
fn select_rom_on_jvm(
|
||||
env: &mut Env<'_>,
|
||||
app: &slint::android::AndroidApp,
|
||||
rom_dir: String,
|
||||
) -> jni::errors::Result<()> {
|
||||
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)
|
||||
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(())
|
||||
}
|
||||
|
||||
pub fn get_dirs() -> ui::Dirs {
|
||||
@@ -528,9 +536,11 @@ pub fn rom_exists(path: &str) -> bool {
|
||||
}
|
||||
|
||||
pub fn get_file_from_uri(path: &std::path::PathBuf) -> Option<std::fs::File> {
|
||||
if let Some(vm) = get_vm() {
|
||||
if let Ok(app) = ANDROID_APP.lock()
|
||||
&& let Some(app) = app.as_ref()
|
||||
{
|
||||
let path = path.to_str().unwrap().into();
|
||||
match vm.attach_current_thread(|env| get_file_from_uri_on_jvm(env, path)) {
|
||||
match get_vm(app).attach_current_thread(|env| get_file_from_uri_on_jvm(env, app, path)) {
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
eprintln!("JNI error while opening URI: {err:?}");
|
||||
@@ -545,32 +555,26 @@ pub fn get_file_from_uri(path: &std::path::PathBuf) -> Option<std::fs::File> {
|
||||
|
||||
fn get_file_from_uri_on_jvm(
|
||||
env: &mut Env<'_>,
|
||||
app: &slint::android::AndroidApp,
|
||||
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 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);
|
||||
}
|
||||
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 {
|
||||
eprintln!("Android app not initialized");
|
||||
return Err(jni::errors::Error::UninitializedJavaVM);
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,8 +592,7 @@ pub extern "system" fn Java_io_github_gopher64_gopher64_SlintActivity_nativeOnAc
|
||||
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()
|
||||
if let Some(tx) = SELECT_ROM_TX.blocking_lock().take()
|
||||
&& !intent_data.is_null()
|
||||
{
|
||||
let result_intent =
|
||||
|
||||
+27
-1
@@ -1,4 +1,5 @@
|
||||
use crate::device;
|
||||
use crate::netplay;
|
||||
use crate::ui;
|
||||
|
||||
static ADJUST_LOCKED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
@@ -27,6 +28,27 @@ unsafe extern "C" fn audio_callback(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_freq(device: &mut device::Device) {
|
||||
let game_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
|
||||
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
|
||||
freq: device.ai.freq as i32,
|
||||
channels: 2,
|
||||
};
|
||||
unsafe {
|
||||
sdl3_sys::audio::SDL_SetAudioStreamFormat(
|
||||
device.ui.audio.audio_stream,
|
||||
&game_audio_spec,
|
||||
std::ptr::null(),
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -153,6 +175,10 @@ pub fn play_audio(device: &device::Device, dram_addr: usize, length: u64) {
|
||||
return;
|
||||
}
|
||||
|
||||
if netplay::in_rollback(device.netplay.as_ref()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut primary_buffer: Vec<i16> = vec![0; length as usize / 2];
|
||||
let mut i = 0;
|
||||
while i < length as usize / 2 {
|
||||
@@ -178,7 +204,7 @@ pub fn play_audio(device: &device::Device, dram_addr: usize, length: u64) {
|
||||
primary_buffer.len() as i32 * 2,
|
||||
)
|
||||
};
|
||||
} else if device.vi.enable_speed_limiter {
|
||||
} else if device.speed_limiter.enabled {
|
||||
adjust_audio_frequency(device.ui.audio.audio_stream, 0.0005);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -3,17 +3,21 @@ use crate::ui::gui::AppWindow;
|
||||
use crate::{cheats, device};
|
||||
use slint::ComponentHandle;
|
||||
|
||||
pub fn cheats_window(app: &AppWindow) {
|
||||
pub fn cheats_window(
|
||||
app: &AppWindow,
|
||||
no_intro_map: std::sync::Arc<tokio::sync::Mutex<rustc_hash::FxHashMap<String, String>>>,
|
||||
) {
|
||||
let weak = app.as_weak();
|
||||
app.on_cheats_select_rom_clicked(move |rom_dir| {
|
||||
let select_rom = ui::gui::select_rom(rom_dir);
|
||||
let weak = weak.clone();
|
||||
let no_intro_map = no_intro_map.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Some(file) = select_rom.await
|
||||
&& 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);
|
||||
let game_name = ui::gui::get_nointro_name(&rom_contents, no_intro_map).await;
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_cheat_game_name(game_name.into());
|
||||
})
|
||||
|
||||
+61
-30
@@ -1,39 +1,41 @@
|
||||
use crate::ui;
|
||||
|
||||
#[derive(Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct InputKeyButton {
|
||||
pub enabled: bool,
|
||||
pub id: i32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct InputControllerAxis {
|
||||
pub enabled: bool,
|
||||
pub id: i32,
|
||||
pub axis: i16,
|
||||
pub initial_state: i16,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct InputJoystickHat {
|
||||
pub enabled: bool,
|
||||
pub id: i32,
|
||||
pub direction: u8,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum InputItem {
|
||||
Key(InputKeyButton),
|
||||
ControllerButton(InputKeyButton),
|
||||
ControllerAxis(InputControllerAxis),
|
||||
JoystickButton(InputKeyButton),
|
||||
JoystickHat(InputJoystickHat),
|
||||
JoystickAxis(InputControllerAxis),
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct InputProfile {
|
||||
pub keys: [InputKeyButton; ui::input::PROFILE_SIZE],
|
||||
pub controller_buttons: [InputKeyButton; ui::input::PROFILE_SIZE],
|
||||
pub controller_axis: [InputControllerAxis; ui::input::PROFILE_SIZE],
|
||||
pub joystick_buttons: [InputKeyButton; ui::input::PROFILE_SIZE],
|
||||
pub joystick_hat: [InputJoystickHat; ui::input::PROFILE_SIZE],
|
||||
pub joystick_axis: [InputControllerAxis; ui::input::PROFILE_SIZE],
|
||||
pub inputs: [[Option<InputItem>; 2]; ui::input_profile::PROFILE_SIZE],
|
||||
pub dinput: bool,
|
||||
pub deadzone: i32,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Input {
|
||||
pub input_profiles: std::collections::BTreeMap<String, InputProfile>,
|
||||
pub input_profile_binding: [String; 4],
|
||||
@@ -45,7 +47,7 @@ pub struct Input {
|
||||
pub emulate_vru: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Video {
|
||||
pub upscale: u32,
|
||||
pub ssaa: bool,
|
||||
@@ -56,26 +58,34 @@ pub struct Video {
|
||||
pub crt: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Emulation {
|
||||
pub overclock: bool,
|
||||
pub disable_expansion_pak: bool,
|
||||
pub usb: bool,
|
||||
pub rewind: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Cheats {
|
||||
pub cheats:
|
||||
std::collections::HashMap<String, std::collections::HashMap<String, Option<String>>>,
|
||||
pub cheats: rustc_hash::FxHashMap<String, rustc_hash::FxHashMap<String, Option<String>>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Ui {
|
||||
pub theme: i32,
|
||||
pub rom_dir: std::path::PathBuf,
|
||||
pub recent_roms: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Config {
|
||||
pub input: Input,
|
||||
pub video: Video,
|
||||
pub emulation: Emulation,
|
||||
pub rom_dir: std::path::PathBuf,
|
||||
pub recent_roms: Vec<String>,
|
||||
pub ui: Ui,
|
||||
#[serde(skip)]
|
||||
write_to_disk: bool,
|
||||
}
|
||||
|
||||
impl Drop for Cheats {
|
||||
@@ -106,14 +116,16 @@ impl Cheats {
|
||||
}
|
||||
}
|
||||
Cheats {
|
||||
cheats: std::collections::HashMap::new(),
|
||||
cheats: rustc_hash::FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Config {
|
||||
fn drop(&mut self) {
|
||||
write_config(self);
|
||||
if self.write_to_disk {
|
||||
write_config(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,16 +144,30 @@ impl Config {
|
||||
let dirs = ui::get_dirs();
|
||||
let file_path = dirs.config_dir.join("config.json");
|
||||
let config_file = std::fs::read(file_path);
|
||||
let mut input_data: Option<Input> = None;
|
||||
if let Ok(config_file) = config_file {
|
||||
let result = serde_json::from_slice(config_file.as_ref());
|
||||
if let Ok(result) = result {
|
||||
let result = serde_json::from_slice::<Config>(config_file.as_ref());
|
||||
if let Ok(mut result) = result {
|
||||
result.write_to_disk = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
// try to restore input data from old config file
|
||||
if let Ok(value) = serde_json::from_slice::<serde_json::Value>(&config_file)
|
||||
&& let Some(data) = value.get("input")
|
||||
&& let Ok(data) = serde_json::from_value::<Input>(data.clone())
|
||||
{
|
||||
input_data = Some(data);
|
||||
}
|
||||
}
|
||||
|
||||
let mut input_profiles = std::collections::BTreeMap::new();
|
||||
input_profiles.insert("default".to_string(), ui::input::get_default_profile());
|
||||
input_profiles.insert(
|
||||
"default".to_string(),
|
||||
ui::input_profile::get_default_profile(),
|
||||
);
|
||||
Config {
|
||||
input: Input {
|
||||
input: input_data.unwrap_or(Input {
|
||||
input_profile_binding: [
|
||||
"default".to_string(),
|
||||
"default".to_string(),
|
||||
@@ -155,7 +181,7 @@ impl Config {
|
||||
gb_rom_path: [String::new(), String::new(), String::new(), String::new()],
|
||||
gb_ram_path: [String::new(), String::new(), String::new(), String::new()],
|
||||
emulate_vru: false,
|
||||
},
|
||||
}),
|
||||
video: Video {
|
||||
upscale: 1,
|
||||
ssaa: false,
|
||||
@@ -169,9 +195,14 @@ impl Config {
|
||||
overclock: false,
|
||||
disable_expansion_pak: false,
|
||||
usb: false,
|
||||
rewind: false,
|
||||
},
|
||||
rom_dir: std::path::PathBuf::new(),
|
||||
recent_roms: Vec::new(),
|
||||
ui: Ui {
|
||||
theme: 0,
|
||||
rom_dir: std::path::PathBuf::new(),
|
||||
recent_roms: Vec::new(),
|
||||
},
|
||||
write_to_disk: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+115
-22
@@ -1,3 +1,4 @@
|
||||
use crate::device;
|
||||
use crate::retroachievements;
|
||||
use crate::ui;
|
||||
#[cfg(target_os = "android")]
|
||||
@@ -19,8 +20,10 @@ struct GithubData {
|
||||
}
|
||||
|
||||
pub struct NetplayDevice {
|
||||
pub peer_addr: std::net::SocketAddr,
|
||||
pub player_number: u8,
|
||||
pub server_addr: String,
|
||||
pub player_number: usize,
|
||||
pub number_of_players: usize,
|
||||
pub input_delay: usize,
|
||||
}
|
||||
|
||||
fn check_latest_version(weak: slint::Weak<AppWindow>) {
|
||||
@@ -32,13 +35,18 @@ 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
|
||||
&& 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 latest_version = match data {
|
||||
Ok(data) => match semver::Version::parse(&data.tag_name[1..]) {
|
||||
Ok(github_version) => github_version,
|
||||
Err(e) => {
|
||||
eprintln!("Error parsing latest version from GitHub: {}", e);
|
||||
semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap()
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Error getting latest version from GitHub: {}", e);
|
||||
semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap()
|
||||
}
|
||||
};
|
||||
let current_version = semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
|
||||
if current_version < latest_version {
|
||||
@@ -72,7 +80,7 @@ fn run_with_path(weak: slint::Weak<AppWindow>, path: std::path::PathBuf) {
|
||||
ui::GameSettings {
|
||||
overclock: handle.get_overclock_n64_cpu(),
|
||||
disable_expansion_pak: handle.get_disable_expansion_pak(),
|
||||
cheats: std::collections::HashMap::new(), // will be filled in later
|
||||
cheats: rustc_hash::FxHashMap::default(), // will be filled in later
|
||||
load_savestate_slot: None,
|
||||
},
|
||||
None,
|
||||
@@ -105,6 +113,7 @@ 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
|
||||
.ui
|
||||
.recent_roms
|
||||
.iter()
|
||||
.filter(|x| rom_exists(x))
|
||||
@@ -172,9 +181,11 @@ fn settings_window(app: &AppWindow, config: &ui::config::Config) {
|
||||
app.set_widescreen(config.video.widescreen);
|
||||
app.set_vsync(config.video.vsync);
|
||||
app.set_apply_crt_shader(config.video.crt);
|
||||
app.set_theme(config.ui.theme);
|
||||
app.set_overclock_n64_cpu(config.emulation.overclock);
|
||||
app.set_disable_expansion_pak(config.emulation.disable_expansion_pak);
|
||||
app.set_emulate_usb(config.emulation.usb);
|
||||
app.set_rewind(config.emulation.rewind);
|
||||
let combobox_value = match config.video.upscale {
|
||||
1 => 0,
|
||||
2 => 1,
|
||||
@@ -184,7 +195,7 @@ fn settings_window(app: &AppWindow, config: &ui::config::Config) {
|
||||
};
|
||||
app.set_resolution(combobox_value);
|
||||
|
||||
if let Some(rom_dir_str) = config.rom_dir.to_str() {
|
||||
if let Some(rom_dir_str) = config.ui.rom_dir.to_str() {
|
||||
app.set_rom_dir(rom_dir_str.into());
|
||||
}
|
||||
}
|
||||
@@ -336,7 +347,7 @@ fn controller_window(app: &AppWindow, config: &ui::config::Config) {
|
||||
app.on_input_profile_button_clicked(move || {
|
||||
weak_app
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_input_deadzone(ui::input::DEADZONE_DEFAULT);
|
||||
handle.set_input_deadzone(ui::input_profile::DEADZONE_DEFAULT);
|
||||
handle.set_input_profile_name(String::new().into());
|
||||
handle.set_input_dinput(false);
|
||||
handle.set_show_input_profile(true);
|
||||
@@ -436,16 +447,18 @@ fn controller_window(app: &AppWindow, config: &ui::config::Config) {
|
||||
|
||||
pub fn save_settings(app: &AppWindow) {
|
||||
let mut config = ui::config::Config::new();
|
||||
config.rom_dir = app.get_rom_dir().to_string().into();
|
||||
config.ui.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();
|
||||
config.video.crt = app.get_apply_crt_shader();
|
||||
config.ui.theme = app.get_theme();
|
||||
config.emulation.overclock = app.get_overclock_n64_cpu();
|
||||
config.emulation.disable_expansion_pak = app.get_disable_expansion_pak();
|
||||
config.emulation.usb = app.get_emulate_usb();
|
||||
config.emulation.rewind = app.get_rewind();
|
||||
let upscale_values = [1, 2, 4, 8];
|
||||
config.video.upscale = upscale_values[app.get_resolution() as usize];
|
||||
|
||||
@@ -503,13 +516,25 @@ fn about_window(app: &AppWindow) {
|
||||
});
|
||||
app.set_version(format!("Version: {}", env!("GIT_DESCRIBE")).into());
|
||||
|
||||
//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() {
|
||||
//flatpak, itch.io, and android have their own update checking mechanism
|
||||
if std::env::var("FLATPAK_ID").is_err()
|
||||
&& std::env::var("ITCHIO_APP").is_err()
|
||||
&& cfg!(not(target_os = "android"))
|
||||
{
|
||||
check_latest_version(app.as_weak());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn app_window(app: &AppWindow, is_android: bool) {
|
||||
pub fn app_window(
|
||||
app: &AppWindow,
|
||||
is_android: bool,
|
||||
no_intro_map: std::sync::Arc<tokio::sync::Mutex<rustc_hash::FxHashMap<String, String>>>,
|
||||
) {
|
||||
let no_intro_map_clone = no_intro_map.clone();
|
||||
tokio::spawn(async move {
|
||||
load_no_intro(no_intro_map_clone).await;
|
||||
});
|
||||
|
||||
retroachievements::init_client(false, false, false);
|
||||
app.set_is_android(is_android);
|
||||
about_window(app);
|
||||
@@ -520,19 +545,23 @@ pub fn app_window(app: &AppWindow, is_android: bool) {
|
||||
controller_window(app, &config);
|
||||
local_game_window(app, &config);
|
||||
}
|
||||
ui::netplay::netplay_window(app);
|
||||
ui::cheats::cheats_window(app);
|
||||
ui::netplay::netplay_window(app, no_intro_map.clone());
|
||||
ui::cheats::cheats_window(app, no_intro_map);
|
||||
|
||||
#[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))
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
save_settings(&handle);
|
||||
handle.invoke_netplay_close();
|
||||
})
|
||||
.unwrap();
|
||||
slint::CloseRequestResponse::HideWindow
|
||||
});
|
||||
}
|
||||
|
||||
app.run().unwrap();
|
||||
retroachievements::shutdown_client();
|
||||
}
|
||||
@@ -574,10 +603,14 @@ pub fn run_rom(
|
||||
serde_json::to_writer_pretty(f, &game_settings.cheats).unwrap();
|
||||
|
||||
command.args([
|
||||
"--netplay-peer-addr",
|
||||
&netplay_device.peer_addr.to_string(),
|
||||
"--netplay-server-addr",
|
||||
&netplay_device.server_addr,
|
||||
"--netplay-player-number",
|
||||
&netplay_device.player_number.to_string(),
|
||||
"--netplay-number-of-players",
|
||||
&netplay_device.number_of_players.to_string(),
|
||||
"--netplay-input-delay",
|
||||
&netplay_device.input_delay.to_string(),
|
||||
"--cheats",
|
||||
cheats_path.to_str().unwrap(),
|
||||
]);
|
||||
@@ -637,6 +670,66 @@ pub fn update_recent_roms(app: &AppWindow, file_path: std::path::PathBuf) {
|
||||
app.set_recent_roms(slint::ModelRc::from(std::rc::Rc::new(recent_roms)));
|
||||
}
|
||||
|
||||
pub async fn get_nointro_name(
|
||||
rom: &[u8],
|
||||
no_intro_map: std::sync::Arc<tokio::sync::Mutex<rustc_hash::FxHashMap<String, String>>>,
|
||||
) -> String {
|
||||
let hash = device::cart::rom::calculate_hash(rom).to_lowercase();
|
||||
if let Some(name) = no_intro_map.lock().await.get(&hash) {
|
||||
name.clone()
|
||||
} else {
|
||||
ui::storage::get_game_name(rom)
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_no_intro(
|
||||
no_intro_map: std::sync::Arc<tokio::sync::Mutex<rustc_hash::FxHashMap<String, String>>>,
|
||||
) {
|
||||
let mut reader = quick_xml::Reader::from_str(include_str!(
|
||||
"../../data/ui/Nintendo - Nintendo 64 (DB Export) (20260609-194259).xml"
|
||||
));
|
||||
let mut current_game = String::new();
|
||||
let mut map = no_intro_map.lock().await;
|
||||
let mut xml_version = quick_xml::XmlVersion::Implicit1_0;
|
||||
loop {
|
||||
match reader.read_event() {
|
||||
Ok(quick_xml::events::Event::Decl(e)) => {
|
||||
if let Ok(version) = e.xml_version() {
|
||||
xml_version = version;
|
||||
}
|
||||
}
|
||||
Ok(quick_xml::events::Event::Start(e)) => {
|
||||
if e.name().as_ref() == b"game"
|
||||
&& let Ok(Some(name_attribute)) = e.try_get_attribute("name")
|
||||
&& let Ok(normalized_value) = name_attribute.normalized_value(xml_version)
|
||||
{
|
||||
current_game = normalized_value.into_owned();
|
||||
}
|
||||
}
|
||||
Ok(quick_xml::events::Event::End(e)) => {
|
||||
if e.name().as_ref() == b"game" {
|
||||
current_game.clear();
|
||||
}
|
||||
}
|
||||
Ok(quick_xml::events::Event::Empty(e)) => {
|
||||
if e.name().as_ref() == b"file"
|
||||
&& let Ok(Some(format_attribute)) = e.try_get_attribute("format")
|
||||
&& let Ok(normalized_value) = format_attribute.normalized_value(xml_version)
|
||||
&& normalized_value.as_ref() == "BigEndian"
|
||||
&& let Ok(Some(sha256_attribute)) = e.try_get_attribute("sha256")
|
||||
&& let Ok(normalized_sha256) = sha256_attribute.normalized_value(xml_version)
|
||||
&& !current_game.is_empty()
|
||||
{
|
||||
map.insert(normalized_sha256.to_lowercase(), current_game.clone());
|
||||
}
|
||||
}
|
||||
Err(e) => panic!("Error at position {}: {:?}", reader.error_position(), e),
|
||||
Ok(quick_xml::events::Event::Eof) => break,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -700,7 +793,7 @@ fn open_rom(app: &AppWindow) {
|
||||
ui::GameSettings {
|
||||
overclock,
|
||||
disable_expansion_pak,
|
||||
cheats: std::collections::HashMap::new(), // will be filled in later
|
||||
cheats: rustc_hash::FxHashMap::default(), // will be filled in later
|
||||
load_savestate_slot: None,
|
||||
},
|
||||
None,
|
||||
|
||||
+47
-41
@@ -13,7 +13,7 @@ import { CheatsData } from "cheat_page.slint";
|
||||
import { InputProfileDialog, InputProfileData } from "input_profile.slint";
|
||||
import { NetplayData } from "netplay_page.slint";
|
||||
export { VruDialog } from "vru_dialog.slint";
|
||||
import { NetplayCreate, CustomServerDialog } from "netplay_create.slint";
|
||||
import { NetplayCreate } from "netplay_create.slint";
|
||||
import { NetplayJoin } from "netplay_join.slint";
|
||||
import { NetplayWait, NetplayWaitData } from "netplay_wait.slint";
|
||||
|
||||
@@ -34,13 +34,12 @@ export component AppWindow inherits Window {
|
||||
callback join_session_button_clicked;
|
||||
callback netplay_discord_button_clicked;
|
||||
callback netplay_feedback_button_clicked;
|
||||
callback netplay_begin_game;
|
||||
callback netplay_begin_game(int);
|
||||
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_create_session(string, string, string, string, string, string, bool, bool);
|
||||
callback netplay_select_rom(string);
|
||||
callback netplay_join_session(string, string, string, string, int);
|
||||
callback netplay_join_session(string, string, string, string);
|
||||
callback netplay_close;
|
||||
callback cheats_select_rom_clicked(string);
|
||||
callback cheats_clear_clicked;
|
||||
@@ -62,10 +61,11 @@ export component AppWindow inherits Window {
|
||||
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;
|
||||
in-out property ra_casual_only <=> RAData.ra_casual_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 theme <=> SettingsData.theme;
|
||||
in-out property fullscreen <=> SettingsData.fullscreen;
|
||||
in-out property widescreen <=> SettingsData.widescreen;
|
||||
in-out property vsync <=> SettingsData.vsync;
|
||||
@@ -74,6 +74,7 @@ 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 rewind <=> SettingsData.rewind;
|
||||
in-out property emulate_vru <=> SettingsData.emulate_vru;
|
||||
in-out property rom_dir <=> SettingsData.rom_dir;
|
||||
in-out property controller_enabled <=> ControllerData.controller_enabled;
|
||||
@@ -98,31 +99,28 @@ export component AppWindow inherits Window {
|
||||
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 show_netplay_wait_session <=> NetplayData.show_wait_session;
|
||||
in-out property show_netplay_create_session <=> NetplayData.show_create_session;
|
||||
in-out property show_netplay_join_session <=> NetplayData.show_join_session;
|
||||
|
||||
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_player_name <=> NetplayData.player_name;
|
||||
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;
|
||||
in-out property netplay_input_delay <=> NetplayWaitData.input_delay;
|
||||
in-out property netplay_recommended_delay <=> NetplayWaitData.recommended_delay;
|
||||
in-out property netplay_game_pal <=> NetplayWaitData.game_pal;
|
||||
in-out property netplay_connected_peers_count <=> NetplayWaitData.connected_peers_count;
|
||||
|
||||
private property <string> popup_message;
|
||||
private property <bool> popup_has_close;
|
||||
@@ -134,11 +132,26 @@ export component AppWindow inherits Window {
|
||||
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;
|
||||
function apply_theme() {
|
||||
if (root.theme == 1) {
|
||||
Palette.color-scheme = ColorScheme.dark;
|
||||
} else if (root.theme == 2) {
|
||||
Palette.color-scheme = ColorScheme.light;
|
||||
} else {
|
||||
Palette.color-scheme = ColorScheme.unknown;
|
||||
}
|
||||
}
|
||||
init => {
|
||||
apply_theme();
|
||||
}
|
||||
changed theme => {
|
||||
apply_theme();
|
||||
}
|
||||
|
||||
min-width: 320px;
|
||||
min-height: 240px;
|
||||
preferred-width: 800px;
|
||||
preferred-height: 600px;
|
||||
preferred-height: 650px;
|
||||
title: "Gopher64";
|
||||
icon: @image-url("../../../data/icon/gopher64.png");
|
||||
|
||||
@@ -224,7 +237,7 @@ export component AppWindow inherits Window {
|
||||
input_profile_creation_button_clicked();
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 2 && !show_netplay_wait_room && !show_netplay_create_room && !show_netplay_join_room): Netplay {
|
||||
if(side-bar.current-item == 2 && !show_netplay_wait_session && !show_netplay_create_session && !show_netplay_join_session): Netplay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
create_session_button_clicked => {
|
||||
@@ -240,36 +253,33 @@ export component AppWindow inherits Window {
|
||||
netplay_feedback_button_clicked();
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 2 && show_netplay_wait_room): NetplayWait {
|
||||
if(side-bar.current-item == 2 && show_netplay_wait_session): NetplayWait {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
send_chat_message(message) => {
|
||||
netplay_send_chat_message(message);
|
||||
}
|
||||
begin_game => {
|
||||
netplay_begin_game();
|
||||
begin_game(a) => {
|
||||
netplay_begin_game(a);
|
||||
}
|
||||
close => {
|
||||
netplay_close();
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 2 && show_netplay_create_room && !show_netplay_wait_room): NetplayCreate {
|
||||
if(side-bar.current-item == 2 && show_netplay_create_session && !show_netplay_wait_session): 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);
|
||||
create_session(a, b, c, d, e, f, g, h) => {
|
||||
netplay_create_session(a,b,c,d,e,f,g,h);
|
||||
}
|
||||
close => {
|
||||
netplay_close();
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 2 && show_netplay_join_room && !show_netplay_wait_room): NetplayJoin {
|
||||
if(side-bar.current-item == 2 && show_netplay_join_session && !show_netplay_wait_session): NetplayJoin {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
refresh_sessions => {
|
||||
@@ -278,8 +288,8 @@ export component AppWindow inherits Window {
|
||||
select_rom(rom_dir) => {
|
||||
netplay_select_rom(rom_dir);
|
||||
}
|
||||
join_session(a, b, c, d, e) => {
|
||||
netplay_join_session(a,b,c,d,e);
|
||||
join_session(a, b, c, d) => {
|
||||
netplay_join_session(a,b,c,d);
|
||||
}
|
||||
close => {
|
||||
netplay_close();
|
||||
@@ -370,15 +380,6 @@ export component AppWindow inherits Window {
|
||||
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;
|
||||
@@ -393,9 +394,14 @@ export component AppWindow inherits Window {
|
||||
NetplayData.game_name = "";
|
||||
NetplayData.password = "";
|
||||
NetplayData.session_name = "";
|
||||
NetplayData.custom_server_url = "";
|
||||
NetplayWaitData.chat_text = "";
|
||||
NetplayWaitData.input_delay = 0;
|
||||
NetplayWaitData.recommended_delay = 0;
|
||||
NetplayWaitData.players = [];
|
||||
NetplayWaitData.connected_peers_count = 0;
|
||||
NetplayData.pending_session = false;
|
||||
NetplayData.pending_refresh = false;
|
||||
NetplayData.sessions = [];
|
||||
NetplayData.current_session = -1;
|
||||
}
|
||||
}
|
||||
|
||||
+30
-35
@@ -47,43 +47,38 @@ export component Cheats inherits Page {
|
||||
|
||||
Rectangle { }
|
||||
|
||||
if CheatsData.cheats.length > 0:ListView {
|
||||
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;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
if data.notes != "":Text {
|
||||
text: data.notes;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
if data.options.length == 0: Switch {
|
||||
checked: data.enabled;
|
||||
toggled => {
|
||||
data.enabled = self.checked;
|
||||
cheat_toggled(CheatsData.cheat_game_crc,data.cheat_name,"",self.checked);
|
||||
}
|
||||
}
|
||||
for item in data.options: CheckBox {
|
||||
text: item.option;
|
||||
enabled: !data.enabled || (data.enabled && item.enabled);
|
||||
checked: item.enabled;
|
||||
toggled => {
|
||||
item.enabled = self.checked;
|
||||
data.enabled = self.checked;
|
||||
cheat_toggled(CheatsData.cheat_game_crc,data.cheat_name,item.option,self.checked);
|
||||
}
|
||||
}
|
||||
Rectangle { }
|
||||
for data in CheatsData.cheats: VerticalBox {
|
||||
Text {
|
||||
text: data.cheat_name;
|
||||
font-size: 16px;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
if data.notes != "":Text {
|
||||
text: data.notes;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
if data.options.length == 0: Switch {
|
||||
checked: data.enabled;
|
||||
toggled => {
|
||||
data.enabled = self.checked;
|
||||
cheat_toggled(CheatsData.cheat_game_crc,data.cheat_name,"",self.checked);
|
||||
}
|
||||
}
|
||||
for item in data.options: CheckBox {
|
||||
text: item.option;
|
||||
enabled: !data.enabled || (data.enabled && item.enabled);
|
||||
checked: item.enabled;
|
||||
toggled => {
|
||||
item.enabled = self.checked;
|
||||
data.enabled = self.checked;
|
||||
cheat_toggled(CheatsData.cheat_game_crc,data.cheat_name,item.option,self.checked);
|
||||
}
|
||||
}
|
||||
Rectangle { }
|
||||
}
|
||||
|
||||
if CheatsData.cheats.length == 0 && CheatsData.cheat_game_name != "": Text {
|
||||
text: @tr("No Cheats Available");
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@ import { Page } from "page.slint";
|
||||
|
||||
export component NetplayCreate inherits Page {
|
||||
title: @tr("Netplay: Create Session");
|
||||
callback get_custom_url();
|
||||
callback select_rom(string);
|
||||
callback create_session(string, string, string, string, string, string, string);
|
||||
callback create_session(string, string, string, string, string, string, bool, bool);
|
||||
callback close;
|
||||
|
||||
VerticalBox {
|
||||
@@ -61,25 +60,6 @@ export component NetplayCreate inherits Page {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -108,24 +88,19 @@ export component NetplayCreate inherits Page {
|
||||
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);
|
||||
NetplayData.password,
|
||||
SettingsData.overclock_n64_cpu,
|
||||
SettingsData.disable_expansion_pak);
|
||||
}
|
||||
}
|
||||
Button {
|
||||
@@ -140,43 +115,3 @@ export component NetplayCreate inherits Page {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export component NetplayJoin inherits Page {
|
||||
title: @tr("Netplay: Join Session");
|
||||
callback refresh_sessions();
|
||||
callback select_rom(string);
|
||||
callback join_session(string, string, string, string, int);
|
||||
callback join_session(string, string, string, string);
|
||||
callback close;
|
||||
|
||||
VerticalBox {
|
||||
@@ -37,18 +37,7 @@ export component NetplayJoin inherits Page {
|
||||
}
|
||||
|
||||
VerticalBox {
|
||||
Text {
|
||||
text: @tr("Custom Server URL (optional):");
|
||||
}
|
||||
|
||||
LineEdit {
|
||||
placeholder-text: "example.com:45000";
|
||||
text: NetplayData.custom_server_url;
|
||||
edited(text) => {
|
||||
NetplayData.custom_server_url = text;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {}
|
||||
if NetplayData.game_name == "": Button {
|
||||
text: @tr("Select ROM");
|
||||
clicked => {
|
||||
@@ -81,7 +70,6 @@ export component NetplayJoin inherits Page {
|
||||
}
|
||||
StandardTableView {
|
||||
columns: [
|
||||
{ title: @tr("Server") },
|
||||
{ title: @tr("Session Name") },
|
||||
{ title: @tr("Game Name") },
|
||||
{ title: @tr("Password") },
|
||||
@@ -104,8 +92,6 @@ export component NetplayJoin inherits Page {
|
||||
NetplayData.pending_refresh = true;
|
||||
NetplayData.current_session = -1;
|
||||
NetplayData.sessions = [];
|
||||
NetplayData.room_urls = [];
|
||||
NetplayData.room_ports = [];
|
||||
refresh_sessions();
|
||||
}
|
||||
}
|
||||
@@ -121,11 +107,10 @@ export component NetplayJoin inherits Page {
|
||||
clicked => {
|
||||
NetplayData.pending_session = true;
|
||||
join_session(
|
||||
NetplayData.sessions[NetplayData.current_session][0].text,
|
||||
NetplayData.player_name,
|
||||
NetplayData.game_hash,
|
||||
NetplayData.password,
|
||||
NetplayData.room_urls[NetplayData.current_session],
|
||||
NetplayData.room_ports[NetplayData.current_session]);
|
||||
NetplayData.password);
|
||||
}
|
||||
}
|
||||
Button {
|
||||
|
||||
@@ -6,27 +6,21 @@ import { Page } from "page.slint";
|
||||
import { State } from "localgame_page.slint";
|
||||
|
||||
export global NetplayData {
|
||||
in-out property <[string]> server_names;
|
||||
in-out property <[string]> server_urls;
|
||||
in-out property <string> player_name;
|
||||
in-out property <string> game_name;
|
||||
in-out property <string> game_hash;
|
||||
in-out property <string> game_cheats;
|
||||
in-out property <string> rom_path;
|
||||
in-out property <string> password;
|
||||
in-out property <string> peer_addr;
|
||||
in-out property <bool> pending_session;
|
||||
in-out property <bool> pending_refresh;
|
||||
in-out property <string> session_name;
|
||||
in-out property <[[StandardListViewItem]]> sessions;
|
||||
in-out property <[string]> room_urls;
|
||||
in-out property <[int]> room_ports;
|
||||
in-out property <int> current_session: -1;
|
||||
in-out property <string> custom_server_url;
|
||||
|
||||
in-out property <bool> show_wait_room;
|
||||
in-out property <bool> show_create_room;
|
||||
in-out property <bool> show_join_room;
|
||||
in-out property <bool> show_wait_session;
|
||||
in-out property <bool> show_create_session;
|
||||
in-out property <bool> show_join_session;
|
||||
}
|
||||
|
||||
export component Netplay inherits Page {
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { VerticalBox, HorizontalBox, TextEdit, LineEdit, Button, GridBox, ScrollView } from "std-widgets.slint";
|
||||
import { VerticalBox, HorizontalBox, TextEdit, SpinBox, GroupBox, LineEdit, Button, GridBox, ScrollView } from "std-widgets.slint";
|
||||
import { NetplayData } from "netplay_page.slint";
|
||||
import { Page } from "page.slint";
|
||||
|
||||
export global NetplayWaitData {
|
||||
in-out property <string> motd;
|
||||
in-out property <[string]> players;
|
||||
in-out property <int> port;
|
||||
in-out property <bool> can_start;
|
||||
in-out property <string> chat_text;
|
||||
in-out property <string> ping;
|
||||
in-out property <int> input_delay;
|
||||
in-out property <int> recommended_delay;
|
||||
in-out property <bool> game_pal;
|
||||
in-out property <int> connected_peers_count;
|
||||
}
|
||||
|
||||
export component NetplayWait inherits Page {
|
||||
title: @tr("Netplay: Waiting Room");
|
||||
callback send_chat_message(string);
|
||||
callback begin_game;
|
||||
callback begin_game(int);
|
||||
callback close;
|
||||
VerticalBox {
|
||||
alignment: start;
|
||||
@@ -35,11 +37,6 @@ export component NetplayWait inherits Page {
|
||||
horizontal-alignment: center;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: @tr("Ping:") + " " + NetplayWaitData.ping;
|
||||
horizontal-alignment: center;
|
||||
}
|
||||
|
||||
HorizontalBox {
|
||||
alignment: center;
|
||||
GridBox {
|
||||
@@ -67,7 +64,7 @@ export component NetplayWait inherits Page {
|
||||
|
||||
TextEdit {
|
||||
read-only: true;
|
||||
min-height: 120px;
|
||||
min-height: 110px;
|
||||
text: NetplayWaitData.chat_text;
|
||||
}
|
||||
|
||||
@@ -94,19 +91,44 @@ export component NetplayWait inherits Page {
|
||||
}
|
||||
|
||||
Text {
|
||||
text: NetplayWaitData.motd;
|
||||
text: NetplayData.player_name == NetplayWaitData.players[0] ? @tr("Setting the input delay too low will cause the game to lag.\nYou can lower the input delay in-game by pressing Alt+[ and raise it with Alt+].") : NetplayWaitData.motd;
|
||||
horizontal-alignment: center;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
if NetplayData.player_name == NetplayWaitData.players[0]: HorizontalBox {
|
||||
alignment: center;
|
||||
Text {
|
||||
text: @tr("Local Input Delay:");
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
SpinBox {
|
||||
minimum: 1;
|
||||
maximum: 16;
|
||||
value: NetplayWaitData.input_delay;
|
||||
enabled: NetplayWaitData.recommended_delay != 0;
|
||||
edited => {
|
||||
NetplayWaitData.input_delay = self.value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: @tr("Recommended: ") + (NetplayWaitData.recommended_delay == 0 ? "Calculating..." : NetplayWaitData.recommended_delay);
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
HorizontalBox {
|
||||
alignment: center;
|
||||
Button {
|
||||
text: @tr("Start Game");
|
||||
enabled: NetplayWaitData.can_start;
|
||||
property <bool> mesh_complete: NetplayWaitData.players.length == (NetplayWaitData.connected_peers_count + 1);
|
||||
text: mesh_complete ? @tr("Start Game") : @tr("Waiting for P2P mesh ") + (NetplayWaitData.connected_peers_count + 1) + "/" + NetplayWaitData.players.length;
|
||||
enabled: NetplayWaitData.players[0] == NetplayData.player_name && NetplayWaitData.can_start && NetplayWaitData.recommended_delay != 0 && mesh_complete;
|
||||
clicked => {
|
||||
NetplayWaitData.can_start = false;
|
||||
begin_game();
|
||||
begin_game(NetplayWaitData.input_delay);
|
||||
}
|
||||
}
|
||||
Button {
|
||||
|
||||
@@ -12,7 +12,7 @@ export global RAData {
|
||||
in-out property <bool> rich_presence;
|
||||
in-out property <bool> ra_logging_in;
|
||||
in-out property <bool> ra_show_profile;
|
||||
in-out property <bool> ra_softcore_only;
|
||||
in-out property <bool> ra_casual_only;
|
||||
in-out property <string> ra_username;
|
||||
in-out property <string> ra_password;
|
||||
in-out property <string> current_user_message;
|
||||
@@ -47,7 +47,7 @@ export component RetroAchievements inherits Page {
|
||||
ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge,RAData.leaderboard,RAData.rich_presence);
|
||||
}
|
||||
}
|
||||
if !RAData.ra_softcore_only: Switch {
|
||||
if !RAData.ra_casual_only: Switch {
|
||||
text: @tr("Hardcore mode");
|
||||
checked: RAData.hardcore;
|
||||
enabled: RAData.enabled;
|
||||
|
||||
+132
-97
@@ -1,7 +1,7 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { ComboBox, HorizontalBox, VerticalBox, Switch } from "std-widgets.slint";
|
||||
import { GroupBox, ComboBox, HorizontalBox, VerticalBox, Switch } from "std-widgets.slint";
|
||||
import { Page } from "page.slint";
|
||||
|
||||
export global SettingsData {
|
||||
@@ -14,7 +14,9 @@ export global SettingsData {
|
||||
in-out property <bool> overclock_n64_cpu;
|
||||
in-out property <bool> disable_expansion_pak;
|
||||
in-out property <int> resolution;
|
||||
in-out property <int> theme;
|
||||
in-out property <bool> emulate_usb;
|
||||
in-out property <bool> rewind;
|
||||
in-out property <bool> emulate_vru;
|
||||
in-out property <string> rom_dir;
|
||||
}
|
||||
@@ -22,109 +24,142 @@ export global SettingsData {
|
||||
export component Settings inherits Page {
|
||||
title: @tr("Settings");
|
||||
|
||||
VerticalBox {
|
||||
alignment: start;
|
||||
HorizontalBox {
|
||||
alignment: center;
|
||||
VerticalBox {
|
||||
alignment: start;
|
||||
HorizontalBox {
|
||||
HorizontalBox {
|
||||
alignment: center;
|
||||
VerticalBox {
|
||||
alignment: start;
|
||||
GroupBox {
|
||||
title: @tr("Game");
|
||||
VerticalBox {
|
||||
alignment: start;
|
||||
Text {
|
||||
text: @tr("Resolution:");
|
||||
vertical-alignment: center;
|
||||
HorizontalBox {
|
||||
alignment: start;
|
||||
Text {
|
||||
text: @tr("Resolution:");
|
||||
vertical-alignment: center;
|
||||
}
|
||||
ComboBox {
|
||||
model: ["1x", "2x", "4x", "8x"];
|
||||
current-index: SettingsData.resolution;
|
||||
selected => {
|
||||
SettingsData.resolution = self.current-index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
model: ["1x", "2x", "4x", "8x"];
|
||||
current-index: SettingsData.resolution;
|
||||
selected => {
|
||||
SettingsData.resolution = self.current-index;
|
||||
Switch {
|
||||
text: @tr("SSAA downscaling");
|
||||
checked: SettingsData.ssaa;
|
||||
toggled => {
|
||||
SettingsData.ssaa = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("Integer Scaling");
|
||||
checked: SettingsData.integer_scaling;
|
||||
toggled => {
|
||||
SettingsData.integer_scaling = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("Fullscreen");
|
||||
checked: SettingsData.fullscreen;
|
||||
toggled => {
|
||||
SettingsData.fullscreen = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("Widescreen (stretch)");
|
||||
checked: SettingsData.widescreen;
|
||||
toggled => {
|
||||
SettingsData.widescreen = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("VSync");
|
||||
checked: SettingsData.vsync;
|
||||
toggled => {
|
||||
SettingsData.vsync = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("Apply CRT shader");
|
||||
checked: SettingsData.apply_crt_shader;
|
||||
toggled => {
|
||||
SettingsData.apply_crt_shader = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("Overclock N64 CPU (may cause bugs)");
|
||||
checked: SettingsData.overclock_n64_cpu;
|
||||
toggled => {
|
||||
SettingsData.overclock_n64_cpu = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("Disable Expansion Pak");
|
||||
checked: SettingsData.disable_expansion_pak;
|
||||
toggled => {
|
||||
SettingsData.disable_expansion_pak = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("Enable Rewind");
|
||||
checked: SettingsData.rewind;
|
||||
toggled => {
|
||||
SettingsData.rewind = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("Emulate UNFLoader");
|
||||
checked: SettingsData.emulate_usb;
|
||||
toggled => {
|
||||
SettingsData.emulate_usb = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("Emulate VRU");
|
||||
checked: SettingsData.emulate_vru;
|
||||
toggled => {
|
||||
SettingsData.emulate_vru = self.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("SSAA downscaling");
|
||||
checked: SettingsData.ssaa;
|
||||
toggled => {
|
||||
SettingsData.ssaa = self.checked;
|
||||
}
|
||||
GroupBox {
|
||||
title: @tr("UI");
|
||||
VerticalBox {
|
||||
alignment: start;
|
||||
HorizontalBox {
|
||||
alignment: start;
|
||||
Text {
|
||||
text: @tr("Theme:");
|
||||
vertical-alignment: center;
|
||||
}
|
||||
ComboBox {
|
||||
model: [@tr("System"), @tr("Dark"), @tr("Light")];
|
||||
current-index: SettingsData.theme;
|
||||
selected => {
|
||||
SettingsData.theme = self.current-index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("Integer Scaling");
|
||||
checked: SettingsData.integer_scaling;
|
||||
toggled => {
|
||||
SettingsData.integer_scaling = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("Fullscreen");
|
||||
checked: SettingsData.fullscreen;
|
||||
toggled => {
|
||||
SettingsData.fullscreen = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("Widescreen (stretch)");
|
||||
checked: SettingsData.widescreen;
|
||||
toggled => {
|
||||
SettingsData.widescreen = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("VSync");
|
||||
checked: SettingsData.vsync;
|
||||
toggled => {
|
||||
SettingsData.vsync = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("Apply CRT shader");
|
||||
checked: SettingsData.apply_crt_shader;
|
||||
toggled => {
|
||||
SettingsData.apply_crt_shader = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("Overclock N64 CPU (may cause bugs)");
|
||||
checked: SettingsData.overclock_n64_cpu;
|
||||
toggled => {
|
||||
SettingsData.overclock_n64_cpu = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("Disable Expansion Pak");
|
||||
checked: SettingsData.disable_expansion_pak;
|
||||
toggled => {
|
||||
SettingsData.disable_expansion_pak = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("Emulate UNFLoader");
|
||||
checked: SettingsData.emulate_usb;
|
||||
toggled => {
|
||||
SettingsData.emulate_usb = self.checked;
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: @tr("Emulate VRU");
|
||||
checked: SettingsData.emulate_vru;
|
||||
toggled => {
|
||||
SettingsData.emulate_vru = self.checked;
|
||||
Rectangle {
|
||||
height: 40px; // so the combobox isn't cut off
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,11 @@ component SideBarItem inherits Rectangle {
|
||||
}
|
||||
|
||||
button := SideBarButton {
|
||||
Tooltip {
|
||||
Text {
|
||||
text: root.label;
|
||||
}
|
||||
}
|
||||
icon: root.icon;
|
||||
selected: root.selected;
|
||||
has-focus: root.has-focus;
|
||||
|
||||
+181
-812
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,929 @@
|
||||
use crate::ui;
|
||||
|
||||
const R_DPAD: usize = 0;
|
||||
const L_DPAD: usize = 1;
|
||||
const D_DPAD: usize = 2;
|
||||
const U_DPAD: usize = 3;
|
||||
pub const START_BUTTON: usize = 4;
|
||||
pub const Z_TRIG: usize = 5;
|
||||
pub const B_BUTTON: usize = 6;
|
||||
const A_BUTTON: usize = 7;
|
||||
const R_CBUTTON: usize = 8;
|
||||
pub const L_CBUTTON: usize = 9;
|
||||
const D_CBUTTON: usize = 10;
|
||||
const U_CBUTTON: usize = 11;
|
||||
pub const R_TRIG: usize = 12;
|
||||
pub const L_TRIG: usize = 13;
|
||||
pub const AXIS_RIGHT: usize = 14;
|
||||
pub const AXIS_LEFT: usize = 15;
|
||||
pub const AXIS_DOWN: usize = 16;
|
||||
pub const AXIS_UP: usize = 17;
|
||||
pub const HOTKEY: usize = 18;
|
||||
pub const PROFILE_SIZE: usize = 19;
|
||||
|
||||
pub const DEADZONE_DEFAULT: i32 = 5;
|
||||
|
||||
const GATHER_TIME: usize = 5;
|
||||
|
||||
pub fn configure_input_profile(
|
||||
config: &mut ui::config::Config,
|
||||
profile_name: String,
|
||||
dinput: bool,
|
||||
deadzone: i32,
|
||||
) {
|
||||
ui::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
|
||||
ui::sdl_init(sdl3_sys::init::SDL_INIT_GAMEPAD);
|
||||
ui::ttf_init();
|
||||
|
||||
if profile_name == "default" {
|
||||
eprintln!("Profile name cannot be default");
|
||||
return;
|
||||
}
|
||||
if profile_name.is_empty() {
|
||||
eprintln!("Profile name cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut profile = if let Some(profile) = config.input.input_profiles.get(&profile_name) {
|
||||
profile.clone()
|
||||
} else {
|
||||
ui::config::InputProfile {
|
||||
inputs: [const { [None, None] }; PROFILE_SIZE],
|
||||
dinput,
|
||||
deadzone,
|
||||
}
|
||||
};
|
||||
|
||||
let mut open_joysticks: Vec<*mut sdl3_sys::joystick::SDL_Joystick> = Vec::new();
|
||||
let mut open_controllers: Vec<*mut sdl3_sys::gamepad::SDL_Gamepad> = Vec::new();
|
||||
|
||||
for joystick in ui::input::get_joysticks().iter() {
|
||||
if !dinput {
|
||||
let controller = unsafe { sdl3_sys::gamepad::SDL_OpenGamepad(*joystick) };
|
||||
if !controller.is_null() {
|
||||
open_controllers.push(controller);
|
||||
}
|
||||
} else {
|
||||
let joystick = unsafe { sdl3_sys::joystick::SDL_OpenJoystick(*joystick) };
|
||||
if !joystick.is_null() {
|
||||
open_joysticks.push(joystick);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let title = std::ffi::CString::new("configure input profile").unwrap();
|
||||
let mut window: *mut sdl3_sys::video::SDL_Window = std::ptr::null_mut();
|
||||
let mut renderer: *mut sdl3_sys::render::SDL_Renderer = std::ptr::null_mut();
|
||||
#[cfg(target_os = "android")]
|
||||
let window_flags = sdl3_sys::video::SDL_WINDOW_FULLSCREEN;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let window_flags = sdl3_sys::video::SDL_WindowFlags(0);
|
||||
if !unsafe {
|
||||
sdl3_sys::render::SDL_CreateWindowAndRenderer(
|
||||
title.as_ptr(),
|
||||
852,
|
||||
480,
|
||||
window_flags,
|
||||
&mut window,
|
||||
&mut renderer,
|
||||
)
|
||||
} {
|
||||
panic!("Could not create window and renderer")
|
||||
}
|
||||
if !unsafe { sdl3_sys::video::SDL_ShowWindow(window) } {
|
||||
panic!("Could not show window")
|
||||
}
|
||||
|
||||
let text_engine = unsafe { sdl3_ttf_sys::ttf::TTF_CreateRendererTextEngine(renderer) };
|
||||
let font = unsafe {
|
||||
let font_bytes = include_bytes!("../../data/ui/RobotoMono-Regular.ttf");
|
||||
sdl3_ttf_sys::ttf::TTF_OpenFontIO(
|
||||
sdl3_sys::everything::SDL_IOFromConstMem(
|
||||
font_bytes.as_ptr() as *const std::ffi::c_void,
|
||||
font_bytes.len(),
|
||||
),
|
||||
true,
|
||||
12.0,
|
||||
)
|
||||
};
|
||||
|
||||
set_font_size(window, font);
|
||||
|
||||
if draw_profile_screen(window, renderer, text_engine, font, &mut profile, dinput) {
|
||||
profile.deadzone = deadzone;
|
||||
profile.dinput = dinput;
|
||||
config.input.input_profiles.insert(profile_name, profile);
|
||||
}
|
||||
|
||||
for joystick in open_joysticks {
|
||||
unsafe { sdl3_sys::joystick::SDL_CloseJoystick(joystick) }
|
||||
}
|
||||
for controller in open_controllers {
|
||||
unsafe { sdl3_sys::gamepad::SDL_CloseGamepad(controller) }
|
||||
}
|
||||
|
||||
unsafe {
|
||||
sdl3_ttf_sys::ttf::TTF_CloseFont(font);
|
||||
sdl3_ttf_sys::ttf::TTF_DestroyRendererTextEngine(text_engine);
|
||||
sdl3_sys::render::SDL_DestroyRenderer(renderer);
|
||||
sdl3_sys::video::SDL_DestroyWindow(window);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_font_size(window: *mut sdl3_sys::video::SDL_Window, font: *mut sdl3_ttf_sys::ttf::TTF_Font) {
|
||||
let mut rect: sdl3_sys::rect::SDL_Rect = Default::default();
|
||||
unsafe {
|
||||
sdl3_ttf_sys::ttf::TTF_SetFontSize(font, 12.0);
|
||||
sdl3_sys::video::SDL_GetWindowSafeArea(window, &mut rect)
|
||||
};
|
||||
let num_rows = PROFILE_SIZE + 2;
|
||||
let min_row_height = rect.h / num_rows as i32;
|
||||
while unsafe { sdl3_ttf_sys::ttf::TTF_GetFontLineSkip(font) } < min_row_height {
|
||||
unsafe {
|
||||
let font_size = sdl3_ttf_sys::ttf::TTF_GetFontSize(font);
|
||||
sdl3_ttf_sys::ttf::TTF_SetFontSize(font, font_size + 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_gather(auto_advance: bool) -> (usize, Option<std::time::Instant>) {
|
||||
if auto_advance {
|
||||
(GATHER_TIME, Some(std::time::Instant::now()))
|
||||
} else {
|
||||
(0, None)
|
||||
}
|
||||
}
|
||||
|
||||
fn advance_cursor(
|
||||
selected: &mut (usize, usize),
|
||||
auto_advance: &mut bool,
|
||||
) -> (usize, Option<std::time::Instant>) {
|
||||
selected.0 += 1;
|
||||
if selected.0 == PROFILE_SIZE {
|
||||
*auto_advance = false;
|
||||
}
|
||||
reset_gather(*auto_advance)
|
||||
}
|
||||
|
||||
fn draw_profile_screen(
|
||||
window: *mut sdl3_sys::video::SDL_Window,
|
||||
renderer: *mut sdl3_sys::render::SDL_Renderer,
|
||||
text_engine: *mut sdl3_ttf_sys::ttf::TTF_TextEngine,
|
||||
font: *mut sdl3_ttf_sys::ttf::TTF_Font,
|
||||
profile: &mut ui::config::InputProfile,
|
||||
dinput: bool,
|
||||
) -> bool {
|
||||
let mut selected: (usize, usize) = (0, 0);
|
||||
let mut auto_advance = dinput && !unsafe { sdl3_sys::keyboard::SDL_HasKeyboard() };
|
||||
|
||||
let (mut gather, mut gather_timer) = reset_gather(auto_advance);
|
||||
loop {
|
||||
if let Some(timer) = gather_timer
|
||||
&& timer.elapsed() > std::time::Duration::from_secs(1)
|
||||
{
|
||||
gather -= 1;
|
||||
if gather > 0 {
|
||||
gather_timer = Some(std::time::Instant::now());
|
||||
} else {
|
||||
(gather, gather_timer) = reset_gather(auto_advance);
|
||||
}
|
||||
}
|
||||
let mut event: sdl3_sys::events::SDL_Event = Default::default();
|
||||
|
||||
if unsafe { sdl3_sys::events::SDL_WaitEventTimeout(&mut event, 20) } {
|
||||
match event.event_type() {
|
||||
sdl3_sys::events::SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED => {
|
||||
set_font_size(window, font);
|
||||
}
|
||||
sdl3_sys::events::SDL_EVENT_WINDOW_CLOSE_REQUESTED => break,
|
||||
sdl3_sys::events::SDL_EVENT_GAMEPAD_BUTTON_DOWN => {
|
||||
if gather == 0 {
|
||||
match sdl3_sys::gamepad::SDL_GamepadButton(
|
||||
unsafe { event.gbutton.button } as i32
|
||||
) {
|
||||
sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_DPAD_DOWN => {
|
||||
move_down(&mut selected);
|
||||
}
|
||||
sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_DPAD_UP => {
|
||||
move_up(&mut selected);
|
||||
}
|
||||
sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_DPAD_LEFT => {
|
||||
move_left(&mut selected);
|
||||
}
|
||||
sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_DPAD_RIGHT => {
|
||||
move_right(&mut selected);
|
||||
}
|
||||
sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_WEST
|
||||
| sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_NORTH => {
|
||||
delete_binding(selected, profile);
|
||||
}
|
||||
sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_SOUTH
|
||||
| sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_EAST
|
||||
if select_or_save(selected, &mut gather, &mut gather_timer) =>
|
||||
{
|
||||
return true;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
} else if !dinput {
|
||||
profile.inputs[selected.0][selected.1] = Some(
|
||||
ui::config::InputItem::ControllerButton(ui::config::InputKeyButton {
|
||||
id: i32::from(unsafe { event.gbutton.button }),
|
||||
}),
|
||||
);
|
||||
(gather, gather_timer) = advance_cursor(&mut selected, &mut auto_advance);
|
||||
}
|
||||
}
|
||||
sdl3_sys::events::SDL_EVENT_KEY_DOWN => {
|
||||
let scancode = unsafe { event.key }.scancode;
|
||||
if gather == 0 || scancode == sdl3_sys::scancode::SDL_SCANCODE_AC_BACK {
|
||||
match scancode {
|
||||
sdl3_sys::scancode::SDL_SCANCODE_AC_BACK => break,
|
||||
sdl3_sys::scancode::SDL_SCANCODE_DOWN => {
|
||||
move_down(&mut selected);
|
||||
}
|
||||
sdl3_sys::scancode::SDL_SCANCODE_UP => {
|
||||
move_up(&mut selected);
|
||||
}
|
||||
sdl3_sys::scancode::SDL_SCANCODE_LEFT => {
|
||||
move_left(&mut selected);
|
||||
}
|
||||
sdl3_sys::scancode::SDL_SCANCODE_RIGHT => {
|
||||
move_right(&mut selected);
|
||||
}
|
||||
sdl3_sys::scancode::SDL_SCANCODE_DELETE
|
||||
| sdl3_sys::scancode::SDL_SCANCODE_BACKSPACE => {
|
||||
delete_binding(selected, profile);
|
||||
}
|
||||
sdl3_sys::scancode::SDL_SCANCODE_RETURN
|
||||
if select_or_save(selected, &mut gather, &mut gather_timer) =>
|
||||
{
|
||||
return true;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
} else {
|
||||
if scancode != sdl3_sys::scancode::SDL_SCANCODE_LALT
|
||||
&& scancode != sdl3_sys::scancode::SDL_SCANCODE_RALT
|
||||
&& !unsafe { event.key }.repeat
|
||||
{
|
||||
profile.inputs[selected.0][selected.1] =
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(scancode),
|
||||
}));
|
||||
(gather, gather_timer) =
|
||||
advance_cursor(&mut selected, &mut auto_advance);
|
||||
}
|
||||
}
|
||||
}
|
||||
sdl3_sys::events::SDL_EVENT_GAMEPAD_AXIS_MOTION => {
|
||||
let axis_value = unsafe { event.gaxis }.value;
|
||||
let axis = unsafe { event.gaxis }.axis;
|
||||
if !dinput
|
||||
&& gather != 0
|
||||
&& axis_value.saturating_abs() > (i16::MAX as i32 * 3 / 4) as i16
|
||||
{
|
||||
profile.inputs[selected.0][selected.1] =
|
||||
Some(ui::config::InputItem::ControllerAxis(
|
||||
ui::config::InputControllerAxis {
|
||||
id: i32::from(axis),
|
||||
axis: axis_value.signum(),
|
||||
initial_state: 0,
|
||||
},
|
||||
));
|
||||
(gather, gather_timer) = advance_cursor(&mut selected, &mut auto_advance);
|
||||
}
|
||||
}
|
||||
sdl3_sys::events::SDL_EVENT_JOYSTICK_BUTTON_DOWN => {
|
||||
if dinput {
|
||||
if gather == 0 {
|
||||
if let Some(ui::config::InputItem::JoystickButton(joystick_button)) =
|
||||
&profile.inputs[D_DPAD][0]
|
||||
&& joystick_button.id == unsafe { event.jbutton.button } as i32
|
||||
{
|
||||
move_down(&mut selected);
|
||||
} else if let Some(ui::config::InputItem::JoystickButton(
|
||||
joystick_button,
|
||||
)) = &profile.inputs[U_DPAD][0]
|
||||
&& joystick_button.id == unsafe { event.jbutton.button } as i32
|
||||
{
|
||||
move_up(&mut selected);
|
||||
} else if let Some(ui::config::InputItem::JoystickButton(
|
||||
joystick_button,
|
||||
)) = &profile.inputs[L_DPAD][0]
|
||||
&& joystick_button.id == unsafe { event.jbutton.button } as i32
|
||||
{
|
||||
move_left(&mut selected);
|
||||
} else if let Some(ui::config::InputItem::JoystickButton(
|
||||
joystick_button,
|
||||
)) = &profile.inputs[R_DPAD][0]
|
||||
&& joystick_button.id == unsafe { event.jbutton.button } as i32
|
||||
{
|
||||
move_right(&mut selected);
|
||||
} else if let Some(ui::config::InputItem::JoystickButton(
|
||||
joystick_button,
|
||||
)) = &profile.inputs[B_BUTTON][0]
|
||||
&& joystick_button.id == unsafe { event.jbutton.button } as i32
|
||||
{
|
||||
delete_binding(selected, profile);
|
||||
} else if let Some(ui::config::InputItem::JoystickButton(
|
||||
joystick_button,
|
||||
)) = &profile.inputs[A_BUTTON][0]
|
||||
&& joystick_button.id == unsafe { event.jbutton.button } as i32
|
||||
&& select_or_save(selected, &mut gather, &mut gather_timer)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
profile.inputs[selected.0][selected.1] = Some(
|
||||
ui::config::InputItem::JoystickButton(ui::config::InputKeyButton {
|
||||
id: i32::from(unsafe { event.jbutton.button }),
|
||||
}),
|
||||
);
|
||||
(gather, gather_timer) =
|
||||
advance_cursor(&mut selected, &mut auto_advance);
|
||||
}
|
||||
}
|
||||
}
|
||||
sdl3_sys::events::SDL_EVENT_JOYSTICK_HAT_MOTION => {
|
||||
let state = unsafe { event.jhat.value };
|
||||
let hat = unsafe { event.jhat.hat };
|
||||
if dinput {
|
||||
if gather == 0 {
|
||||
if let Some(ui::config::InputItem::JoystickHat(joystick_hat)) =
|
||||
&profile.inputs[D_DPAD][0]
|
||||
&& joystick_hat.id == hat as i32
|
||||
&& joystick_hat.direction == state
|
||||
{
|
||||
move_down(&mut selected);
|
||||
} else if let Some(ui::config::InputItem::JoystickHat(joystick_hat)) =
|
||||
&profile.inputs[U_DPAD][0]
|
||||
&& joystick_hat.id == hat as i32
|
||||
&& joystick_hat.direction == state
|
||||
{
|
||||
move_up(&mut selected);
|
||||
} else if let Some(ui::config::InputItem::JoystickHat(joystick_hat)) =
|
||||
&profile.inputs[L_DPAD][0]
|
||||
&& joystick_hat.id == hat as i32
|
||||
&& joystick_hat.direction == state
|
||||
{
|
||||
move_left(&mut selected);
|
||||
} else if let Some(ui::config::InputItem::JoystickHat(joystick_hat)) =
|
||||
&profile.inputs[R_DPAD][0]
|
||||
&& joystick_hat.id == hat as i32
|
||||
&& joystick_hat.direction == state
|
||||
{
|
||||
move_right(&mut selected);
|
||||
} else if let Some(ui::config::InputItem::JoystickHat(joystick_hat)) =
|
||||
&profile.inputs[B_BUTTON][0]
|
||||
&& joystick_hat.id == hat as i32
|
||||
&& joystick_hat.direction == state
|
||||
{
|
||||
delete_binding(selected, profile);
|
||||
} else if let Some(ui::config::InputItem::JoystickHat(joystick_hat)) =
|
||||
&profile.inputs[A_BUTTON][0]
|
||||
&& joystick_hat.id == hat as i32
|
||||
&& joystick_hat.direction == state
|
||||
&& select_or_save(selected, &mut gather, &mut gather_timer)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
} else if state != sdl3_sys::joystick::SDL_HAT_CENTERED {
|
||||
profile.inputs[selected.0][selected.1] = Some(
|
||||
ui::config::InputItem::JoystickHat(ui::config::InputJoystickHat {
|
||||
id: hat as i32,
|
||||
direction: state,
|
||||
}),
|
||||
);
|
||||
(gather, gather_timer) =
|
||||
advance_cursor(&mut selected, &mut auto_advance);
|
||||
}
|
||||
}
|
||||
}
|
||||
sdl3_sys::events::SDL_EVENT_JOYSTICK_AXIS_MOTION => {
|
||||
let axis_value = unsafe { event.jaxis }.value;
|
||||
let axis = unsafe { event.jaxis }.axis;
|
||||
let mut initial_state = 0;
|
||||
let has_initial_state = unsafe {
|
||||
sdl3_sys::joystick::SDL_GetJoystickAxisInitialState(
|
||||
sdl3_sys::joystick::SDL_GetJoystickFromID(event.jaxis.which),
|
||||
axis as i32,
|
||||
&mut initial_state,
|
||||
)
|
||||
};
|
||||
initial_state = if has_initial_state {
|
||||
if initial_state < i16::MIN / 2 {
|
||||
i16::MIN
|
||||
} else if initial_state > i16::MAX / 2 {
|
||||
i16::MAX
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if dinput && gather != 0 && axis_value.abs_diff(initial_state) > (u16::MAX / 4)
|
||||
{
|
||||
profile.inputs[selected.0][selected.1] = Some(
|
||||
ui::config::InputItem::JoystickAxis(ui::config::InputControllerAxis {
|
||||
id: i32::from(axis),
|
||||
axis: axis_value.signum(),
|
||||
initial_state,
|
||||
}),
|
||||
);
|
||||
(gather, gather_timer) = advance_cursor(&mut selected, &mut auto_advance);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
sdl3_sys::everything::SDL_RenderClear(renderer);
|
||||
let ttf_text =
|
||||
sdl3_ttf_sys::ttf::TTF_CreateText(text_engine, font, std::ptr::null(), 0);
|
||||
render_screen(window, renderer, ttf_text, font, profile, selected, gather);
|
||||
sdl3_ttf_sys::ttf::TTF_DestroyText(ttf_text);
|
||||
sdl3_sys::render::SDL_RenderPresent(renderer);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn move_left(selected: &mut (usize, usize)) {
|
||||
selected.1 = selected.1.saturating_sub(1)
|
||||
}
|
||||
|
||||
fn move_right(selected: &mut (usize, usize)) {
|
||||
selected.1 = selected.1.saturating_add(1).min(1)
|
||||
}
|
||||
|
||||
fn move_up(selected: &mut (usize, usize)) {
|
||||
selected.0 = selected.0.saturating_sub(1)
|
||||
}
|
||||
|
||||
fn move_down(selected: &mut (usize, usize)) {
|
||||
selected.0 = selected.0.saturating_add(1).min(PROFILE_SIZE)
|
||||
}
|
||||
|
||||
fn delete_binding(selected: (usize, usize), profile: &mut ui::config::InputProfile) {
|
||||
if selected.0 < PROFILE_SIZE {
|
||||
profile.inputs[selected.0][selected.1] = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn select_or_save(
|
||||
selected: (usize, usize),
|
||||
gather: &mut usize,
|
||||
gather_timer: &mut Option<std::time::Instant>,
|
||||
) -> bool {
|
||||
if selected.0 == PROFILE_SIZE {
|
||||
return true;
|
||||
} else {
|
||||
*gather = GATHER_TIME;
|
||||
*gather_timer = Some(std::time::Instant::now());
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn render_screen(
|
||||
window: *mut sdl3_sys::video::SDL_Window,
|
||||
renderer: *mut sdl3_sys::render::SDL_Renderer,
|
||||
ttf_text: *mut sdl3_ttf_sys::ttf::TTF_Text,
|
||||
font: *mut sdl3_ttf_sys::ttf::TTF_Font,
|
||||
profile: &ui::config::InputProfile,
|
||||
selected: (usize, usize),
|
||||
gather: usize,
|
||||
) {
|
||||
let mut rect: sdl3_sys::rect::SDL_Rect = Default::default();
|
||||
unsafe { sdl3_sys::video::SDL_GetWindowSafeArea(window, &mut rect) };
|
||||
|
||||
let row_height = unsafe { sdl3_ttf_sys::ttf::TTF_GetFontLineSkip(font) } as usize;
|
||||
let key_labels: [&str; PROFILE_SIZE] = [
|
||||
"D Right",
|
||||
"D Left",
|
||||
"D Down",
|
||||
"D Up",
|
||||
"Start",
|
||||
"Z",
|
||||
"B",
|
||||
"A",
|
||||
"C Right",
|
||||
"C Left",
|
||||
"C Down",
|
||||
"C Up",
|
||||
"R",
|
||||
"L",
|
||||
"Control Stick Right",
|
||||
"Control Stick Left",
|
||||
"Control Stick Down",
|
||||
"Control Stick Up",
|
||||
"Hotkey Activator",
|
||||
];
|
||||
|
||||
// headers
|
||||
unsafe {
|
||||
sdl3_ttf_sys::ttf::TTF_SetFontStyle(
|
||||
font,
|
||||
sdl3_ttf_sys::ttf::TTF_STYLE_BOLD | sdl3_ttf_sys::ttf::TTF_STYLE_UNDERLINE,
|
||||
);
|
||||
let c_text = std::ffi::CString::new("Inputs").unwrap();
|
||||
sdl3_ttf_sys::ttf::TTF_SetTextString(ttf_text, c_text.as_ptr(), 0);
|
||||
sdl3_ttf_sys::ttf::TTF_DrawRendererText(ttf_text, rect.x as f32, rect.y as f32);
|
||||
|
||||
let c_text = std::ffi::CString::new("Binding 1").unwrap();
|
||||
sdl3_ttf_sys::ttf::TTF_SetTextString(ttf_text, c_text.as_ptr(), 0);
|
||||
sdl3_ttf_sys::ttf::TTF_DrawRendererText(
|
||||
ttf_text,
|
||||
rect.x as f32 + (rect.w / 3) as f32 * 1.0,
|
||||
rect.y as f32,
|
||||
);
|
||||
|
||||
let c_text = std::ffi::CString::new("Binding 2").unwrap();
|
||||
sdl3_ttf_sys::ttf::TTF_SetTextString(ttf_text, c_text.as_ptr(), 0);
|
||||
sdl3_ttf_sys::ttf::TTF_DrawRendererText(
|
||||
ttf_text,
|
||||
rect.x as f32 + (rect.w / 3) as f32 * 2.0,
|
||||
rect.y as f32,
|
||||
);
|
||||
sdl3_ttf_sys::ttf::TTF_SetFontStyle(font, sdl3_ttf_sys::ttf::TTF_STYLE_NORMAL);
|
||||
}
|
||||
|
||||
for (i, label) in key_labels.iter().enumerate() {
|
||||
let c_text = std::ffi::CString::new(*label).unwrap();
|
||||
unsafe {
|
||||
sdl3_ttf_sys::ttf::TTF_SetTextString(ttf_text, c_text.as_ptr(), 0);
|
||||
sdl3_ttf_sys::ttf::TTF_DrawRendererText(
|
||||
ttf_text,
|
||||
rect.x as f32,
|
||||
rect.y as f32 + ((i + 1) * row_height) as f32,
|
||||
);
|
||||
|
||||
let binding_1 =
|
||||
std::ffi::CString::new(get_binding_text(&profile.inputs[i][0])).unwrap();
|
||||
sdl3_ttf_sys::ttf::TTF_SetTextString(ttf_text, binding_1.as_ptr(), 0);
|
||||
draw_binding_text(renderer, ttf_text, rect, row_height, selected, gather, i, 0);
|
||||
|
||||
let binding_2 =
|
||||
std::ffi::CString::new(get_binding_text(&profile.inputs[i][1])).unwrap();
|
||||
sdl3_ttf_sys::ttf::TTF_SetTextString(ttf_text, binding_2.as_ptr(), 0);
|
||||
draw_binding_text(renderer, ttf_text, rect, row_height, selected, gather, i, 1);
|
||||
}
|
||||
}
|
||||
let c_text = std::ffi::CString::new("Save and Exit").unwrap();
|
||||
unsafe {
|
||||
sdl3_ttf_sys::ttf::TTF_SetFontStyle(font, sdl3_ttf_sys::ttf::TTF_STYLE_BOLD);
|
||||
sdl3_ttf_sys::ttf::TTF_SetTextString(ttf_text, c_text.as_ptr(), 0);
|
||||
let (mut text_w, mut text_h) = (0, 0);
|
||||
sdl3_ttf_sys::ttf::TTF_GetTextSize(ttf_text, &mut text_w, &mut text_h);
|
||||
|
||||
if selected.0 == PROFILE_SIZE {
|
||||
draw_highlight(
|
||||
renderer,
|
||||
rect.x as f32 + (rect.w - text_w) as f32 / 2.0,
|
||||
rect.y as f32 + ((PROFILE_SIZE + 1) * row_height) as f32,
|
||||
text_w,
|
||||
text_h,
|
||||
);
|
||||
}
|
||||
|
||||
sdl3_ttf_sys::ttf::TTF_DrawRendererText(
|
||||
ttf_text,
|
||||
rect.x as f32 + (rect.w - text_w) as f32 / 2.0,
|
||||
rect.y as f32 + ((PROFILE_SIZE + 1) * row_height) as f32,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
fn draw_highlight(
|
||||
renderer: *mut sdl3_sys::render::SDL_Renderer,
|
||||
x: f32,
|
||||
y: f32,
|
||||
text_w: i32,
|
||||
text_h: i32,
|
||||
) {
|
||||
unsafe {
|
||||
sdl3_sys::render::SDL_SetRenderDrawColor(renderer, 50, 100, 180, 255);
|
||||
|
||||
let highlight = sdl3_sys::rect::SDL_FRect {
|
||||
x,
|
||||
y,
|
||||
w: text_w as f32,
|
||||
h: text_h as f32,
|
||||
};
|
||||
sdl3_sys::render::SDL_RenderFillRect(renderer, &highlight);
|
||||
sdl3_sys::render::SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_binding_text(
|
||||
renderer: *mut sdl3_sys::render::SDL_Renderer,
|
||||
ttf_text: *mut sdl3_ttf_sys::ttf::TTF_Text,
|
||||
rect: sdl3_sys::rect::SDL_Rect,
|
||||
row_height: usize,
|
||||
selected: (usize, usize),
|
||||
gather: usize,
|
||||
i: usize,
|
||||
binding: usize,
|
||||
) {
|
||||
unsafe {
|
||||
if selected.0 == i && selected.1 == binding {
|
||||
if gather > 0 {
|
||||
let gather_text = std::ffi::CString::new(format!("{}...", gather)).unwrap();
|
||||
sdl3_ttf_sys::ttf::TTF_SetTextString(ttf_text, gather_text.as_ptr(), 0);
|
||||
}
|
||||
let (mut text_w, mut text_h) = (0, 0);
|
||||
sdl3_ttf_sys::ttf::TTF_GetTextSize(ttf_text, &mut text_w, &mut text_h);
|
||||
draw_highlight(
|
||||
renderer,
|
||||
rect.x as f32 + (rect.w / 3) as f32 * if selected.1 == 0 { 1.0 } else { 2.0 },
|
||||
rect.y as f32 + ((i + 1) * row_height) as f32,
|
||||
text_w,
|
||||
text_h,
|
||||
);
|
||||
}
|
||||
sdl3_ttf_sys::ttf::TTF_DrawRendererText(
|
||||
ttf_text,
|
||||
rect.x as f32 + (rect.w / 3) as f32 * (binding as f32 + 1.0),
|
||||
rect.y as f32 + ((i + 1) * row_height) as f32,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_binding_text(binding: &Option<ui::config::InputItem>) -> String {
|
||||
if let Some(binding) = binding {
|
||||
match binding {
|
||||
ui::config::InputItem::Key(key) => {
|
||||
return unsafe {
|
||||
std::ffi::CStr::from_ptr(sdl3_sys::keyboard::SDL_GetScancodeName(
|
||||
sdl3_sys::scancode::SDL_Scancode(key.id),
|
||||
))
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
};
|
||||
}
|
||||
ui::config::InputItem::ControllerButton(controller_button) => {
|
||||
return unsafe {
|
||||
std::ffi::CStr::from_ptr(sdl3_sys::gamepad::SDL_GetGamepadStringForButton(
|
||||
sdl3_sys::gamepad::SDL_GamepadButton(controller_button.id),
|
||||
))
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
};
|
||||
}
|
||||
ui::config::InputItem::ControllerAxis(controller_axis) => {
|
||||
return unsafe {
|
||||
let axis =
|
||||
std::ffi::CStr::from_ptr(sdl3_sys::gamepad::SDL_GetGamepadStringForAxis(
|
||||
sdl3_sys::gamepad::SDL_GamepadAxis(controller_axis.id),
|
||||
))
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let direction = if controller_axis.axis > 0 { "+" } else { "-" };
|
||||
format!("{} {}", axis, direction)
|
||||
};
|
||||
}
|
||||
ui::config::InputItem::JoystickButton(joystick_button) => {
|
||||
return format!("Joystick Button {}", joystick_button.id);
|
||||
}
|
||||
ui::config::InputItem::JoystickHat(joystick_hat) => {
|
||||
let direction = match joystick_hat.direction {
|
||||
1 => "Up",
|
||||
2 => "Right",
|
||||
4 => "Down",
|
||||
8 => "Left",
|
||||
_ => "Unknown",
|
||||
};
|
||||
return format!("Joystick Hat {} {}", joystick_hat.id, direction);
|
||||
}
|
||||
ui::config::InputItem::JoystickAxis(joystick_axis) => {
|
||||
let direction = if joystick_axis.axis > 0 { "+" } else { "-" };
|
||||
return format!("Joystick Axis {} {}", joystick_axis.id, direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
"(unset)".to_string()
|
||||
}
|
||||
|
||||
pub fn get_default_profile() -> ui::config::InputProfile {
|
||||
let mut inputs: [[Option<ui::config::InputItem>; 2]; PROFILE_SIZE] =
|
||||
[const { [None, None] }; PROFILE_SIZE];
|
||||
|
||||
inputs[R_DPAD] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_D),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerButton(
|
||||
ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_DPAD_RIGHT),
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[L_DPAD] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_A),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerButton(
|
||||
ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_DPAD_LEFT),
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[D_DPAD] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_S),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerButton(
|
||||
ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_DPAD_DOWN),
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[U_DPAD] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_W),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerButton(
|
||||
ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_DPAD_UP),
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[START_BUTTON] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_RETURN),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerButton(
|
||||
ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_START),
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[Z_TRIG] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_Z),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerAxis(
|
||||
ui::config::InputControllerAxis {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_AXIS_LEFT_TRIGGER),
|
||||
axis: 1,
|
||||
initial_state: 0,
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[B_BUTTON] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_LCTRL),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerButton(
|
||||
ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_WEST),
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[A_BUTTON] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_LSHIFT),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerButton(
|
||||
ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_SOUTH),
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[R_CBUTTON] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_L),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerAxis(
|
||||
ui::config::InputControllerAxis {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_AXIS_RIGHTX),
|
||||
axis: 1,
|
||||
initial_state: 0,
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[L_CBUTTON] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_J),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerAxis(
|
||||
ui::config::InputControllerAxis {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_AXIS_RIGHTX),
|
||||
axis: -1,
|
||||
initial_state: 0,
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[D_CBUTTON] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_K),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerAxis(
|
||||
ui::config::InputControllerAxis {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_AXIS_RIGHTY),
|
||||
axis: 1,
|
||||
initial_state: 0,
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[U_CBUTTON] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_I),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerAxis(
|
||||
ui::config::InputControllerAxis {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_AXIS_RIGHTY),
|
||||
axis: -1,
|
||||
initial_state: 0,
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[R_TRIG] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_C),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerButton(
|
||||
ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER),
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[L_TRIG] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_X),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerButton(
|
||||
ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_LEFT_SHOULDER),
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[AXIS_LEFT] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_LEFT),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerAxis(
|
||||
ui::config::InputControllerAxis {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_AXIS_LEFTX),
|
||||
axis: -1,
|
||||
initial_state: 0,
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[AXIS_RIGHT] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_RIGHT),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerAxis(
|
||||
ui::config::InputControllerAxis {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_AXIS_LEFTX),
|
||||
axis: 1,
|
||||
initial_state: 0,
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[AXIS_UP] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_UP),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerAxis(
|
||||
ui::config::InputControllerAxis {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_AXIS_LEFTY),
|
||||
axis: -1,
|
||||
initial_state: 0,
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[AXIS_DOWN] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_DOWN),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerAxis(
|
||||
ui::config::InputControllerAxis {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_AXIS_LEFTY),
|
||||
axis: 1,
|
||||
initial_state: 0,
|
||||
},
|
||||
)),
|
||||
];
|
||||
inputs[HOTKEY] = [
|
||||
Some(ui::config::InputItem::Key(ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_COMMA),
|
||||
})),
|
||||
Some(ui::config::InputItem::ControllerButton(
|
||||
ui::config::InputKeyButton {
|
||||
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_BACK),
|
||||
},
|
||||
)),
|
||||
];
|
||||
|
||||
ui::config::InputProfile {
|
||||
inputs,
|
||||
dinput: false,
|
||||
deadzone: DEADZONE_DEFAULT,
|
||||
}
|
||||
}
|
||||
+744
-905
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ pub fn ra_window(app: &ui::gui::AppWindow) {
|
||||
}
|
||||
|
||||
if !cfg!(ra_hardcore_enabled) {
|
||||
app.set_ra_softcore_only(true);
|
||||
app.set_ra_casual_only(true);
|
||||
}
|
||||
|
||||
if app.get_ra_enabled() && !app.get_ra_username().is_empty() {
|
||||
|
||||
+7
-20
@@ -13,6 +13,7 @@ pub enum SaveTypes {
|
||||
Romsave,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Paths {
|
||||
pub eep_file_path: std::path::PathBuf,
|
||||
pub sra_file_path: std::path::PathBuf,
|
||||
@@ -25,19 +26,19 @@ pub struct Paths {
|
||||
|
||||
// the bool indicates whether the save has been written to
|
||||
// if that is the case, it will be flushed to the disk when the program closes
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Save {
|
||||
pub data: Vec<u8>,
|
||||
pub write_pending: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RomSave {
|
||||
pub data: std::collections::HashMap<u32, u8>,
|
||||
pub data: rustc_hash::FxHashMap<u32, u8>,
|
||||
pub write_pending: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Saves {
|
||||
pub eeprom: Save,
|
||||
pub sram: Save,
|
||||
@@ -242,25 +243,21 @@ pub fn load_saves(ui: &mut ui::Ui, netplay: &mut Option<netplay::Netplay>) {
|
||||
netplay.as_mut().unwrap(),
|
||||
"eep",
|
||||
&ui.storage.saves.eeprom.data,
|
||||
ui.storage.saves.eeprom.data.len(),
|
||||
);
|
||||
netplay::send_save(
|
||||
netplay.as_mut().unwrap(),
|
||||
"sra",
|
||||
&ui.storage.saves.sram.data,
|
||||
ui.storage.saves.sram.data.len(),
|
||||
);
|
||||
netplay::send_save(
|
||||
netplay.as_mut().unwrap(),
|
||||
"fla",
|
||||
&ui.storage.saves.flash.data,
|
||||
ui.storage.saves.flash.data.len(),
|
||||
);
|
||||
netplay::send_save(
|
||||
netplay.as_mut().unwrap(),
|
||||
"mpk",
|
||||
&ui.storage.saves.mempak.data,
|
||||
ui.storage.saves.mempak.data.len(),
|
||||
);
|
||||
|
||||
let mut compressed_sd = Vec::new();
|
||||
@@ -269,12 +266,7 @@ pub fn load_saves(ui: &mut ui::Ui, netplay: &mut Option<netplay::Netplay>) {
|
||||
{
|
||||
compressed_sd = file_data;
|
||||
}
|
||||
netplay::send_save(
|
||||
netplay.as_mut().unwrap(),
|
||||
"img",
|
||||
&compressed_sd,
|
||||
compressed_sd.len(),
|
||||
);
|
||||
netplay::send_save(netplay.as_mut().unwrap(), "img", &compressed_sd);
|
||||
|
||||
let mut compressed_romsave = Vec::new();
|
||||
if !ui.storage.saves.romsave.data.is_empty()
|
||||
@@ -283,12 +275,7 @@ pub fn load_saves(ui: &mut ui::Ui, netplay: &mut Option<netplay::Netplay>) {
|
||||
{
|
||||
compressed_romsave = compressed_file_data;
|
||||
}
|
||||
netplay::send_save(
|
||||
netplay.as_mut().unwrap(),
|
||||
"rom",
|
||||
&compressed_romsave,
|
||||
compressed_romsave.len(),
|
||||
);
|
||||
netplay::send_save(netplay.as_mut().unwrap(), "rom", &compressed_romsave);
|
||||
} else {
|
||||
netplay::receive_save(
|
||||
netplay.as_mut().unwrap(),
|
||||
|
||||
+87
-86
@@ -11,7 +11,7 @@ pub struct UsbData {
|
||||
pub data_size: u32,
|
||||
}
|
||||
|
||||
fn respond_to_handshake(usb_tx: &tokio::sync::broadcast::Sender<UsbData>, data: Vec<u8>) {
|
||||
fn respond_to_handshake(usb_tx: &tokio::sync::mpsc::UnboundedSender<UsbData>, data: Vec<u8>) {
|
||||
if let Ok(data) = String::from_utf8(data)
|
||||
&& data == "N64"
|
||||
{
|
||||
@@ -29,98 +29,98 @@ fn respond_to_handshake(usb_tx: &tokio::sync::broadcast::Sender<UsbData>, data:
|
||||
async fn handle_connection(
|
||||
conn: tokio::net::TcpStream,
|
||||
mut shutdown_rx: tokio::sync::watch::Receiver<()>,
|
||||
mut usb_rx: tokio::sync::broadcast::Receiver<UsbData>,
|
||||
usb_tx: tokio::sync::broadcast::Sender<UsbData>,
|
||||
cart_tx: tokio::sync::broadcast::Sender<UsbData>,
|
||||
usb_rx: &mut tokio::sync::mpsc::UnboundedReceiver<UsbData>,
|
||||
usb_tx: tokio::sync::mpsc::UnboundedSender<UsbData>,
|
||||
cart_tx: tokio::sync::mpsc::UnboundedSender<UsbData>,
|
||||
) {
|
||||
let (mut incoming, mut outgoing) = conn.into_split();
|
||||
|
||||
let mut shutdown_rx_clone = shutdown_rx.clone();
|
||||
|
||||
while usb_rx.try_recv().is_ok() {} // drain stale USB messages
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut incoming_buffer = vec![0u8; 4096];
|
||||
let mut data_type: Option<u32> = None;
|
||||
let mut data_size: Option<u32> = None;
|
||||
let mut usb_buffer: Vec<u8> = vec![];
|
||||
loop {
|
||||
tokio::select! {
|
||||
data = usb_rx.recv() => {
|
||||
match data {
|
||||
Ok(data) => {
|
||||
let mut output: Vec<u8> = vec![];
|
||||
output.extend_from_slice(&data.data_type.to_be_bytes());
|
||||
output.extend_from_slice(&data.data_size.to_be_bytes());
|
||||
output.extend_from_slice(&data.data);
|
||||
if outgoing.write_all(&output).await.is_err() {
|
||||
break;
|
||||
result = incoming.read(&mut incoming_buffer) => {
|
||||
match result {
|
||||
Ok(0) => {
|
||||
break;
|
||||
}
|
||||
Ok(n) => {
|
||||
usb_buffer.extend_from_slice(&incoming_buffer[0..n]);
|
||||
if data_type.is_none() {
|
||||
if usb_buffer.len() < 4 {
|
||||
continue;
|
||||
} else {
|
||||
data_type = Some(u32::from_be_bytes(usb_buffer[0..4].try_into().unwrap()));
|
||||
usb_buffer.drain(0..4);
|
||||
}
|
||||
}
|
||||
if data_type.is_some() && data_size.is_none() {
|
||||
if usb_buffer.len() < 4 {
|
||||
continue;
|
||||
} else {
|
||||
data_size = Some(u32::from_be_bytes(usb_buffer[0..4].try_into().unwrap()));
|
||||
usb_buffer.drain(0..4);
|
||||
}
|
||||
}
|
||||
if let Some(d_type) = data_type && let Some(d_size) = data_size {
|
||||
let length = d_size as usize;
|
||||
if usb_buffer.len() >= length {
|
||||
let usb_data = UsbData {
|
||||
data: usb_buffer[0..length].to_vec(),
|
||||
data_type: d_type,
|
||||
data_size: d_size,
|
||||
};
|
||||
usb_buffer.drain(0..length);
|
||||
if usb_data.data_type == DATATYPE_TCPTEST {
|
||||
respond_to_handshake(&usb_tx,usb_data.data);
|
||||
} else if usb_data.data_type == DATATYPE_ROMUPLOAD {
|
||||
panic!("ROM upload not supported");
|
||||
} else {
|
||||
cart_tx.send(usb_data).unwrap();
|
||||
}
|
||||
data_type = None;
|
||||
data_size = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
|
||||
panic!("usb_rx lagged");
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
||||
break;
|
||||
Err(_e) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = shutdown_rx_clone.changed() => {
|
||||
_ = shutdown_rx.changed() => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut incoming_buffer = vec![0u8; 4096];
|
||||
let mut data_type: Option<u32> = None;
|
||||
let mut data_size: Option<u32> = None;
|
||||
let mut usb_buffer: Vec<u8> = vec![];
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = incoming.read(&mut incoming_buffer) => {
|
||||
match result {
|
||||
Ok(0) => {
|
||||
break;
|
||||
}
|
||||
Ok(n) => {
|
||||
usb_buffer.extend_from_slice(&incoming_buffer[0..n]);
|
||||
if data_type.is_none() {
|
||||
if usb_buffer.len() < 4 {
|
||||
continue;
|
||||
} else {
|
||||
data_type = Some(u32::from_be_bytes(usb_buffer[0..4].try_into().unwrap()));
|
||||
usb_buffer.drain(0..4);
|
||||
}
|
||||
}
|
||||
if data_type.is_some() && data_size.is_none() {
|
||||
if usb_buffer.len() < 4 {
|
||||
continue;
|
||||
} else {
|
||||
data_size = Some(u32::from_be_bytes(usb_buffer[0..4].try_into().unwrap()));
|
||||
usb_buffer.drain(0..4);
|
||||
}
|
||||
}
|
||||
if let Some(d_type) = data_type && let Some(d_size) = data_size {
|
||||
let length = d_size as usize;
|
||||
if usb_buffer.len() >= length {
|
||||
let usb_data = UsbData {
|
||||
data: usb_buffer[0..length].to_vec(),
|
||||
data_type: d_type,
|
||||
data_size: d_size,
|
||||
};
|
||||
usb_buffer.drain(0..length);
|
||||
if usb_data.data_type == DATATYPE_TCPTEST {
|
||||
respond_to_handshake(&usb_tx,usb_data.data);
|
||||
} else if usb_data.data_type == DATATYPE_ROMUPLOAD {
|
||||
panic!("ROM upload not supported");
|
||||
} else {
|
||||
cart_tx.send(usb_data).unwrap();
|
||||
}
|
||||
data_type = None;
|
||||
data_size = None;
|
||||
}
|
||||
data = usb_rx.recv() => {
|
||||
match data {
|
||||
Some(data) => {
|
||||
let mut output: Vec<u8> = vec![];
|
||||
output.extend_from_slice(&data.data_type.to_be_bytes());
|
||||
output.extend_from_slice(&data.data_size.to_be_bytes());
|
||||
output.extend_from_slice(&data.data);
|
||||
if outgoing.write_all(&output).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_e) => {
|
||||
break;
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = shutdown_rx.changed() => {
|
||||
_ = shutdown_rx_clone.changed() => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -133,24 +133,23 @@ pub fn init() -> (
|
||||
ui::Usb,
|
||||
) {
|
||||
let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(());
|
||||
let (usb_tx, usb_rx): (
|
||||
tokio::sync::broadcast::Sender<UsbData>,
|
||||
tokio::sync::broadcast::Receiver<UsbData>,
|
||||
) = tokio::sync::broadcast::channel(1024);
|
||||
let (usb_tx, mut usb_rx): (
|
||||
tokio::sync::mpsc::UnboundedSender<UsbData>,
|
||||
tokio::sync::mpsc::UnboundedReceiver<UsbData>,
|
||||
) = tokio::sync::mpsc::unbounded_channel();
|
||||
let (cart_tx, cart_rx): (
|
||||
tokio::sync::broadcast::Sender<UsbData>,
|
||||
tokio::sync::broadcast::Receiver<UsbData>,
|
||||
) = tokio::sync::broadcast::channel(1024);
|
||||
tokio::sync::mpsc::UnboundedSender<UsbData>,
|
||||
tokio::sync::mpsc::UnboundedReceiver<UsbData>,
|
||||
) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
let usb_tx_clone = usb_tx.clone();
|
||||
let cart_rx_clone = cart_rx.resubscribe();
|
||||
let handle = tokio::spawn(async move {
|
||||
if let Ok(listener) = tokio::net::TcpListener::bind("localhost:48646").await {
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = listener.accept() => {
|
||||
if let Ok((c,_)) = res {
|
||||
handle_connection(c,shutdown_rx.clone(),usb_rx.resubscribe(),usb_tx.clone(),cart_tx.clone()).await;
|
||||
handle_connection(c,shutdown_rx.clone(),&mut usb_rx,usb_tx.clone(),cart_tx.clone()).await;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -169,12 +168,12 @@ pub fn init() -> (
|
||||
Some(handle),
|
||||
ui::Usb {
|
||||
usb_tx: Some(usb_tx_clone),
|
||||
cart_rx: Some(cart_rx_clone),
|
||||
cart_rx: Some(cart_rx),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn close(
|
||||
pub fn close(
|
||||
shutdown_tx: Option<tokio::sync::watch::Sender<()>>,
|
||||
usb_handle: Option<tokio::task::JoinHandle<()>>,
|
||||
) {
|
||||
@@ -182,15 +181,17 @@ pub async fn close(
|
||||
&& let Some(shutdown_tx) = shutdown_tx
|
||||
{
|
||||
let _ = shutdown_tx.send(());
|
||||
if tokio::time::timeout(std::time::Duration::from_secs(1), &mut handle)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
handle.abort();
|
||||
}
|
||||
tokio::task::spawn(async move {
|
||||
if tokio::time::timeout(std::time::Duration::from_secs(1), &mut handle)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
handle.abort();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_to_usb(usb_tx: &tokio::sync::broadcast::Sender<UsbData>, buffer: UsbData) {
|
||||
pub fn send_to_usb(usb_tx: &tokio::sync::mpsc::UnboundedSender<UsbData>, buffer: UsbData) {
|
||||
usb_tx.send(buffer).unwrap();
|
||||
}
|
||||
|
||||
+32
-48
@@ -1,7 +1,7 @@
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_camel_case_types)]
|
||||
include!(concat!(env!("OUT_DIR"), "/parallel_bindings.rs"));
|
||||
use crate::{device, retroachievements, ui};
|
||||
use crate::{device, netplay, retroachievements, ui};
|
||||
|
||||
const PAL_WIDESCREEN_WIDTH: i32 = 512;
|
||||
const PAL_STANDARD_WIDTH: i32 = 384;
|
||||
@@ -10,7 +10,7 @@ const NTSC_WIDESCREEN_WIDTH: i32 = 426;
|
||||
const NTSC_STANDARD_WIDTH: i32 = 320;
|
||||
const NTSC_HEIGHT: i32 = 240;
|
||||
|
||||
fn build_gfx_info(device: &mut device::Device) -> GFX_INFO {
|
||||
fn build_gfx_info(device: &mut device::Device, netplay: bool) -> GFX_INFO {
|
||||
GFX_INFO {
|
||||
RDRAM: device.rdram.mem.as_mut_ptr(),
|
||||
DMEM: device.rsp.mem.as_mut_ptr(),
|
||||
@@ -20,9 +20,9 @@ fn build_gfx_info(device: &mut device::Device) -> GFX_INFO {
|
||||
DPC_END_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_END_REG],
|
||||
DPC_STATUS_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_STATUS_REG],
|
||||
PAL: device.cart.pal,
|
||||
widescreen: device.ui.config.video.widescreen,
|
||||
fullscreen: device.ui.video.fullscreen,
|
||||
vsync: if device.netplay.is_none() {
|
||||
widescreen: device.ui.video.widescreen,
|
||||
vsync: if !netplay {
|
||||
device.ui.config.video.vsync
|
||||
} else {
|
||||
false
|
||||
@@ -34,7 +34,7 @@ fn build_gfx_info(device: &mut device::Device) -> GFX_INFO {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
pub fn init(device: &mut device::Device, netplay: bool) {
|
||||
ui::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
|
||||
ui::ttf_init();
|
||||
|
||||
@@ -56,14 +56,14 @@ pub fn init(device: &mut device::Device) {
|
||||
2
|
||||
};
|
||||
if device.cart.pal {
|
||||
window_width = if device.ui.config.video.widescreen {
|
||||
window_width = if device.ui.video.widescreen {
|
||||
PAL_WIDESCREEN_WIDTH * scale
|
||||
} else {
|
||||
PAL_STANDARD_WIDTH * scale
|
||||
};
|
||||
window_height = PAL_HEIGHT * scale;
|
||||
} else {
|
||||
window_width = if device.ui.config.video.widescreen {
|
||||
window_width = if device.ui.video.widescreen {
|
||||
NTSC_WIDESCREEN_WIDTH * scale
|
||||
} else {
|
||||
NTSC_STANDARD_WIDTH * scale
|
||||
@@ -99,7 +99,7 @@ pub fn init(device: &mut device::Device) {
|
||||
sdl3_sys::everything::SDL_HideCursor();
|
||||
}
|
||||
|
||||
let gfx_info = build_gfx_info(device);
|
||||
let gfx_info = build_gfx_info(device, netplay);
|
||||
|
||||
unsafe {
|
||||
let font_bytes = include_bytes!("../../data/ui/RobotoMono-Regular.ttf");
|
||||
@@ -165,8 +165,12 @@ pub fn save_state(rdp_state: *mut u8) {
|
||||
unsafe { rdp_save_state(rdp_state) }
|
||||
}
|
||||
|
||||
pub fn idle() {
|
||||
unsafe { rdp_idle() }
|
||||
}
|
||||
|
||||
pub fn load_state(device: &mut device::Device, rdp_state: *const u8) {
|
||||
let gfx_info = build_gfx_info(device);
|
||||
let gfx_info = build_gfx_info(device, device.netplay.is_some());
|
||||
unsafe {
|
||||
rdp_load_state(gfx_info, rdp_state);
|
||||
for reg in 0..device::vi::VI_REGS_COUNT {
|
||||
@@ -175,12 +179,13 @@ pub fn load_state(device: &mut device::Device, rdp_state: *const u8) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause_loop(frame_time: f64) {
|
||||
pub fn pause_loop(ui: &mut ui::Ui, frame_time: f64) {
|
||||
let mut paused = true;
|
||||
let mut frame_advance = false;
|
||||
while paused && !frame_advance {
|
||||
std::thread::sleep(std::time::Duration::from_secs_f64(frame_time));
|
||||
unsafe { sdl3_sys::events::SDL_PumpEvents() };
|
||||
ui::input::get(ui, 0); // to gather hotkey input
|
||||
retroachievements::do_idle();
|
||||
let callback = unsafe { rdp_check_callback() };
|
||||
paused = callback.paused;
|
||||
@@ -194,9 +199,12 @@ pub fn check_callback(device: &mut device::Device) -> (bool, bool) {
|
||||
device.cpu.running = callback.emu_running;
|
||||
if device.netplay.is_none() {
|
||||
if callback.save_state {
|
||||
device.save_state = true;
|
||||
device.savestate.save_state = true;
|
||||
} else if callback.load_state {
|
||||
device.load_state = true;
|
||||
device.savestate.load_state = true;
|
||||
}
|
||||
if callback.load_rewind && device.ui.config.emulation.rewind {
|
||||
device.savestate.load_rewind = true;
|
||||
}
|
||||
if callback.reset_game {
|
||||
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] |= device::cop0::COP0_CAUSE_IP4;
|
||||
@@ -209,9 +217,9 @@ pub fn check_callback(device: &mut device::Device) -> (bool, bool) {
|
||||
device.cpu.clock_rate, // 1 second
|
||||
);
|
||||
}
|
||||
if device.vi.enable_speed_limiter != callback.enable_speedlimiter {
|
||||
if device.speed_limiter.enabled != callback.enable_speedlimiter {
|
||||
speed_limiter_toggled = true;
|
||||
device.vi.enable_speed_limiter = callback.enable_speedlimiter;
|
||||
device.speed_limiter.enabled = callback.enable_speedlimiter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,6 +241,16 @@ pub fn check_callback(device: &mut device::Device) -> (bool, bool) {
|
||||
} else if callback.raise_volume {
|
||||
ui::audio::raise_audio_volume(&mut device.ui);
|
||||
}
|
||||
|
||||
if let Some(netplay) = &mut device.netplay
|
||||
&& netplay.player_number == 0
|
||||
{
|
||||
if callback.decrease_input_delay {
|
||||
netplay::send_input_delay(netplay, netplay.input_delay - 1);
|
||||
} else if callback.increase_input_delay {
|
||||
netplay::send_input_delay(netplay, netplay.input_delay + 1);
|
||||
}
|
||||
}
|
||||
(speed_limiter_toggled, callback.paused)
|
||||
}
|
||||
|
||||
@@ -256,37 +274,3 @@ pub fn onscreen_message(message: &str, milliseconds: MESSAGE_LENGTH) {
|
||||
rdp_onscreen_message(c_message.as_ptr(), milliseconds)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn draw_text(
|
||||
text: &str,
|
||||
renderer: *mut sdl3_sys::render::SDL_Renderer,
|
||||
image_texture: *mut sdl3_sys::render::SDL_Texture,
|
||||
text_engine: *mut sdl3_ttf_sys::ttf::TTF_TextEngine,
|
||||
font: *mut sdl3_ttf_sys::ttf::TTF_Font,
|
||||
) {
|
||||
unsafe {
|
||||
let (mut w, mut h) = (0, 0);
|
||||
sdl3_sys::render::SDL_GetRenderOutputSize(renderer, &mut w, &mut h);
|
||||
|
||||
let c_text = std::ffi::CString::new(text).unwrap();
|
||||
let ttf_text = sdl3_ttf_sys::ttf::TTF_CreateText(text_engine, font, c_text.as_ptr(), 0);
|
||||
|
||||
sdl3_sys::everything::SDL_RenderClear(renderer);
|
||||
let rect = sdl3_sys::rect::SDL_FRect {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
w: w as f32,
|
||||
h: (h - sdl3_ttf_sys::ttf::TTF_GetFontHeight(font)) as f32,
|
||||
};
|
||||
sdl3_sys::render::SDL_RenderTexture(renderer, image_texture, std::ptr::null(), &rect);
|
||||
let (mut text_w, mut text_h) = (0, 0);
|
||||
sdl3_ttf_sys::ttf::TTF_GetTextSize(ttf_text, &mut text_w, &mut text_h);
|
||||
sdl3_ttf_sys::ttf::TTF_DrawRendererText(
|
||||
ttf_text,
|
||||
(w / 2 - text_w / 2) as f32,
|
||||
(h - text_h) as f32,
|
||||
);
|
||||
sdl3_sys::render::SDL_RenderPresent(renderer);
|
||||
sdl3_ttf_sys::ttf::TTF_DestroyText(ttf_text);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -7,13 +7,13 @@ pub fn prompt_for_match(words: &[String], frame_time: f64) -> u16 {
|
||||
dedup_words.sort();
|
||||
dedup_words.dedup();
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
let vru_dialog = gui::VruDialog::new().unwrap();
|
||||
let vru_dialog_weak = vru_dialog.as_weak();
|
||||
|
||||
vru_dialog.on_vru_button_clicked(move |chosen_word| {
|
||||
tx.try_send(chosen_word.to_string()).unwrap();
|
||||
tx.send(chosen_word.to_string()).unwrap();
|
||||
vru_dialog_weak.unwrap().hide().unwrap();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user