Compare commits

..

82 Commits

Author SHA1 Message Date
Logan McNaughton 81cc78ce90 bump crate and rust versions (#1155) 2026-07-09 13:06:49 +00:00
PolygonPrincess e8a7abffe3 Add cli flag for widescreen (#1152)
* Add cli flag for widescreen and enable config fallback

* Fix else statement brace spacing and trailing whitespace

* Fix trailing whitespace within args.widescreen else statement
2026-07-06 12:54:51 +00:00
dependabot[bot] f7ad6a4b0d Bump rust-toolchain from 1.96.0 to 1.96.1 (#1151)
Bumps [rust-toolchain](https://github.com/rust-lang/rust) from 1.96.0 to 1.96.1.
- [Release notes](https://github.com/rust-lang/rust/releases)
- [Changelog](https://github.com/rust-lang/rust/blob/main/RELEASES.md)
- [Commits](https://github.com/rust-lang/rust/compare/1.96.0...1.96.1)

---
updated-dependencies:
- dependency-name: rust-toolchain
  dependency-version: 1.96.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 04:34:17 +00:00
Logan McNaughton 1bfb50de8a re-arrange input profiles (#1150) 2026-07-05 10:43:23 +00:00
Logan McNaughton 3bc7c96191 move input polling (#1145)
* move input polling

* more

* more

* more
2026-07-02 08:04:51 +00:00
Logan McNaughton 032bf1acf3 Update contribution guidelines (#1143) 2026-07-01 07:58:17 +00:00
Logan McNaughton 8bc765c1a3 match RA branding softcore->casual (#1142) 2026-07-01 07:34:48 +00:00
Logan McNaughton da061240af normalize no-intro names (#1139) 2026-06-30 10:32:43 +00:00
Logan McNaughton 407a1b967c Re-arrange settings (#1138)
* update setting locations

* tra
2026-06-30 09:53:03 +00:00
999sian ad756cd6bd settings: add a dark/light theme toggle (#1132)
* settings: add dark/light theme toggle

* i18n: translate theme selector strings

* settings: align Resolution and Theme dropdowns in a GridBox
2026-06-30 09:17:12 +00:00
Logan McNaughton 6e11bc18d6 translation tweaks (#1137) 2026-06-30 05:24:48 +00:00
Logan McNaughton c8eee9f727 don't copy from db (#1136) 2026-06-30 05:23:38 +00:00
Logan McNaughton 01a53ce1c0 use no-intro names in netplay/cheats (#1133)
* use no-intro names in netplay/cheats

* more

* more
2026-06-29 19:37:33 +00:00
Logan McNaughton 0eddf31f92 update contributing guidelines (#1130) 2026-06-29 09:53:00 +00:00
Logan McNaughton e257ba717b update ggrs (#1127) 2026-06-27 08:19:14 +00:00
Logan McNaughton 7a797165fd Fix translation for 'Open Saves Folder' in Portuguese (#1125) 2026-06-26 07:23:50 +00:00
Logan McNaughton a659413090 bump ver (#1124) 2026-06-25 13:09:55 +00:00
Logan McNaughton 08e7ec71d9 clippy fix (#1123)
* clippy fix

* more
2026-06-25 13:09:18 +00:00
Logan McNaughton c97f14980b show proper number of players (#1121) 2026-06-25 12:28:09 +00:00
Logan McNaughton 5d090f496b update ggrs (#1119) 2026-06-25 06:41:33 +00:00
Logan McNaughton d6b46037b4 update netplay connection (#1118) 2026-06-24 20:44:38 +00:00
Logan McNaughton 5b5acfdd25 update slint (#1117)
* update slint

* more
2026-06-24 15:04:14 +00:00
Logan McNaughton 926338ba62 fix desync issue (#1116)
* fix desync issue

* more

* more

* more

* more
2026-06-24 12:52:56 +00:00
Logan McNaughton cc420013f9 add sleep while waiting for state (#1115)
* add sleep while waiting for state

* more

* more
2026-06-23 16:37:14 +00:00
Logan McNaughton ba0a21feca register tx early (#1114) 2026-06-23 14:52:27 +00:00
Logan McNaughton 70b9d20c64 fix android build (#1113)
* fix android build

* more

* more
2026-06-23 13:57:52 +00:00
Logan McNaughton dee2b85daa use tokio mutex (#1112)
* use tokio mutex

* more
2026-06-23 10:27:15 +00:00
Logan McNaughton 119e9044a8 cargo update (#1111) 2026-06-23 05:35:05 +00:00
dependabot[bot] e6a260bd6a Bump actions/checkout from 6 to 7 (#1108)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 05:00:57 +00:00
Logan McNaughton a23ede433b bump version (#1107) 2026-06-21 19:13:16 +00:00
Logan McNaughton 346b0a139d Clarify netplay support (#1106)
Updated netplay section to clarify support for P2P connections and added a note to read the netplay guide.
2026-06-21 15:12:19 +00:00
Logan McNaughton f5cda2901f clear join sessions list (#1105) 2026-06-21 14:22:21 +00:00
Logan McNaughton 2dda5c8706 handle matchbox panics (#1104) 2026-06-21 08:51:32 +00:00
Logan McNaughton d6aa2ddce3 send disconnect notice (#1103)
* send disconnect notice

* more

* more

* more

* more
2026-06-20 16:40:56 +00:00
Logan McNaughton 6f9801a185 cargo update (#1102) 2026-06-20 11:20:11 +00:00
Logan McNaughton c82edae338 show message more frequently (#1101)
* show message more frequently

* more
2026-06-20 07:58:25 +00:00
Logan McNaughton 57327c1f73 improve netplay startup (#1100) 2026-06-19 21:06:05 +00:00
Logan McNaughton e3a22bf7d0 tweak connection times (#1099)
* tweak connection times

* more

* more
2026-06-19 07:26:39 +00:00
Logan McNaughton 3718d914fd try to restore input data from old config (#1097) 2026-06-17 06:58:44 +00:00
Logan McNaughton 97a8b63c9c fix disconnect (#1096) 2026-06-16 16:46:57 +00:00
Logan McNaughton d6d0b66078 bump version (#1095) 2026-06-16 16:41:13 +00:00
Logan McNaughton b173598320 reset timeout (#1094) 2026-06-16 14:45:04 +00:00
Logan McNaughton f211eede3b retry socket creation (#1093)
* retry sockey creation

* more

* more

* more

* more
2026-06-16 14:22:53 +00:00
Logan McNaughton b123ed9fa3 input delay cannot be less than 1 (#1092) 2026-06-16 14:02:02 +00:00
Logan McNaughton 2b0d8e5423 skip save states (#1091)
* skip save states

* more
2026-06-16 13:34:18 +00:00
Logan McNaughton 49b8024fbe Revert translation changes (#1090)
* revert translation changes

* more
2026-06-16 13:05:58 +00:00
Logan McNaughton be987e1434 don't spam errors (#1089) 2026-06-16 12:58:16 +00:00
Logan McNaughton 27e2b89fcf re-arrange netplay setup (#1088)
* re-arrange netplay setup

* more

* more

* more

* more

* more

* more

* more

* more
2026-06-16 12:48:31 +00:00
Logan McNaughton c261a746a3 enable rollback (#1087)
* enable rollback

* more
2026-06-16 10:43:50 +00:00
Logan McNaughton c290e60af1 put tokio runtime in dedicated thread (#1086)
* put tokio runtime in dedicated thread

* more

* more

* more

* more
2026-06-16 09:33:27 +00:00
Logan McNaughton 19142a30c6 use unbounded channel (#1084)
* use unbounded channel

* more
2026-06-15 17:44:20 +00:00
Logan McNaughton 2ca317d742 put rec back (#1083) 2026-06-15 15:56:53 +00:00
Logan McNaughton 45963aeea5 Fix ping recommendation (#1082)
* don't block tokio

* more

* more

* more

* more

* more
2026-06-15 15:29:51 +00:00
Logan McNaughton 4929f2cc51 remove highest ping (#1081) 2026-06-15 13:02:29 +00:00
Logan McNaughton 90e51f5f09 setup callbacks at start (#1080)
* setup callbacks at start

* more

* more
2026-06-15 09:44:33 +00:00
Logan McNaughton 9b7aeccd48 close window on user request (#1079)
* close window on user request

* more

* more

* more
2026-06-15 06:53:54 +00:00
Logan McNaughton 10d55bd25c move netplay init (#1078)
* move netplay init

* more
2026-06-14 20:36:54 +00:00
Logan McNaughton 16224cd5a6 use spinbox for latency (#1077)
* use spinbox for latency

* more

* more
2026-06-14 18:28:36 +00:00
Logan McNaughton d62d4c852a Increase timeout duration from 2 to 3 seconds (#1075) 2026-06-14 10:50:35 +00:00
Logan McNaughton 6d6349c79d Increase timeout duration from 2 to 3 seconds (#1074) 2026-06-14 08:27:11 +00:00
Logan McNaughton 9d2eec1732 bump version (#1073) 2026-06-14 06:53:05 +00:00
Logan McNaughton d6da1656b8 clean up ICE config (#1072) 2026-06-13 17:29:36 +00:00
Logan McNaughton ae7bac65d5 use custom ICE server config (#1071)
* use custom ICE server config

* more

* more

* more

* more

* more

* more
2026-06-13 16:28:33 +00:00
Logan McNaughton 99c4d6dff8 average top 3 values (#1070) 2026-06-13 13:07:10 +00:00
Logan McNaughton 74600b4b25 pump sdl events before callbacks (#1069) 2026-06-13 13:00:12 +00:00
Logan McNaughton 6cae2ed3ac dont use usize in netplay messages (#1068) 2026-06-13 09:55:12 +00:00
Logan McNaughton 611b56186d update recommendation formula (#1067)
* update recommendation formula

* more
2026-06-13 09:14:43 +00:00
Logan McNaughton dfa673e6e9 add note about input delay (#1066)
* add note about input delay

* more
2026-06-13 08:00:29 +00:00
Logan McNaughton be74e3358a Use mpsc instead of mutex (#1065)
* use mpsc

* more

* more

* more
2026-06-12 14:16:06 +00:00
Logan McNaughton 4cc85d595f pass channel to ggrs (#1064)
* pass channel to ggrs

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-06-12 13:34:21 +00:00
Logan McNaughton b2d7a12033 wait for mesh (#1063)
* wait for mesh

* more

* more
2026-06-12 07:02:53 +00:00
Logan McNaughton 65cffbba0c cap recommendation (#1062) 2026-06-12 05:36:56 +00:00
Logan McNaughton 5f8e3b9760 run netplay_close on window close (#1061)
* run netplay_close on window close

* more
2026-06-11 19:16:06 +00:00
Logan McNaughton 397a15349c handle close messages (#1060) 2026-06-11 12:53:00 +00:00
Logan McNaughton 235a4af0a9 check number of peers (#1059) 2026-06-11 07:38:05 +00:00
Logan McNaughton b171d48f87 print errors (#1058) 2026-06-11 05:36:00 +00:00
Logan McNaughton bfeba94e20 show error (#1057) 2026-06-11 05:29:28 +00:00
Logan McNaughton aa0bde0999 use tls-vendored (#1056) 2026-06-11 05:16:13 +00:00
Logan McNaughton 72e4a6b94a early rollback code (#1054)
* some rollback test

* more
2026-06-10 15:09:07 +00:00
Logan McNaughton 88f00f5638 clean up if statement (#1053) 2026-06-10 12:07:10 +00:00
Logan McNaughton db21800d9f move rewind check (#1051)
* move rewind check

* more
2026-06-10 10:54:39 +00:00
Logan McNaughton 4a63f469bf update VI on V_START (#1050)
* update VI on V_START

* more

* more

* more

* more
2026-06-10 08:55:51 +00:00
51 changed files with 26000 additions and 2933 deletions
+4 -4
View File
@@ -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
@@ -229,7 +229,7 @@ jobs:
platform: 33
ndk: 29.0.14206865
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+545 -825
View File
File diff suppressed because it is too large Load Diff
+6 -9
View File
@@ -1,8 +1,8 @@
[package]
name = "gopher64"
version = "1.1.24"
version = "1.1.34"
edition = "2024"
rust-version = "1.96.0"
rust-version = "1.97.0"
[features]
default = ["gui"]
@@ -19,14 +19,14 @@ 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 = { version = "0.29", features = ["native-tls"] }
tokio-tungstenite = { version = "0.29", features = ["native-tls-vendored"] }
matchbox_socket = "0.14"
ggrs = { git = "https://github.com/gschup/ggrs.git", features = ["sync-send"] }
ggrs = { version = "0.13", features = ["sync-send"] }
rustc-hash = "2.1"
futures = "0.3"
rand = { version = "0.10", features = ["serde"] }
@@ -36,6 +36,7 @@ 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.41"
[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 }
@@ -44,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"
@@ -61,7 +60,6 @@ slint-build = { version = "1.12", optional = true }
cc = { version = "1.2", features = ["parallel"] }
[profile.release]
panic = "abort"
lto = "thin"
strip = true
@@ -69,7 +67,6 @@ strip = true
inherits = "release"
debug = true
strip = false
panic = "unwind"
[package.metadata.bundle]
name = "Gopher64"
+6 -2
View File
@@ -41,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
@@ -66,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,6 @@ class N64Activity : SDLActivity() {
override fun getLibraries(): Array<String> = arrayOf(
"SDL3",
"SDL3_ttf",
"SDL3_image",
"gopher64"
)
+81 -35
View File
@@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-05 12:03+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:147
#: src/ui/gui/appwindow.slint:164
msgctxt "Menu"
msgid "Local Game"
msgstr "Lokales Spiel"
#: src/ui/gui/appwindow.slint:151
#: src/ui/gui/appwindow.slint:168
msgctxt "Menu"
msgid "Controller Config"
msgstr "Controller-Konfiguration"
#: src/ui/gui/appwindow.slint:155
#: src/ui/gui/appwindow.slint:172
msgctxt "Menu"
msgid "Netplay"
msgstr "Netplay"
#: src/ui/gui/appwindow.slint:159
#: src/ui/gui/appwindow.slint:176
msgctxt "Menu"
msgid "Cheats"
msgstr "Cheats"
#: src/ui/gui/appwindow.slint:163
#: src/ui/gui/appwindow.slint:180
msgctxt "Menu"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:167
#: src/ui/gui/appwindow.slint:184
msgctxt "Menu"
msgid "Settings"
msgstr "Einstellungen"
#: src/ui/gui/appwindow.slint:171
#: src/ui/gui/appwindow.slint:188
msgctxt "Menu"
msgid "About"
msgstr "Über"
@@ -240,7 +240,7 @@ msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Sitzung erstellen"
#: src/ui/gui/netplay_create.slint:105
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Abbrechen"
@@ -345,67 +345,83 @@ msgctxt "Netplay"
msgid "Provide Feedback"
msgstr "Feedback geben"
#: src/ui/gui/netplay_wait.slint:16
#: src/ui/gui/netplay_wait.slint:17
msgctxt "NetplayWait"
msgid "Netplay: Waiting Room"
msgstr "Netplay: Warteraum"
#: src/ui/gui/netplay_wait.slint:28
#: src/ui/gui/netplay_wait.slint:29
msgctxt "NetplayWait"
msgid "Session Name:"
msgstr "Sitzungsname:"
#: src/ui/gui/netplay_wait.slint:34
#: src/ui/gui/netplay_wait.slint:35
msgctxt "NetplayWait"
msgid "Game Name:"
msgstr "Spielname:"
#: src/ui/gui/netplay_wait.slint:44
#: src/ui/gui/netplay_wait.slint:45
msgctxt "NetplayWait"
msgid "Player 1:"
msgstr "Spieler 1:"
#: src/ui/gui/netplay_wait.slint:48
#: src/ui/gui/netplay_wait.slint:49
msgctxt "NetplayWait"
msgid "Player 2:"
msgstr "Spieler 2:"
#: src/ui/gui/netplay_wait.slint:54
#: src/ui/gui/netplay_wait.slint:55
msgctxt "NetplayWait"
msgid "Player 3:"
msgstr "Spieler 3:"
#: src/ui/gui/netplay_wait.slint:58
#: src/ui/gui/netplay_wait.slint:59
msgctxt "NetplayWait"
msgid "Player 4:"
msgstr "Spieler 4:"
#: src/ui/gui/netplay_wait.slint:72
#: src/ui/gui/netplay_wait.slint:73
msgctxt "NetplayWait"
msgid "Enter chat message"
msgstr "Chatnachricht eingeben"
#: src/ui/gui/netplay_wait.slint:83
#: src/ui/gui/netplay_wait.slint:84
msgctxt "NetplayWait"
msgid "Send Message"
msgstr "Nachricht senden"
#: src/ui/gui/netplay_wait.slint:100
#: 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:115
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Empfohlen: "
#: src/ui/gui/netplay_wait.slint:123
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Spiel starten"
#: src/ui/gui/netplay_wait.slint:131
#: 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"
@@ -470,71 +486,101 @@ msgctxt "RetroAchievements"
msgid "View Supported Games"
msgstr "Unterstützte Spiele anzeigen"
#: src/ui/gui/settings_page.slint:24
#: src/ui/gui/settings_page.slint:25
msgctxt "Settings"
msgid "Settings"
msgstr "Einstellungen"
#: src/ui/gui/settings_page.slint:35
#: 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:49
#: src/ui/gui/settings_page.slint:51
msgctxt "Settings"
msgid "SSAA downscaling"
msgstr "SSAA-Herunterskalierung"
#: src/ui/gui/settings_page.slint:57
#: src/ui/gui/settings_page.slint:59
msgctxt "Settings"
msgid "Integer Scaling"
msgstr "Pixelgenaue Skalierung"
#: src/ui/gui/settings_page.slint:65
#: src/ui/gui/settings_page.slint:67
msgctxt "Settings"
msgid "Fullscreen"
msgstr "Vollbild"
#: src/ui/gui/settings_page.slint:73
#: src/ui/gui/settings_page.slint:75
msgctxt "Settings"
msgid "Widescreen (stretch)"
msgstr "16:9 (gestreckt)"
#: src/ui/gui/settings_page.slint:81
#: src/ui/gui/settings_page.slint:83
msgctxt "Settings"
msgid "VSync"
msgstr "VSync"
#: src/ui/gui/settings_page.slint:89
#: src/ui/gui/settings_page.slint:91
msgctxt "Settings"
msgid "Apply CRT shader"
msgstr "CRT-Shader anwenden"
#: src/ui/gui/settings_page.slint:101
#: 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:109
#: src/ui/gui/settings_page.slint:111
msgctxt "Settings"
msgid "Disable Expansion Pak"
msgstr "Expansion Pak deaktivieren"
#: src/ui/gui/settings_page.slint:117
#: src/ui/gui/settings_page.slint:119
msgctxt "Settings"
msgid "Enable Rewind"
msgstr "Rewind aktivieren"
#: src/ui/gui/settings_page.slint:125
#: src/ui/gui/settings_page.slint:127
msgctxt "Settings"
msgid "Emulate UNFLoader"
msgstr "UNFLoader emulieren"
#: src/ui/gui/settings_page.slint:133
#: 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?"
+82 -35
View File
@@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-05 12:03+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:147
#: src/ui/gui/appwindow.slint:164
msgctxt "Menu"
msgid "Local Game"
msgstr "Juego local"
#: src/ui/gui/appwindow.slint:151
#: src/ui/gui/appwindow.slint:168
msgctxt "Menu"
msgid "Controller Config"
msgstr "Config. del mando"
#: src/ui/gui/appwindow.slint:155
#: src/ui/gui/appwindow.slint:172
msgctxt "Menu"
msgid "Netplay"
msgstr "Netplay"
#: src/ui/gui/appwindow.slint:159
#: src/ui/gui/appwindow.slint:176
msgctxt "Menu"
msgid "Cheats"
msgstr "Trucos"
#: src/ui/gui/appwindow.slint:163
#: src/ui/gui/appwindow.slint:180
msgctxt "Menu"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:167
#: src/ui/gui/appwindow.slint:184
msgctxt "Menu"
msgid "Settings"
msgstr "Ajustes"
#: src/ui/gui/appwindow.slint:171
#: src/ui/gui/appwindow.slint:188
msgctxt "Menu"
msgid "About"
msgstr "Acerca de"
@@ -240,7 +240,7 @@ msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Crear sesión"
#: src/ui/gui/netplay_create.slint:105
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Cancelar"
@@ -345,67 +345,84 @@ msgctxt "Netplay"
msgid "Provide Feedback"
msgstr "Enviar comentarios"
#: src/ui/gui/netplay_wait.slint:16
#: src/ui/gui/netplay_wait.slint:17
msgctxt "NetplayWait"
msgid "Netplay: Waiting Room"
msgstr "Netplay: Sala de espera"
#: src/ui/gui/netplay_wait.slint:28
#: src/ui/gui/netplay_wait.slint:29
msgctxt "NetplayWait"
msgid "Session Name:"
msgstr "Nombre de la sesión:"
#: src/ui/gui/netplay_wait.slint:34
#: src/ui/gui/netplay_wait.slint:35
msgctxt "NetplayWait"
msgid "Game Name:"
msgstr "Nombre del juego:"
#: src/ui/gui/netplay_wait.slint:44
#: src/ui/gui/netplay_wait.slint:45
msgctxt "NetplayWait"
msgid "Player 1:"
msgstr "Jugador 1:"
#: src/ui/gui/netplay_wait.slint:48
#: src/ui/gui/netplay_wait.slint:49
msgctxt "NetplayWait"
msgid "Player 2:"
msgstr "Jugador 2:"
#: src/ui/gui/netplay_wait.slint:54
#: src/ui/gui/netplay_wait.slint:55
msgctxt "NetplayWait"
msgid "Player 3:"
msgstr "Jugador 3:"
#: src/ui/gui/netplay_wait.slint:58
#: src/ui/gui/netplay_wait.slint:59
msgctxt "NetplayWait"
msgid "Player 4:"
msgstr "Jugador 4:"
#: src/ui/gui/netplay_wait.slint:72
#: 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:83
#: src/ui/gui/netplay_wait.slint:84
msgctxt "NetplayWait"
msgid "Send Message"
msgstr "Enviar mensaje"
#: src/ui/gui/netplay_wait.slint:100
#: 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:115
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Recomendado: "
#: src/ui/gui/netplay_wait.slint:123
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Iniciar juego"
#: src/ui/gui/netplay_wait.slint:131
#: 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"
@@ -470,71 +487,101 @@ msgctxt "RetroAchievements"
msgid "View Supported Games"
msgstr "Ver juegos compatibles"
#: src/ui/gui/settings_page.slint:24
#: src/ui/gui/settings_page.slint:25
msgctxt "Settings"
msgid "Settings"
msgstr "Ajustes"
#: src/ui/gui/settings_page.slint:35
#: 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:49
#: src/ui/gui/settings_page.slint:51
msgctxt "Settings"
msgid "SSAA downscaling"
msgstr "Reducción de escala SSAA"
#: src/ui/gui/settings_page.slint:57
#: src/ui/gui/settings_page.slint:59
msgctxt "Settings"
msgid "Integer Scaling"
msgstr "Escalado por enteros"
#: src/ui/gui/settings_page.slint:65
#: src/ui/gui/settings_page.slint:67
msgctxt "Settings"
msgid "Fullscreen"
msgstr "Pantalla completa"
#: src/ui/gui/settings_page.slint:73
#: src/ui/gui/settings_page.slint:75
msgctxt "Settings"
msgid "Widescreen (stretch)"
msgstr "Pantalla ancha (estirado)"
#: src/ui/gui/settings_page.slint:81
#: src/ui/gui/settings_page.slint:83
msgctxt "Settings"
msgid "VSync"
msgstr "Sincronización vertical (VSync)"
#: src/ui/gui/settings_page.slint:89
#: src/ui/gui/settings_page.slint:91
msgctxt "Settings"
msgid "Apply CRT shader"
msgstr "Aplicar sombreador CRT"
#: src/ui/gui/settings_page.slint:101
#: 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:109
#: src/ui/gui/settings_page.slint:111
msgctxt "Settings"
msgid "Disable Expansion Pak"
msgstr "Desactivar Expansion Pak"
#: src/ui/gui/settings_page.slint:117
#: src/ui/gui/settings_page.slint:119
msgctxt "Settings"
msgid "Enable Rewind"
msgstr "Activar rebobinado"
#: src/ui/gui/settings_page.slint:125
#: src/ui/gui/settings_page.slint:127
msgctxt "Settings"
msgid "Emulate UNFLoader"
msgstr "Emular UNFLoader"
#: src/ui/gui/settings_page.slint:133
#: 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?"
+81 -35
View File
@@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-05 12:03+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:147
#: src/ui/gui/appwindow.slint:164
msgctxt "Menu"
msgid "Local Game"
msgstr "Jeu local"
#: src/ui/gui/appwindow.slint:151
#: src/ui/gui/appwindow.slint:168
msgctxt "Menu"
msgid "Controller Config"
msgstr "Configuration manette"
#: src/ui/gui/appwindow.slint:155
#: src/ui/gui/appwindow.slint:172
msgctxt "Menu"
msgid "Netplay"
msgstr "Netplay"
#: src/ui/gui/appwindow.slint:159
#: src/ui/gui/appwindow.slint:176
msgctxt "Menu"
msgid "Cheats"
msgstr "Triche"
#: src/ui/gui/appwindow.slint:163
#: src/ui/gui/appwindow.slint:180
msgctxt "Menu"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:167
#: src/ui/gui/appwindow.slint:184
msgctxt "Menu"
msgid "Settings"
msgstr "Paramètres"
#: src/ui/gui/appwindow.slint:171
#: src/ui/gui/appwindow.slint:188
msgctxt "Menu"
msgid "About"
msgstr "À propos"
@@ -240,7 +240,7 @@ msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Créer la session"
#: src/ui/gui/netplay_create.slint:105
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Annuler"
@@ -346,67 +346,83 @@ msgctxt "Netplay"
msgid "Provide Feedback"
msgstr "Donner votre avis"
#: src/ui/gui/netplay_wait.slint:16
#: src/ui/gui/netplay_wait.slint:17
msgctxt "NetplayWait"
msgid "Netplay: Waiting Room"
msgstr "Netplay : salle d'attente"
#: src/ui/gui/netplay_wait.slint:28
#: src/ui/gui/netplay_wait.slint:29
msgctxt "NetplayWait"
msgid "Session Name:"
msgstr "Nom de la session :"
#: src/ui/gui/netplay_wait.slint:34
#: src/ui/gui/netplay_wait.slint:35
msgctxt "NetplayWait"
msgid "Game Name:"
msgstr "Nom du jeu :"
#: src/ui/gui/netplay_wait.slint:44
#: src/ui/gui/netplay_wait.slint:45
msgctxt "NetplayWait"
msgid "Player 1:"
msgstr "Joueur 1 :"
#: src/ui/gui/netplay_wait.slint:48
#: src/ui/gui/netplay_wait.slint:49
msgctxt "NetplayWait"
msgid "Player 2:"
msgstr "Joueur 2 :"
#: src/ui/gui/netplay_wait.slint:54
#: src/ui/gui/netplay_wait.slint:55
msgctxt "NetplayWait"
msgid "Player 3:"
msgstr "Joueur 3 :"
#: src/ui/gui/netplay_wait.slint:58
#: src/ui/gui/netplay_wait.slint:59
msgctxt "NetplayWait"
msgid "Player 4:"
msgstr "Joueur 4 :"
#: src/ui/gui/netplay_wait.slint:72
#: src/ui/gui/netplay_wait.slint:73
msgctxt "NetplayWait"
msgid "Enter chat message"
msgstr "Saisir un message"
#: src/ui/gui/netplay_wait.slint:83
#: src/ui/gui/netplay_wait.slint:84
msgctxt "NetplayWait"
msgid "Send Message"
msgstr "Envoyer"
#: src/ui/gui/netplay_wait.slint:100
#: 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:115
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Recommandé : "
#: src/ui/gui/netplay_wait.slint:123
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Lancer la partie"
#: src/ui/gui/netplay_wait.slint:131
#: 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"
@@ -471,71 +487,101 @@ msgctxt "RetroAchievements"
msgid "View Supported Games"
msgstr "Voir les jeux pris en charge"
#: src/ui/gui/settings_page.slint:24
#: src/ui/gui/settings_page.slint:25
msgctxt "Settings"
msgid "Settings"
msgstr "Paramètres"
#: src/ui/gui/settings_page.slint:35
#: 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:49
#: src/ui/gui/settings_page.slint:51
msgctxt "Settings"
msgid "SSAA downscaling"
msgstr "Réduction SSAA"
#: src/ui/gui/settings_page.slint:57
#: src/ui/gui/settings_page.slint:59
msgctxt "Settings"
msgid "Integer Scaling"
msgstr "Mise à l'échelle par entiers"
#: src/ui/gui/settings_page.slint:65
#: src/ui/gui/settings_page.slint:67
msgctxt "Settings"
msgid "Fullscreen"
msgstr "Plein écran"
#: src/ui/gui/settings_page.slint:73
#: src/ui/gui/settings_page.slint:75
msgctxt "Settings"
msgid "Widescreen (stretch)"
msgstr "Écran large (étiré)"
#: src/ui/gui/settings_page.slint:81
#: src/ui/gui/settings_page.slint:83
msgctxt "Settings"
msgid "VSync"
msgstr "Synchro verticale"
#: src/ui/gui/settings_page.slint:89
#: src/ui/gui/settings_page.slint:91
msgctxt "Settings"
msgid "Apply CRT shader"
msgstr "Appliquer le filtre CRT"
#: src/ui/gui/settings_page.slint:101
#: 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:109
#: 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:117
#: src/ui/gui/settings_page.slint:119
msgctxt "Settings"
msgid "Enable Rewind"
msgstr "Activer le rembobinage"
#: src/ui/gui/settings_page.slint:125
#: src/ui/gui/settings_page.slint:127
msgctxt "Settings"
msgid "Emulate UNFLoader"
msgstr "Émuler UNFLoader"
#: src/ui/gui/settings_page.slint:133
#: 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?"
+77 -35
View File
@@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2026-06-05 12:03+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:147
#: src/ui/gui/appwindow.slint:164
msgctxt "Menu"
msgid "Local Game"
msgstr ""
#: src/ui/gui/appwindow.slint:151
#: src/ui/gui/appwindow.slint:168
msgctxt "Menu"
msgid "Controller Config"
msgstr ""
#: src/ui/gui/appwindow.slint:155
#: src/ui/gui/appwindow.slint:172
msgctxt "Menu"
msgid "Netplay"
msgstr ""
#: src/ui/gui/appwindow.slint:159
#: src/ui/gui/appwindow.slint:176
msgctxt "Menu"
msgid "Cheats"
msgstr ""
#: src/ui/gui/appwindow.slint:163
#: src/ui/gui/appwindow.slint:180
msgctxt "Menu"
msgid "RetroAchievements"
msgstr ""
#: src/ui/gui/appwindow.slint:167
#: src/ui/gui/appwindow.slint:184
msgctxt "Menu"
msgid "Settings"
msgstr ""
#: src/ui/gui/appwindow.slint:171
#: src/ui/gui/appwindow.slint:188
msgctxt "Menu"
msgid "About"
msgstr ""
@@ -238,7 +238,7 @@ msgctxt "NetplayCreate"
msgid "Create Session"
msgstr ""
#: src/ui/gui/netplay_create.slint:105
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr ""
@@ -343,67 +343,79 @@ msgctxt "Netplay"
msgid "Provide Feedback"
msgstr ""
#: src/ui/gui/netplay_wait.slint:16
#: src/ui/gui/netplay_wait.slint:17
msgctxt "NetplayWait"
msgid "Netplay: Waiting Room"
msgstr ""
#: src/ui/gui/netplay_wait.slint:28
#: src/ui/gui/netplay_wait.slint:29
msgctxt "NetplayWait"
msgid "Session Name:"
msgstr ""
#: src/ui/gui/netplay_wait.slint:34
#: src/ui/gui/netplay_wait.slint:35
msgctxt "NetplayWait"
msgid "Game Name:"
msgstr ""
#: src/ui/gui/netplay_wait.slint:44
#: src/ui/gui/netplay_wait.slint:45
msgctxt "NetplayWait"
msgid "Player 1:"
msgstr ""
#: src/ui/gui/netplay_wait.slint:48
#: src/ui/gui/netplay_wait.slint:49
msgctxt "NetplayWait"
msgid "Player 2:"
msgstr ""
#: src/ui/gui/netplay_wait.slint:54
#: src/ui/gui/netplay_wait.slint:55
msgctxt "NetplayWait"
msgid "Player 3:"
msgstr ""
#: src/ui/gui/netplay_wait.slint:58
#: src/ui/gui/netplay_wait.slint:59
msgctxt "NetplayWait"
msgid "Player 4:"
msgstr ""
#: src/ui/gui/netplay_wait.slint:72
#: src/ui/gui/netplay_wait.slint:73
msgctxt "NetplayWait"
msgid "Enter chat message"
msgstr ""
#: src/ui/gui/netplay_wait.slint:83
#: src/ui/gui/netplay_wait.slint:84
msgctxt "NetplayWait"
msgid "Send Message"
msgstr ""
#: src/ui/gui/netplay_wait.slint:100
#: 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:115
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr ""
#: src/ui/gui/netplay_wait.slint:123
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr ""
#: src/ui/gui/netplay_wait.slint:131
#: 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 ""
@@ -468,71 +480,101 @@ msgctxt "RetroAchievements"
msgid "View Supported Games"
msgstr ""
#: src/ui/gui/settings_page.slint:24
#: src/ui/gui/settings_page.slint:25
msgctxt "Settings"
msgid "Settings"
msgstr ""
#: src/ui/gui/settings_page.slint:35
#: 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:49
#: src/ui/gui/settings_page.slint:51
msgctxt "Settings"
msgid "SSAA downscaling"
msgstr ""
#: src/ui/gui/settings_page.slint:57
#: src/ui/gui/settings_page.slint:59
msgctxt "Settings"
msgid "Integer Scaling"
msgstr ""
#: src/ui/gui/settings_page.slint:65
#: src/ui/gui/settings_page.slint:67
msgctxt "Settings"
msgid "Fullscreen"
msgstr ""
#: src/ui/gui/settings_page.slint:73
#: src/ui/gui/settings_page.slint:75
msgctxt "Settings"
msgid "Widescreen (stretch)"
msgstr ""
#: src/ui/gui/settings_page.slint:81
#: src/ui/gui/settings_page.slint:83
msgctxt "Settings"
msgid "VSync"
msgstr ""
#: src/ui/gui/settings_page.slint:89
#: src/ui/gui/settings_page.slint:91
msgctxt "Settings"
msgid "Apply CRT shader"
msgstr ""
#: src/ui/gui/settings_page.slint:101
#: src/ui/gui/settings_page.slint:103
msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)"
msgstr ""
#: src/ui/gui/settings_page.slint:109
#: src/ui/gui/settings_page.slint:111
msgctxt "Settings"
msgid "Disable Expansion Pak"
msgstr ""
#: src/ui/gui/settings_page.slint:117
#: src/ui/gui/settings_page.slint:119
msgctxt "Settings"
msgid "Enable Rewind"
msgstr ""
#: src/ui/gui/settings_page.slint:125
#: src/ui/gui/settings_page.slint:127
msgctxt "Settings"
msgid "Emulate UNFLoader"
msgstr ""
#: src/ui/gui/settings_page.slint:133
#: 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?"
+80 -35
View File
@@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-05 12:03+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:147
#: src/ui/gui/appwindow.slint:164
msgctxt "Menu"
msgid "Local Game"
msgstr "ローカルゲーム"
#: src/ui/gui/appwindow.slint:151
#: src/ui/gui/appwindow.slint:168
msgctxt "Menu"
msgid "Controller Config"
msgstr "コントローラー設定"
#: src/ui/gui/appwindow.slint:155
#: src/ui/gui/appwindow.slint:172
msgctxt "Menu"
msgid "Netplay"
msgstr "ネットプレイ"
#: src/ui/gui/appwindow.slint:159
#: src/ui/gui/appwindow.slint:176
msgctxt "Menu"
msgid "Cheats"
msgstr "チート"
#: src/ui/gui/appwindow.slint:163
#: src/ui/gui/appwindow.slint:180
msgctxt "Menu"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:167
#: src/ui/gui/appwindow.slint:184
msgctxt "Menu"
msgid "Settings"
msgstr "設定"
#: src/ui/gui/appwindow.slint:171
#: src/ui/gui/appwindow.slint:188
msgctxt "Menu"
msgid "About"
msgstr "バージョン情報"
@@ -238,7 +238,7 @@ msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "セッションを作成"
#: src/ui/gui/netplay_create.slint:105
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "キャンセル"
@@ -343,67 +343,82 @@ msgctxt "Netplay"
msgid "Provide Feedback"
msgstr "フィードバックを送る"
#: src/ui/gui/netplay_wait.slint:16
#: src/ui/gui/netplay_wait.slint:17
msgctxt "NetplayWait"
msgid "Netplay: Waiting Room"
msgstr "ネットプレイ:待機室"
#: src/ui/gui/netplay_wait.slint:28
#: src/ui/gui/netplay_wait.slint:29
msgctxt "NetplayWait"
msgid "Session Name:"
msgstr "セッション名:"
#: src/ui/gui/netplay_wait.slint:34
#: src/ui/gui/netplay_wait.slint:35
msgctxt "NetplayWait"
msgid "Game Name:"
msgstr "ゲーム名:"
#: src/ui/gui/netplay_wait.slint:44
#: src/ui/gui/netplay_wait.slint:45
msgctxt "NetplayWait"
msgid "Player 1:"
msgstr "プレイヤー1"
#: src/ui/gui/netplay_wait.slint:48
#: src/ui/gui/netplay_wait.slint:49
msgctxt "NetplayWait"
msgid "Player 2:"
msgstr "プレイヤー2"
#: src/ui/gui/netplay_wait.slint:54
#: src/ui/gui/netplay_wait.slint:55
msgctxt "NetplayWait"
msgid "Player 3:"
msgstr "プレイヤー3"
#: src/ui/gui/netplay_wait.slint:58
#: src/ui/gui/netplay_wait.slint:59
msgctxt "NetplayWait"
msgid "Player 4:"
msgstr "プレイヤー4"
#: src/ui/gui/netplay_wait.slint:72
#: src/ui/gui/netplay_wait.slint:73
msgctxt "NetplayWait"
msgid "Enter chat message"
msgstr "チャットメッセージを入力"
#: src/ui/gui/netplay_wait.slint:83
#: src/ui/gui/netplay_wait.slint:84
msgctxt "NetplayWait"
msgid "Send Message"
msgstr "送信"
#: src/ui/gui/netplay_wait.slint:100
#: 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:115
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "推奨: "
#: src/ui/gui/netplay_wait.slint:123
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "ゲーム開始"
#: src/ui/gui/netplay_wait.slint:131
#: 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 "キャンセル"
@@ -468,71 +483,101 @@ msgctxt "RetroAchievements"
msgid "View Supported Games"
msgstr "対応ゲームを表示"
#: src/ui/gui/settings_page.slint:24
#: src/ui/gui/settings_page.slint:25
msgctxt "Settings"
msgid "Settings"
msgstr "設定"
#: src/ui/gui/settings_page.slint:35
#: 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:49
#: src/ui/gui/settings_page.slint:51
msgctxt "Settings"
msgid "SSAA downscaling"
msgstr "SSAAダウンスケール"
#: src/ui/gui/settings_page.slint:57
#: src/ui/gui/settings_page.slint:59
msgctxt "Settings"
msgid "Integer Scaling"
msgstr "整数倍スケーリング"
#: src/ui/gui/settings_page.slint:65
#: src/ui/gui/settings_page.slint:67
msgctxt "Settings"
msgid "Fullscreen"
msgstr "全画面表示"
#: src/ui/gui/settings_page.slint:73
#: src/ui/gui/settings_page.slint:75
msgctxt "Settings"
msgid "Widescreen (stretch)"
msgstr "ワイド画面(伸ばす)"
#: src/ui/gui/settings_page.slint:81
#: src/ui/gui/settings_page.slint:83
msgctxt "Settings"
msgid "VSync"
msgstr "垂直同期(VSync"
#: src/ui/gui/settings_page.slint:89
#: src/ui/gui/settings_page.slint:91
msgctxt "Settings"
msgid "Apply CRT shader"
msgstr "CRTシェーダーを適用"
#: src/ui/gui/settings_page.slint:101
#: 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:109
#: src/ui/gui/settings_page.slint:111
msgctxt "Settings"
msgid "Disable Expansion Pak"
msgstr "拡張パックを無効にする"
#: src/ui/gui/settings_page.slint:117
#: src/ui/gui/settings_page.slint:119
msgctxt "Settings"
msgid "Enable Rewind"
msgstr "巻き戻しを有効にする"
#: src/ui/gui/settings_page.slint:125
#: src/ui/gui/settings_page.slint:127
msgctxt "Settings"
msgid "Emulate UNFLoader"
msgstr "UNFLoaderをエミュレート"
#: src/ui/gui/settings_page.slint:133
#: 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?"
+81 -35
View File
@@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-05 12:03+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:147
#: src/ui/gui/appwindow.slint:164
msgctxt "Menu"
msgid "Local Game"
msgstr "Lokaal spel"
#: src/ui/gui/appwindow.slint:151
#: src/ui/gui/appwindow.slint:168
msgctxt "Menu"
msgid "Controller Config"
msgstr "Controllerconfiguratie"
#: src/ui/gui/appwindow.slint:155
#: src/ui/gui/appwindow.slint:172
msgctxt "Menu"
msgid "Netplay"
msgstr "Netplay"
#: src/ui/gui/appwindow.slint:159
#: src/ui/gui/appwindow.slint:176
msgctxt "Menu"
msgid "Cheats"
msgstr "Cheats"
#: src/ui/gui/appwindow.slint:163
#: src/ui/gui/appwindow.slint:180
msgctxt "Menu"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:167
#: src/ui/gui/appwindow.slint:184
msgctxt "Menu"
msgid "Settings"
msgstr "Instellingen"
#: src/ui/gui/appwindow.slint:171
#: src/ui/gui/appwindow.slint:188
msgctxt "Menu"
msgid "About"
msgstr "Over"
@@ -240,7 +240,7 @@ msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Sessie aanmaken"
#: src/ui/gui/netplay_create.slint:105
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Annuleren"
@@ -345,67 +345,83 @@ msgctxt "Netplay"
msgid "Provide Feedback"
msgstr "Feedback geven"
#: src/ui/gui/netplay_wait.slint:16
#: src/ui/gui/netplay_wait.slint:17
msgctxt "NetplayWait"
msgid "Netplay: Waiting Room"
msgstr "Netplay: wachtruimte"
#: src/ui/gui/netplay_wait.slint:28
#: src/ui/gui/netplay_wait.slint:29
msgctxt "NetplayWait"
msgid "Session Name:"
msgstr "Sessienaam:"
#: src/ui/gui/netplay_wait.slint:34
#: src/ui/gui/netplay_wait.slint:35
msgctxt "NetplayWait"
msgid "Game Name:"
msgstr "Spelnaam:"
#: src/ui/gui/netplay_wait.slint:44
#: src/ui/gui/netplay_wait.slint:45
msgctxt "NetplayWait"
msgid "Player 1:"
msgstr "Speler 1:"
#: src/ui/gui/netplay_wait.slint:48
#: src/ui/gui/netplay_wait.slint:49
msgctxt "NetplayWait"
msgid "Player 2:"
msgstr "Speler 2:"
#: src/ui/gui/netplay_wait.slint:54
#: src/ui/gui/netplay_wait.slint:55
msgctxt "NetplayWait"
msgid "Player 3:"
msgstr "Speler 3:"
#: src/ui/gui/netplay_wait.slint:58
#: src/ui/gui/netplay_wait.slint:59
msgctxt "NetplayWait"
msgid "Player 4:"
msgstr "Speler 4:"
#: src/ui/gui/netplay_wait.slint:72
#: src/ui/gui/netplay_wait.slint:73
msgctxt "NetplayWait"
msgid "Enter chat message"
msgstr "Chatbericht invoeren"
#: src/ui/gui/netplay_wait.slint:83
#: src/ui/gui/netplay_wait.slint:84
msgctxt "NetplayWait"
msgid "Send Message"
msgstr "Bericht verzenden"
#: src/ui/gui/netplay_wait.slint:100
#: 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:115
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Aanbevolen: "
#: src/ui/gui/netplay_wait.slint:123
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Spel starten"
#: src/ui/gui/netplay_wait.slint:131
#: 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"
@@ -470,71 +486,101 @@ msgctxt "RetroAchievements"
msgid "View Supported Games"
msgstr "Ondersteunde spellen bekijken"
#: src/ui/gui/settings_page.slint:24
#: src/ui/gui/settings_page.slint:25
msgctxt "Settings"
msgid "Settings"
msgstr "Instellingen"
#: src/ui/gui/settings_page.slint:35
#: 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:49
#: src/ui/gui/settings_page.slint:51
msgctxt "Settings"
msgid "SSAA downscaling"
msgstr "SSAA-downscaling"
#: src/ui/gui/settings_page.slint:57
#: src/ui/gui/settings_page.slint:59
msgctxt "Settings"
msgid "Integer Scaling"
msgstr "Schalen in hele getallen"
#: src/ui/gui/settings_page.slint:65
#: src/ui/gui/settings_page.slint:67
msgctxt "Settings"
msgid "Fullscreen"
msgstr "Volledig scherm"
#: src/ui/gui/settings_page.slint:73
#: src/ui/gui/settings_page.slint:75
msgctxt "Settings"
msgid "Widescreen (stretch)"
msgstr "Breedbeeld (uitgerekt)"
#: src/ui/gui/settings_page.slint:81
#: src/ui/gui/settings_page.slint:83
msgctxt "Settings"
msgid "VSync"
msgstr "VSync"
#: src/ui/gui/settings_page.slint:89
#: src/ui/gui/settings_page.slint:91
msgctxt "Settings"
msgid "Apply CRT shader"
msgstr "CRT-shader toepassen"
#: src/ui/gui/settings_page.slint:101
#: 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:109
#: src/ui/gui/settings_page.slint:111
msgctxt "Settings"
msgid "Disable Expansion Pak"
msgstr "Expansion Pak uitschakelen"
#: src/ui/gui/settings_page.slint:117
#: src/ui/gui/settings_page.slint:119
msgctxt "Settings"
msgid "Enable Rewind"
msgstr "Terugspoelen inschakelen"
#: src/ui/gui/settings_page.slint:125
#: src/ui/gui/settings_page.slint:127
msgctxt "Settings"
msgid "Emulate UNFLoader"
msgstr "UNFLoader emuleren"
#: src/ui/gui/settings_page.slint:133
#: 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?"
+85 -39
View File
@@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2026-06-05 12:03+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:147
#: src/ui/gui/appwindow.slint:164
msgctxt "Menu"
msgid "Local Game"
msgstr "Jogo Local"
#: src/ui/gui/appwindow.slint:151
#: src/ui/gui/appwindow.slint:168
msgctxt "Menu"
msgid "Controller Config"
msgstr "Configurar Controle"
#: src/ui/gui/appwindow.slint:155
#: src/ui/gui/appwindow.slint:172
msgctxt "Menu"
msgid "Netplay"
msgstr "Netplay"
#: src/ui/gui/appwindow.slint:159
#: src/ui/gui/appwindow.slint:176
msgctxt "Menu"
msgid "Cheats"
msgstr "Trapaças"
#: src/ui/gui/appwindow.slint:163
#: src/ui/gui/appwindow.slint:180
msgctxt "Menu"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:167
#: src/ui/gui/appwindow.slint:184
msgctxt "Menu"
msgid "Settings"
msgstr "Configurações"
#: src/ui/gui/appwindow.slint:171
#: src/ui/gui/appwindow.slint:188
msgctxt "Menu"
msgid "About"
msgstr "Sobre"
@@ -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"
@@ -200,7 +200,7 @@ msgstr "Nome da Sessão:"
#: 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:36
msgctxt "NetplayCreate"
@@ -240,7 +240,7 @@ msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Criar Sessão"
#: src/ui/gui/netplay_create.slint:105
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Cancelar"
@@ -346,67 +346,83 @@ msgctxt "Netplay"
msgid "Provide Feedback"
msgstr "Fornecer Feedback"
#: src/ui/gui/netplay_wait.slint:16
#: src/ui/gui/netplay_wait.slint:17
msgctxt "NetplayWait"
msgid "Netplay: Waiting Room"
msgstr "Netplay: Aguardando Sala"
#: src/ui/gui/netplay_wait.slint:28
#: src/ui/gui/netplay_wait.slint:29
msgctxt "NetplayWait"
msgid "Session Name:"
msgstr "Nome da Sessão:"
#: src/ui/gui/netplay_wait.slint:34
#: src/ui/gui/netplay_wait.slint:35
msgctxt "NetplayWait"
msgid "Game Name:"
msgstr "Nome do Jogo:"
#: src/ui/gui/netplay_wait.slint:44
#: src/ui/gui/netplay_wait.slint:45
msgctxt "NetplayWait"
msgid "Player 1:"
msgstr "Jogador 1"
msgstr "Jogador 1:"
#: src/ui/gui/netplay_wait.slint:48
#: src/ui/gui/netplay_wait.slint:49
msgctxt "NetplayWait"
msgid "Player 2:"
msgstr "Jogador 2:"
#: src/ui/gui/netplay_wait.slint:54
#: src/ui/gui/netplay_wait.slint:55
msgctxt "NetplayWait"
msgid "Player 3:"
msgstr "Jogador 3:"
#: src/ui/gui/netplay_wait.slint:58
#: src/ui/gui/netplay_wait.slint:59
msgctxt "NetplayWait"
msgid "Player 4:"
msgstr "Jogador 4:"
#: src/ui/gui/netplay_wait.slint:72
#: src/ui/gui/netplay_wait.slint:73
msgctxt "NetplayWait"
msgid "Enter chat message"
msgstr "Escrever mensagem no chat"
#: src/ui/gui/netplay_wait.slint:83
#: src/ui/gui/netplay_wait.slint:84
msgctxt "NetplayWait"
msgid "Send Message"
msgstr "Enviar Mensagem"
#: src/ui/gui/netplay_wait.slint:100
#: 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:115
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Recomendado: "
#: src/ui/gui/netplay_wait.slint:123
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Iniciar jogo"
#: src/ui/gui/netplay_wait.slint:131
#: 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"
@@ -471,71 +487,101 @@ msgctxt "RetroAchievements"
msgid "View Supported Games"
msgstr "Ver Jogos com Suporte"
#: src/ui/gui/settings_page.slint:24
#: src/ui/gui/settings_page.slint:25
msgctxt "Settings"
msgid "Settings"
msgstr "Configurações"
#: src/ui/gui/settings_page.slint:35
#: 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:49
#: src/ui/gui/settings_page.slint:51
msgctxt "Settings"
msgid "SSAA downscaling"
msgstr "Redução de SSAA"
#: src/ui/gui/settings_page.slint:57
#: src/ui/gui/settings_page.slint:59
msgctxt "Settings"
msgid "Integer Scaling"
msgstr "Proporção Íntegra"
#: src/ui/gui/settings_page.slint:65
#: src/ui/gui/settings_page.slint:67
msgctxt "Settings"
msgid "Fullscreen"
msgstr "Tela Cheia"
#: src/ui/gui/settings_page.slint:73
#: src/ui/gui/settings_page.slint:75
msgctxt "Settings"
msgid "Widescreen (stretch)"
msgstr "Widescreen (esticar)"
#: src/ui/gui/settings_page.slint:81
#: src/ui/gui/settings_page.slint:83
msgctxt "Settings"
msgid "VSync"
msgstr "VSync"
#: src/ui/gui/settings_page.slint:89
#: src/ui/gui/settings_page.slint:91
msgctxt "Settings"
msgid "Apply CRT shader"
msgstr "Aplicar filtro de CRT"
#: src/ui/gui/settings_page.slint:101
#: 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:109
#: src/ui/gui/settings_page.slint:111
msgctxt "Settings"
msgid "Disable Expansion Pak"
msgstr "Desabilitar Expansion Pak"
#: src/ui/gui/settings_page.slint:117
#: src/ui/gui/settings_page.slint:119
msgctxt "Settings"
msgid "Enable Rewind"
msgstr "Habilitar rebobinamento"
#: src/ui/gui/settings_page.slint:125
#: src/ui/gui/settings_page.slint:127
msgctxt "Settings"
msgid "Emulate UNFLoader"
msgstr "Emular UNFLoader"
#: src/ui/gui/settings_page.slint:133
#: 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?"
+83 -36
View File
@@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-05 12:03+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:147
#: src/ui/gui/appwindow.slint:164
msgctxt "Menu"
msgid "Local Game"
msgstr "Локальная игра"
#: src/ui/gui/appwindow.slint:151
#: src/ui/gui/appwindow.slint:168
msgctxt "Menu"
msgid "Controller Config"
msgstr "Настройка контроллера"
#: src/ui/gui/appwindow.slint:155
#: src/ui/gui/appwindow.slint:172
msgctxt "Menu"
msgid "Netplay"
msgstr "Сетевая игра"
#: src/ui/gui/appwindow.slint:159
#: src/ui/gui/appwindow.slint:176
msgctxt "Menu"
msgid "Cheats"
msgstr "Читы"
#: src/ui/gui/appwindow.slint:163
#: src/ui/gui/appwindow.slint:180
msgctxt "Menu"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:167
#: src/ui/gui/appwindow.slint:184
msgctxt "Menu"
msgid "Settings"
msgstr "Настройки"
#: src/ui/gui/appwindow.slint:171
#: src/ui/gui/appwindow.slint:188
msgctxt "Menu"
msgid "About"
msgstr "О программе"
@@ -239,7 +240,7 @@ msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Создать сессию"
#: src/ui/gui/netplay_create.slint:105
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Отмена"
@@ -344,67 +345,83 @@ msgctxt "Netplay"
msgid "Provide Feedback"
msgstr "Оставить отзыв"
#: src/ui/gui/netplay_wait.slint:16
#: src/ui/gui/netplay_wait.slint:17
msgctxt "NetplayWait"
msgid "Netplay: Waiting Room"
msgstr "Сетевая игра: Комната ожидания"
#: src/ui/gui/netplay_wait.slint:28
#: src/ui/gui/netplay_wait.slint:29
msgctxt "NetplayWait"
msgid "Session Name:"
msgstr "Имя сессии:"
#: src/ui/gui/netplay_wait.slint:34
#: src/ui/gui/netplay_wait.slint:35
msgctxt "NetplayWait"
msgid "Game Name:"
msgstr "Название игры:"
#: src/ui/gui/netplay_wait.slint:44
#: src/ui/gui/netplay_wait.slint:45
msgctxt "NetplayWait"
msgid "Player 1:"
msgstr "Игрок 1:"
#: src/ui/gui/netplay_wait.slint:48
#: src/ui/gui/netplay_wait.slint:49
msgctxt "NetplayWait"
msgid "Player 2:"
msgstr "Игрок 2:"
#: src/ui/gui/netplay_wait.slint:54
#: src/ui/gui/netplay_wait.slint:55
msgctxt "NetplayWait"
msgid "Player 3:"
msgstr "Игрок 3:"
#: src/ui/gui/netplay_wait.slint:58
#: src/ui/gui/netplay_wait.slint:59
msgctxt "NetplayWait"
msgid "Player 4:"
msgstr "Игрок 4:"
#: src/ui/gui/netplay_wait.slint:72
#: src/ui/gui/netplay_wait.slint:73
msgctxt "NetplayWait"
msgid "Enter chat message"
msgstr "Введите сообщение чата"
#: src/ui/gui/netplay_wait.slint:83
#: src/ui/gui/netplay_wait.slint:84
msgctxt "NetplayWait"
msgid "Send Message"
msgstr "Отправить сообщение"
#: src/ui/gui/netplay_wait.slint:100
#: 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:115
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Рекомендуется: "
#: src/ui/gui/netplay_wait.slint:123
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Начать игру"
#: src/ui/gui/netplay_wait.slint:131
#: 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 "Отмена"
@@ -469,71 +486,101 @@ msgctxt "RetroAchievements"
msgid "View Supported Games"
msgstr "Просмотреть поддерживаемые игры"
#: src/ui/gui/settings_page.slint:24
#: src/ui/gui/settings_page.slint:25
msgctxt "Settings"
msgid "Settings"
msgstr "Настройки"
#: src/ui/gui/settings_page.slint:35
#: 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:49
#: src/ui/gui/settings_page.slint:51
msgctxt "Settings"
msgid "SSAA downscaling"
msgstr "SSAA даунскейлинг"
#: src/ui/gui/settings_page.slint:57
#: src/ui/gui/settings_page.slint:59
msgctxt "Settings"
msgid "Integer Scaling"
msgstr "Целочисленное масштабирование"
#: src/ui/gui/settings_page.slint:65
#: src/ui/gui/settings_page.slint:67
msgctxt "Settings"
msgid "Fullscreen"
msgstr "Полноэкранный режим"
#: src/ui/gui/settings_page.slint:73
#: src/ui/gui/settings_page.slint:75
msgctxt "Settings"
msgid "Widescreen (stretch)"
msgstr "Широкоэкранный режим (растянуто)"
#: src/ui/gui/settings_page.slint:81
#: src/ui/gui/settings_page.slint:83
msgctxt "Settings"
msgid "VSync"
msgstr "Вертикальная синхронизация"
#: src/ui/gui/settings_page.slint:89
#: src/ui/gui/settings_page.slint:91
msgctxt "Settings"
msgid "Apply CRT shader"
msgstr "Применить шейдер ЭЛТ"
#: src/ui/gui/settings_page.slint:101
#: src/ui/gui/settings_page.slint:103
msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)"
msgstr "Разгон процессора N64 (может вызвать ошибки)"
#: src/ui/gui/settings_page.slint:109
#: src/ui/gui/settings_page.slint:111
msgctxt "Settings"
msgid "Disable Expansion Pak"
msgstr "Отключить Expansion Pak"
#: src/ui/gui/settings_page.slint:117
#: src/ui/gui/settings_page.slint:119
msgctxt "Settings"
msgid "Enable Rewind"
msgstr "Включить перемотку назад"
#: src/ui/gui/settings_page.slint:125
#: src/ui/gui/settings_page.slint:127
msgctxt "Settings"
msgid "Emulate UNFLoader"
msgstr "Эмулировать UNFLoader"
#: src/ui/gui/settings_page.slint:133
#: 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?"
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

+33 -31
View File
@@ -335,11 +335,11 @@ static void rdp_new_processor() {
gfx_info.RDRAM_SIZE / 2, flags);
g_tmem = processor->get_tmem();
if (!g_tmem) {
printf("Failed to get tmem\n");
LOGE("Failed to get tmem\n");
}
g_hidden_rdram = processor->begin_read_hidden_rdram();
if (!g_hidden_rdram) {
printf("Failed to get hidden_rdram\n");
LOGE("Failed to get hidden_rdram\n");
}
sync_signal = 0;
@@ -376,7 +376,7 @@ void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
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;
}
@@ -622,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);
@@ -646,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();
@@ -693,7 +692,10 @@ 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();
}
@@ -788,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;
}
}
+1 -1
View File
@@ -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) {}
+1 -1
View File
@@ -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
View File
@@ -1,2 +1,2 @@
[toolchain]
channel = "1.96.0"
channel = "1.97.0"
+38 -6
View File
@@ -52,6 +52,7 @@ pub fn run_game(
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 {
@@ -62,17 +63,32 @@ pub fn run_game(
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)
@@ -105,6 +121,11 @@ pub fn run_game(
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);
@@ -114,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 {
@@ -124,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>> {
@@ -314,10 +347,9 @@ impl Device {
savestate: savestates::Savestate {
save_state: false,
load_state: false,
save_rewind: false,
load_rewind: false,
last_rewind_saved: 0.0,
rewind_pool: std::sync::Arc::new(std::sync::Mutex::new(
rewind_pool: std::sync::Arc::new(tokio::sync::Mutex::new(
std::collections::BTreeMap::new(),
)),
},
-13
View File
@@ -1,5 +1,4 @@
use crate::device;
use crate::netplay;
use crate::ui;
use sha2::{Digest, Sha256};
@@ -122,18 +121,6 @@ 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();
+3 -10
View File
@@ -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 {
+1 -1
View File
@@ -59,7 +59,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
let input = if let Some(netplay) = &mut device.netplay {
netplay.inputs[channel].0
} else {
ui::input::get(&mut device.ui, channel, device.speed_limiter.frame_counter)
ui::input::get(&mut device.ui, channel)
};
device.pif.ram[offset..offset + 4].copy_from_slice(&input.data.to_ne_bytes());
+3 -1
View File
@@ -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,
}
}
+41 -32
View File
@@ -3,14 +3,14 @@ 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;
@@ -103,7 +103,9 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
if device.netplay.is_none() {
savestates::process_savestates(device);
}
let _ = device.ui.video.fps_tx.as_ref().unwrap().try_send(true);
if !netplay::in_rollback(device.netplay.as_ref()) {
let _ = device.ui.video.fps_tx.as_ref().unwrap().send(true);
}
}
}
_ => {
@@ -113,50 +115,36 @@ 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) {
device.vi.elapsed_time += device.vi.frame_time;
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.as_ref().unwrap().try_send(true);
retroachievements::do_frame();
let (speed_limiter_toggled, paused) = ui::video::check_callback(device);
if device.netplay.is_none()
&& device.ui.config.emulation.rewind
&& device.vi.elapsed_time - device.savestate.last_rewind_saved > 1.0
{
device.savestate.save_rewind = true;
device.savestate.last_rewind_saved = device.vi.elapsed_time;
}
if speed_limiter_toggled {
reset_pace_deadline(device);
}
if device
.speed_limiter
.frame_counter
.is_multiple_of(device.speed_limiter.limit_freq)
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() };
ui::video::update_screen();
device.speed_limiter.frame_counter += 1;
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);
}
if device.netplay.is_some() {
device.netplay.as_mut().unwrap().inputs = netplay::process_requests(device);
}
if device.netplay.is_none() && paused {
} else if paused {
if retroachievements::get_hardcore() {
ui::video::onscreen_message(
"Cannot pause in RA hardcore mode",
@@ -166,6 +154,16 @@ pub fn vertical_interrupt_event(device: &mut device::Device) {
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;
@@ -176,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) {
@@ -212,6 +220,7 @@ fn speed_limiter(device: &mut device::Device) {
if now < deadline {
let dur = deadline - now;
spin_sleep::sleep(dur);
if dur < device.speed_limiter.min_wait_time {
device.speed_limiter.min_wait_time = dur;
}
+62 -33
View File
@@ -22,6 +22,8 @@ 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>,
@@ -104,6 +106,19 @@ fn set_app_id() {
}
}
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();
@@ -115,8 +130,6 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
ui::sdl_hints();
if let Some(game) = args.game {
ui::disable_auto_update_joysticks();
let file_path = std::path::Path::new(&game).to_path_buf();
let Some(rom_contents) = device::get_rom_contents(&file_path) else {
return Err(Error::other(format!(
@@ -150,15 +163,22 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
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);
@@ -169,22 +189,7 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
let mut shutdown_tx = None;
let mut usb_handle = None;
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
{
device.netplay = netplay::init(
server_addr,
player_number,
number_of_players,
input_delay,
device::cart::rom::is_system_pal(&rom_contents),
);
if device.netplay.is_none() {
return Err(Error::other("Failed to connect to netplay server"));
}
} 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()
@@ -210,7 +215,7 @@ pub 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) && device.netplay.is_none() {
if cfg!(ra_hardcore_enabled) && args.netplay_server_addr.is_none() {
ra_config.hardcore
} else {
false
@@ -228,16 +233,29 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
tx.send(false).unwrap();
}
let join_handle = std::thread::spawn(move || {
rx.blocking_recv().unwrap();
});
join_handle.join().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,
@@ -248,6 +266,7 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
load_savestate_slot: args.load_state,
},
ra_config,
netplay_config,
);
// on Android, the client is shut down in the app_window function
@@ -274,11 +293,11 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
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();
@@ -322,8 +341,10 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
#[cfg(feature = "gui")]
{
let app = ui::gui::AppWindow::new().unwrap();
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);
ui::gui::app_window(&app, false, no_intro_map);
}
}
@@ -333,8 +354,10 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
#[cfg(target_os = "android")]
#[unsafe(no_mangle)]
#[tokio::main]
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 { .. } => {
@@ -342,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();
}
}
@@ -363,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
View File
@@ -5,8 +5,12 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use clap::Parser;
#[tokio::main]
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())
let result = gopher64::run(args, std::env::args().count());
close_tx.send(()).unwrap();
result
}
+252 -69
View File
@@ -1,8 +1,14 @@
use crate::device;
use crate::savestates;
use crate::ui;
use sha2::digest::Digest;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RtcIceServerConfig {
pub urls: Vec<String>,
pub username: Option<String>,
pub credential: Option<String>,
}
pub struct GgrsConfig;
impl ggrs::Config for GgrsConfig {
type Input = ui::input::InputData;
@@ -11,21 +17,18 @@ impl ggrs::Config for GgrsConfig {
type Address = matchbox_socket::PeerId;
}
pub struct MatchboxSocket(matchbox_socket::WebRtcSocket);
pub struct MatchboxChannel {
channel: matchbox_socket::WebRtcChannel,
}
impl ggrs::NonBlockingSocket<matchbox_socket::PeerId> for MatchboxSocket {
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 channel = self.0.get_channel_mut(0).unwrap();
if channel.config().max_retransmits != Some(0) || channel.config().ordered {
eprintln!("Sending GGRS traffic over reliable channel");
}
channel.send(encoded.into(), *addr);
let _ = self.channel.try_send(encoded.into(), *addr);
}
fn receive_all_messages(&mut self) -> Vec<(matchbox_socket::PeerId, ggrs::Message)> {
let channel = self.0.get_channel_mut(0).unwrap();
channel
self.channel
.receive()
.iter()
.filter_map(|(peer, packet)| {
@@ -36,7 +39,16 @@ impl ggrs::NonBlockingSocket<matchbox_socket::PeerId> for MatchboxSocket {
}
}
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 {
pub disconnected: bool,
pub session: ggrs::P2PSession<GgrsConfig>,
pub reliable_channel: matchbox_socket::WebRtcChannel,
pub peers: Vec<matchbox_socket::PeerId>,
@@ -48,6 +60,7 @@ pub struct Netplay {
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,
}
#[derive(serde::Serialize, serde::Deserialize)]
@@ -61,7 +74,12 @@ fn send_message(netplay: &mut Netplay, message: NetplayMessage) {
let chunks = data.chunks(16384).collect::<Vec<&[u8]>>();
for peer in netplay.peers.iter() {
for chunk in chunks.iter() {
netplay.reliable_channel.send(chunk.to_vec().into(), *peer);
if let Err(e) = netplay
.reliable_channel
.try_send(chunk.to_vec().into(), *peer)
{
eprintln!("Failed to send message: {}", e);
}
}
}
}
@@ -86,6 +104,8 @@ fn process_reliable_messages(netplay: &mut Netplay) {
.messages
.insert(decoded_message.name, decoded_message.data);
netplay.incoming_message.clear();
check_input_delay(netplay);
check_disconnect(netplay);
}
}
}
@@ -110,16 +130,18 @@ fn receive_message(netplay: &mut Netplay, name: &str) -> Vec<u8> {
fn send_player_number(
channel: &mut matchbox_socket::WebRtcChannel,
peers: Vec<matchbox_socket::PeerId>,
peers: &[matchbox_socket::PeerId],
player_number: usize,
) {
let message = NetplayMessage {
name: "player_number".to_string(),
data: player_number.to_be_bytes().to_vec(),
data: (player_number as u64).to_be_bytes().to_vec(),
};
let data = postcard::to_stdvec(&message).unwrap();
for peer in peers {
channel.send(data.clone().into(), peer);
if let Err(e) = channel.try_send(data.clone().into(), *peer) {
eprintln!("Failed to send message: {}", e);
}
}
}
@@ -131,7 +153,7 @@ fn get_player_numbers(
let message = postcard::from_bytes::<NetplayMessage>(&data).unwrap();
if message.name == "player_number" {
player_numbers.insert(
usize::from_be_bytes(message.data.try_into().unwrap()),
u64::from_be_bytes(message.data.try_into().unwrap()) as usize,
Some(peer),
);
}
@@ -179,9 +201,13 @@ pub fn receive_save(netplay: &mut Netplay, save_type: &str, save_data: &mut Vec<
}
pub fn send_input_delay(netplay: &mut Netplay, input_delay: usize) {
if input_delay < 1 {
return;
}
let message = NetplayMessage {
name: "input_delay".to_string(),
data: input_delay.to_be_bytes().to_vec(),
data: (input_delay as u64).to_be_bytes().to_vec(),
};
send_message(netplay, message);
change_input_delay(netplay, input_delay);
@@ -203,13 +229,39 @@ fn change_input_delay(netplay: &mut Netplay, input_delay: usize) {
fn check_input_delay(netplay: &mut Netplay) {
if let Some(data) = netplay.messages.remove("input_delay") {
let input_delay = usize::from_be_bytes(data.try_into().unwrap());
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)> {
@@ -217,7 +269,7 @@ pub fn process_requests(
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));
//savestates::create_savestate(device, true, Some(frame));
let mut hasher = sha2::Sha256::new();
for reg in device.cpu.cop0.regs.as_ref() {
@@ -227,8 +279,9 @@ pub fn process_requests(
cell.save(frame, Some(frame), Some(hash));
}
ggrs::GgrsRequest::LoadGameState { cell, frame: _ } => {
if let Some(frame) = cell.load() {
savestates::load_savestate(device, true, Some(frame));
eprintln!("attempting to load game state");
if let Some(_frame) = cell.load() {
// savestates::load_savestate(device, true, Some(frame));
}
}
ggrs::GgrsRequest::AdvanceFrame { inputs } => {
@@ -236,26 +289,25 @@ pub fn process_requests(
}
}
} else {
// unsafe { sdl3_sys::events::SDL_PumpEvents() }; // so the screen doesn't freeze
process_netplay(device);
}
}
}
fn process_netplay(device: &mut device::Device) {
let netplay = device.netplay.as_mut().unwrap();
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 { .. } => {
eprintln!("peer disconnected");
ui::video::onscreen_message(
"Peer disconnected",
ui::video::MESSAGE_LENGTH_MESSAGE_LONG,
);
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");
@@ -275,20 +327,51 @@ fn process_netplay(device: &mut device::Device) {
}
}
}
}
fn process_netplay(device: &mut device::Device) {
let netplay = device.netplay.as_mut().unwrap();
poll_clients(netplay);
process_reliable_messages(netplay);
check_input_delay(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, device.speed_limiter.frame_counter);
let local_input = if netplay.session.current_frame() > netplay.session.max_prediction() as i32 {
ui::input::get(&mut device.ui, 0)
} else {
//workaround for disabled rollback
ui::input::InputData::default()
};
let local_handle = *netplay.session.local_player_handles().first().unwrap();
netplay
.session
.add_local_input(local_handle, local_input)
.unwrap();
// avoid rollback
while !netplay.disconnected
&& netplay.session.current_frame() > netplay.session.confirmed_frame()
&& netplay.session.confirmed_frame() != ggrs::NULL_FRAME
{
poll_clients(netplay);
}
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);
@@ -300,56 +383,128 @@ fn advance_frame(device: &mut device::Device) {
}
}
pub fn init(
server_addr: String,
player_number: usize,
number_of_players: usize,
input_delay: usize,
pal: bool,
) -> Option<Netplay> {
let (socket, loop_fut) = matchbox_socket::WebRtcSocketBuilder::new(server_addr)
.add_unreliable_channel()
.add_reliable_channel()
.build();
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);
}
});
let mut matchbox_socket = MatchboxSocket(socket);
let mut reliable_channel = matchbox_socket.0.take_channel(1).unwrap();
let now = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(10);
let mut player_numbers = std::collections::BTreeMap::new();
player_numbers.insert(player_number, None);
loop {
matchbox_socket.0.update_peers();
let peers = matchbox_socket
.0
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>>();
send_player_number(&mut reliable_channel, peers, player_number);
get_player_numbers(&mut reliable_channel, &mut player_numbers);
if player_numbers.len() == number_of_players {
break;
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 now.elapsed() > timeout {
eprintln!("Could not connect to netplay peers");
return None;
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,
);
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(number_of_players)
.with_num_players(netplay_config.number_of_players)
.unwrap()
.with_input_delay(input_delay)
.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_max_prediction_window(16)
.with_disconnect_timeout(std::time::Duration::from_secs(if cfg!(debug_assertions) {
10
} else {
@@ -370,34 +525,62 @@ pub fn init(
}
}
let mut session = session_builder.start_p2p_session(matchbox_socket).unwrap();
let matchbox_channel = MatchboxChannel {
channel: socket.take_channel(0).unwrap(),
};
let reliable_channel = socket.take_channel(1).unwrap();
if matchbox_channel.channel.config().max_retransmits != Some(0)
|| matchbox_channel.channel.config().ordered
{
eprintln!("Sending GGRS traffic over reliable channel");
}
let mut session = session_builder.start_p2p_session(matchbox_channel).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));
}
Some(Netplay {
disconnected: false,
incoming_message: vec![],
input_delay,
input_delay: netplay_config.input_delay,
session,
reliable_channel,
peers,
player_number,
player_number: netplay_config.player_number,
connected: [
number_of_players > 0,
number_of_players > 1,
number_of_players > 2,
number_of_players > 3,
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
}
let _ = std::fs::remove_file(&netplay.ice_config_path);
}
+2 -4
View File
@@ -150,10 +150,8 @@ pub 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);
};
let join_handle = std::thread::spawn(move || {
rx.blocking_recv().unwrap();
});
join_handle.join().unwrap();
rx.blocking_recv().unwrap();
let mut c_title = std::ptr::null();
let mut c_image_url = std::ptr::null();
+61 -68
View File
@@ -74,12 +74,11 @@ where
pub struct Savestate {
pub save_state: bool,
pub load_state: bool,
pub save_rewind: bool,
pub load_rewind: bool,
pub last_rewind_saved: f64,
#[serde(skip)]
pub rewind_pool:
std::sync::Arc<std::sync::Mutex<std::collections::BTreeMap<i32, SavestateData>>>,
std::sync::Arc<tokio::sync::Mutex<std::collections::BTreeMap<i32, SavestateData>>>,
}
pub struct SavestateData {
@@ -89,25 +88,30 @@ pub struct SavestateData {
ra_state: Vec<u8>,
}
static DEVICE_CLONE: std::sync::LazyLock<std::sync::Mutex<Box<device::Device>>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(device::Device::new(false)));
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<std::sync::Mutex<ui::storage::Saves>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(ui::storage::Saves::default()));
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) {
if device.savestate.save_state || device.savestate.save_rewind {
let rewind = device.savestate.save_rewind && !device.savestate.save_state;
if rewind {
device.savestate.save_rewind = false;
}
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;
create_savestate(device, rewind, None);
} else if device.savestate.load_state || device.savestate.load_rewind {
load_savestate(device, device.savestate.load_rewind, None);
device.savestate.load_state = false;
let rewind = device.savestate.load_rewind;
device.savestate.load_rewind = false;
load_savestate(device, rewind, None);
}
}
@@ -122,18 +126,10 @@ pub fn create_savestate(device: &mut device::Device, rewind: bool, rewind_frame:
let mut ra_state: Vec<u8> = vec![0; retroachievements::state_size()];
retroachievements::save_state(ra_state.as_mut_ptr(), ra_state.len());
if let Ok(mut device_clone) = DEVICE_CLONE.lock()
&& let Ok(mut saves_clone) = SAVES_CLONE.lock()
{
device_clone.clone_state(device);
saves_clone.clone_from(&device.ui.storage.saves);
} else {
ui::video::onscreen_message(
"Failed to create savestate",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
return;
}
DEVICE_CLONE.blocking_lock().clone_state(device);
SAVES_CLONE
.blocking_lock()
.clone_from(&device.ui.storage.saves);
let save_path = device.ui.storage.paths.savestate_file_path.clone();
let save_state_slot = device.ui.storage.save_state_slot;
@@ -143,39 +139,32 @@ pub fn create_savestate(device: &mut device::Device, rewind: bool, rewind_frame:
let mut error = false;
if rewind {
if let Ok(device_clone) = DEVICE_CLONE.lock()
&& let Ok(saves_clone) = SAVES_CLONE.lock()
&& let Ok(mut pool) = rewind_pool.lock()
{
let mut state = device::Device::new(false);
state.clone_state(device_clone.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.clone(),
rdp_state,
ra_state,
},
);
if pool.len() > 30 {
pool.pop_first();
}
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 {
error = true;
0
};
pool.insert(
key,
SavestateData {
device: state,
saves: SAVES_CLONE.lock().await.clone(),
rdp_state,
ra_state,
},
);
if pool.len() > 30 {
pool.pop_first();
}
} else {
let compressed_file = if let Ok(device_clone) = DEVICE_CLONE.lock()
&& let Ok(saves_clone) = SAVES_CLONE.lock()
&& let Ok(device_data) = postcard::to_stdvec(device_clone.deref())
&& let Ok(saves_data) = postcard::to_stdvec(saves_clone.deref())
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"),
@@ -225,28 +214,30 @@ pub fn load_savestate(device: &mut device::Device, rewind: bool, rewind_frame: O
let timeout = std::time::Duration::from_secs(1);
let now = std::time::Instant::now();
loop {
if let Ok(mut pool) = device.savestate.rewind_pool.lock()
&& pool.contains_key(&rewind_frame)
if let Some(state) = device
.savestate
.rewind_pool
.blocking_lock()
.remove(&rewind_frame)
{
break pool.remove(&rewind_frame);
break Some(state);
}
if now.elapsed() > timeout {
break None;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
} else if let Ok(mut pool) = device.savestate.rewind_pool.lock()
&& let Some((_key, state)) = pool.pop_last()
} else if let Some((_key, state)) = device.savestate.rewind_pool.blocking_lock().pop_last()
{
Some(state)
} else {
None
}
} else if let savestate = std::fs::read(&device.ui.storage.paths.savestate_file_path)
&& let Ok(savestate) = &savestate
&& 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")
} 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)
{
@@ -263,7 +254,9 @@ pub fn load_savestate(device: &mut device::Device, rewind: bool, rewind_frame: O
if let Some(mut state) = state_data
&& device.rdram.size == state.device.rdram.size
{
ui::video::idle();
if device.netplay.is_none() {
ui::video::idle();
}
device.savestate.last_rewind_saved = state.device.vi.elapsed_time;
+11 -20
View File
@@ -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")]
@@ -49,7 +50,6 @@ unsafe impl Sync for Audio {}
#[derive(Default)]
pub struct Input {
pub keyboard_state: *const bool,
pub last_polled: u64,
pub controllers: [input::Controllers; 4],
}
@@ -68,10 +68,11 @@ pub struct Storage {
pub struct Video {
pub window: *mut sdl3_sys::video::SDL_Window,
pub fullscreen: bool,
pub fps_tx: Option<tokio::sync::mpsc::Sender<bool>>,
pub fps_rx: Option<tokio::sync::mpsc::Receiver<bool>>,
pub vis_tx: Option<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 {}
@@ -79,8 +80,8 @@ 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)]
@@ -120,16 +121,6 @@ pub fn sdl_hints() {
}
}
pub fn disable_auto_update_joysticks() {
unsafe {
let hint = std::ffi::CString::new("0").unwrap();
sdl3_sys::everything::SDL_SetHint(
sdl3_sys::everything::SDL_HINT_AUTO_UPDATE_JOYSTICKS,
hint.as_ptr(),
);
}
}
pub fn sdl_init(flag: sdl3_sys::init::SDL_InitFlags) {
unsafe {
if sdl3_sys::init::SDL_WasInit(flag) == 0 && !sdl3_sys::init::SDL_InitSubSystem(flag) {
@@ -191,11 +182,10 @@ 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 {
last_polled: 0,
controllers: [
input::Controllers {
game_controller: std::ptr::null_mut(),
@@ -278,6 +268,7 @@ impl Ui {
video: Video {
window: std::ptr::null_mut(),
fullscreen: false,
widescreen: false,
fps_tx: Some(fps_tx),
fps_rx: Some(fps_rx),
vis_tx: Some(vis_tx),
+192 -196
View File
@@ -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]
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) {
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,97 +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-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())?);
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:?}");
@@ -357,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:?}");
@@ -417,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> {
@@ -475,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 {
@@ -535,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:?}");
@@ -552,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);
}
}
@@ -595,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 =
+5
View File
@@ -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);
@@ -174,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 {
+6 -2
View File
@@ -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());
})
+44 -20
View File
@@ -1,34 +1,36 @@
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,
}
@@ -69,13 +71,19 @@ pub struct Cheats {
pub cheats: rustc_hash::FxHashMap<String, rustc_hash::FxHashMap<String, Option<String>>>,
}
#[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,
}
@@ -136,17 +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>(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(),
@@ -160,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,
@@ -176,8 +197,11 @@ impl Config {
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,
}
}
+96 -14
View File
@@ -1,3 +1,4 @@
use crate::device;
use crate::retroachievements;
use crate::ui;
#[cfg(target_os = "android")]
@@ -34,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 {
@@ -107,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))
@@ -174,6 +181,7 @@ 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);
@@ -187,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());
}
}
@@ -339,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);
@@ -439,13 +447,14 @@ 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();
@@ -516,7 +525,16 @@ fn about_window(app: &AppWindow) {
}
}
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);
@@ -527,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();
}
@@ -648,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;
+25 -5
View File
@@ -37,7 +37,7 @@ export component AppWindow inherits Window {
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);
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);
callback netplay_close;
@@ -61,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;
@@ -119,6 +120,7 @@ export component AppWindow inherits Window {
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;
@@ -130,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");
@@ -255,8 +272,8 @@ export component AppWindow inherits Window {
select_rom(rom_dir) => {
netplay_select_rom(rom_dir);
}
create_session(a, b, c, d, e, f) => {
netplay_create_session(a,b,c,d,e,f);
create_session(a, b, c, d, e, f, g, h) => {
netplay_create_session(a,b,c,d,e,f,g,h);
}
close => {
netplay_close();
@@ -381,7 +398,10 @@ export component AppWindow inherits Window {
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;
}
}
+4 -2
View File
@@ -6,7 +6,7 @@ import { Page } from "page.slint";
export component NetplayCreate inherits Page {
title: @tr("Netplay: Create Session");
callback select_rom(string);
callback create_session(string, string, string, string, string, string);
callback create_session(string, string, string, string, string, string, bool, bool);
callback close;
VerticalBox {
@@ -98,7 +98,9 @@ export component NetplayCreate inherits Page {
NetplayData.game_name,
NetplayData.game_hash,
NetplayData.game_cheats,
NetplayData.password);
NetplayData.password,
SettingsData.overclock_n64_cpu,
SettingsData.disable_expansion_pak);
}
}
Button {
+12 -9
View File
@@ -1,4 +1,4 @@
import { VerticalBox, HorizontalBox, TextEdit, ComboBox, GroupBox, 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";
@@ -10,6 +10,7 @@ export global NetplayWaitData {
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 {
@@ -90,7 +91,7 @@ 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;
}
@@ -102,12 +103,13 @@ export component NetplayWait inherits Page {
vertical-alignment: center;
}
ComboBox {
model: ["0", "1", "2", "3", "4", "5", "6", "7", "8"];
current-index: NetplayWaitData.input_delay;
SpinBox {
minimum: 1;
maximum: 16;
value: NetplayWaitData.input_delay;
enabled: NetplayWaitData.recommended_delay != 0;
selected => {
NetplayWaitData.input_delay = self.current-index;
edited => {
NetplayWaitData.input_delay = self.value;
}
}
@@ -121,8 +123,9 @@ export component NetplayWait inherits Page {
HorizontalBox {
alignment: center;
Button {
text: @tr("Start Game");
enabled: NetplayWaitData.players[0] == NetplayData.player_name && NetplayWaitData.can_start && NetplayWaitData.recommended_delay != 0;
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(NetplayWaitData.input_delay);
+2 -2
View File
@@ -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;
+131 -105
View File
@@ -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,6 +14,7 @@ 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;
@@ -23,117 +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("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;
Rectangle {
height: 40px; // so the combobox isn't cut off
}
}
}
+5
View File
@@ -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;
+170 -819
View File
File diff suppressed because it is too large Load Diff
+929
View File
@@ -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,
}
}
+325 -209
View File
@@ -1,4 +1,5 @@
use crate::device;
use crate::netplay::RtcIceServerConfig;
use crate::ui;
use crate::ui::gui::{AppWindow, open_uri, run_rom, save_settings};
use futures::{SinkExt, StreamExt};
@@ -24,6 +25,12 @@ enum MessageType {
ReceiveChatMessage,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ServerAddresses {
pub lobby: String,
pub game: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct NetplaySession {
password: Option<String>,
@@ -33,8 +40,9 @@ struct NetplaySession {
client_version: Option<String>,
features: Option<std::collections::HashMap<String, String>>,
players: Vec<String>,
server_address: Option<String>,
server_address: Option<ServerAddresses>,
input_delay: Option<usize>,
ice_server_config: Option<RtcIceServerConfig>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@@ -54,15 +62,20 @@ enum PingType {
struct NetplayPingMessage {
message_type: PingType,
timestamp: u128,
num_of_peers: usize,
}
fn select_rom(weak: slint::Weak<AppWindow>, rom_dir: slint::SharedString) {
fn select_rom(
weak: slint::Weak<AppWindow>,
rom_dir: slint::SharedString,
no_intro_map: std::sync::Arc<tokio::sync::Mutex<rustc_hash::FxHashMap<String, String>>>,
) {
let select_rom = ui::gui::select_rom(rom_dir);
tokio::spawn(async move {
if let Some(file) = select_rom.await {
if let Some(rom_contents) = device::get_rom_contents(&file) {
let hash = device::cart::rom::calculate_hash(&rom_contents);
let mut game_name = ui::storage::get_game_name(&rom_contents);
let mut game_name = ui::gui::get_nointro_name(&rom_contents, no_intro_map).await;
let pal = device::cart::rom::is_system_pal(&rom_contents);
let game_crc = ui::storage::get_game_crc(&rom_contents);
let cheats = ui::config::Cheats::new();
@@ -99,39 +112,177 @@ fn select_rom(weak: slint::Weak<AppWindow>, rom_dir: slint::SharedString) {
});
}
fn setup_create_window(
#[allow(clippy::too_many_arguments)]
fn setup_callbacks(
app: &AppWindow,
game_settings: ui::GameSettings,
netplay_write_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
netplay_read_receiver: tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
netplay_read_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
netplay_write_receiver: tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
close_ping_rx: tokio::sync::broadcast::Receiver<()>,
netplay_write_sender: &tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
netplay_read_receiver: &tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
netplay_read_sender: &tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
netplay_write_receiver: &tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
close_ping_tx: &tokio::sync::broadcast::Sender<()>,
close_ping_rx: &tokio::sync::broadcast::Receiver<()>,
no_intro_map: std::sync::Arc<tokio::sync::Mutex<rustc_hash::FxHashMap<String, String>>>,
) {
let weak = app.as_weak();
let netplay_write_sender_create_session = netplay_write_sender.clone();
let netplay_read_receiver_create_session = netplay_read_receiver.resubscribe();
let netplay_write_receiver_create_session = netplay_write_receiver.resubscribe();
let netplay_read_sender_create_session = netplay_read_sender.clone();
let close_ping_tx_create_session = close_ping_tx.clone();
let close_ping_rx_create_session = close_ping_rx.resubscribe();
app.on_netplay_create_session(
move |session_name, player_name, game_name, game_hash, game_cheats, password| {
move |session_name,
player_name,
game_name,
game_hash,
game_cheats,
password,
overclock,
disable_expansion_pak| {
close_connections(
&netplay_write_sender_create_session,
&netplay_read_sender_create_session,
&close_ping_tx_create_session,
);
manage_websocket(
netplay_read_sender.clone(),
netplay_write_receiver.resubscribe(),
netplay_read_sender_create_session.clone(),
netplay_write_receiver_create_session.resubscribe(),
);
create_session(
netplay_write_sender.clone(),
netplay_read_receiver.resubscribe(),
close_ping_rx.resubscribe(),
netplay_write_sender_create_session.clone(),
netplay_read_receiver_create_session.resubscribe(),
close_ping_rx_create_session.resubscribe(),
session_name.to_string(),
player_name.to_string(),
game_name.to_string(),
game_hash.to_string(),
game_cheats.to_string(),
password.to_string(),
game_settings.clone(),
overclock,
disable_expansion_pak,
weak.clone(),
);
},
);
let write_sender_chat_message = netplay_write_sender.clone();
app.on_netplay_send_chat_message(move |message| {
let send_chat = NetplayLobbyMessage {
message_type: MessageType::SendChatMessage,
sessions: std::collections::HashMap::new(),
message: Some(message.into()),
};
write_sender_chat_message.send(Some(send_chat)).unwrap();
});
let write_sender_begin_game = netplay_write_sender.clone();
app.on_netplay_begin_game(move |chosen_input_delay| {
let begin_game = NetplayLobbyMessage {
message_type: MessageType::RequestBeginGame,
sessions: std::collections::HashMap::new(),
message: Some(chosen_input_delay.to_string()),
};
write_sender_begin_game.send(Some(begin_game)).unwrap();
});
let write_sender_refresh_sessions = netplay_write_sender.clone();
app.on_netplay_refresh_sessions(move || {
update_sessions(write_sender_refresh_sessions.clone());
});
let write_sender_join_session = netplay_write_sender.clone();
app.on_netplay_join_session(move |session_name, player_name, game_hash, password| {
join_session(
write_sender_join_session.clone(),
session_name.to_string(),
player_name.to_string(),
game_hash.to_string(),
password.to_string(),
);
});
let weak_app = app.as_weak();
app.on_create_session_button_clicked(move || {
weak_app
.upgrade_in_event_loop(move |handle| {
save_settings(&handle);
setup_create_window(&handle);
})
.unwrap();
});
let weak_app = app.as_weak();
let netplay_write_sender_join_session_button = netplay_write_sender.clone();
let netplay_read_receiver_join_session_button = netplay_read_receiver.resubscribe();
let netplay_read_sender_join_session_button = netplay_read_sender.clone();
let netplay_write_receiver_join_session_button = netplay_write_receiver.resubscribe();
let close_ping_tx_join_session_button = close_ping_tx.clone();
let close_ping_rx_join_session_button = close_ping_rx.resubscribe();
app.on_join_session_button_clicked(move || {
let netplay_write_sender = netplay_write_sender_join_session_button.clone();
let netplay_read_receiver = netplay_read_receiver_join_session_button.resubscribe();
let netplay_read_sender = netplay_read_sender_join_session_button.clone();
let netplay_write_receiver = netplay_write_receiver_join_session_button.resubscribe();
let close_ping_tx = close_ping_tx_join_session_button.clone();
let close_ping_rx = close_ping_rx_join_session_button.resubscribe();
weak_app
.upgrade_in_event_loop(move |handle| {
save_settings(&handle);
setup_join_window(
&handle,
netplay_write_sender,
netplay_read_receiver,
netplay_read_sender,
netplay_write_receiver,
close_ping_tx,
close_ping_rx,
);
})
.unwrap();
});
let weak_app = app.as_weak();
app.on_netplay_select_rom(move |rom_dir| {
select_rom(weak_app.clone(), rom_dir, no_intro_map.clone());
});
let weak_app = app.as_weak();
let netplay_write_sender = netplay_write_sender.clone();
let netplay_read_sender = netplay_read_sender.clone();
let close_ping_tx = close_ping_tx.clone();
app.on_netplay_close(move || {
weak_app
.upgrade_in_event_loop(move |handle| {
handle.set_show_netplay_wait_session(false);
handle.set_show_netplay_create_session(false);
handle.set_show_netplay_join_session(false);
handle.invoke_clear_netplay_data();
handle.invoke_close_message();
})
.unwrap();
close_connections(&netplay_write_sender, &netplay_read_sender, &close_ping_tx);
});
app.on_netplay_discord_button_clicked(move || {
open_uri("https://discord.gg/JyW6ZgBUyS");
});
app.on_netplay_feedback_button_clicked(move || {
open_uri("https://github.com/gopher64/gopher64/discussions/453");
});
}
fn close_connections(
netplay_write_sender: &tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
netplay_read_sender: &tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
close_ping_tx: &tokio::sync::broadcast::Sender<()>,
) {
let _ = netplay_write_sender.send(None); // close current websocket if any
let _ = netplay_read_sender.send(None); // close current receiver if any
let _ = close_ping_tx.send(()); // close ping
}
fn setup_create_window(app: &AppWindow) {
app.set_show_netplay_create_session(true);
}
@@ -139,67 +290,86 @@ fn manage_websocket(
netplay_read_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
mut netplay_write_receiver: tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
) {
let mut request = "wss://netplay.gopher64.com".into_client_request().unwrap();
let mut request = std::env::var("NETPLAY_SERVER_URL")
.unwrap_or("wss://netplay.gopher64.com".to_string())
.into_client_request()
.unwrap();
request
.headers_mut()
.insert("Authorization", env!("NETPLAY_ID").parse().unwrap());
tokio::spawn(async move {
if let Ok(Ok((socket, _response))) = tokio::time::timeout(
std::time::Duration::from_secs(2),
match tokio::time::timeout(
std::time::Duration::from_secs(3),
tokio_tungstenite::connect_async(request),
)
.await
{
let (mut write, mut read) = socket.split();
Ok(Ok((socket, _response))) => {
let (mut write, mut read) = socket.split();
tokio::spawn(async move {
while let Some(Ok(response)) = read.next().await {
let decoded_response =
postcard::from_bytes::<NetplayLobbyMessage>(&response.into_data());
match decoded_response {
Ok(message) => {
let _ = netplay_read_sender.send(Some(message));
}
Err(e) => {
eprintln!("Failed to parse message: {}", e);
tokio::spawn(async move {
while let Some(Ok(response)) = read.next().await {
match response {
Message::Binary(data) => {
let decoded_response =
postcard::from_bytes::<NetplayLobbyMessage>(&data);
match decoded_response {
Ok(message) => {
let _ = netplay_read_sender.send(Some(message));
}
Err(e) => {
eprintln!("Failed to parse message: {}", e);
}
}
}
Message::Close(_) => {
return;
}
_ => {}
}
}
}
});
tokio::spawn(async move {
loop {
match netplay_write_receiver.recv().await {
Ok(Some(response)) => {
if let Err(e) = write
.send(Message::Binary(Bytes::from(
postcard::to_stdvec(&response).unwrap(),
)))
.await
{
eprintln!("Failed to send message: {}", e);
});
tokio::spawn(async move {
loop {
match netplay_write_receiver.recv().await {
Ok(Some(response)) => {
if let Err(e) = write
.send(Message::Binary(Bytes::from(
postcard::to_stdvec(&response).unwrap(),
)))
.await
{
eprintln!("Failed to send message: {}", e);
}
}
Ok(None) => {
break;
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
panic!("netplay_write_receiver lagged");
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
break; // exit the loop if the receiver is closed
}
}
Ok(None) => {
break;
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
panic!("netplay_write_receiver lagged");
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
break; // exit the loop if the receiver is closed
}
}
}
if let Err(e) = write
.send(Message::Close(Some(CloseFrame {
code: CloseCode::Normal,
reason: Utf8Bytes::from(""),
})))
.await
{
eprintln!("Failed to send close message: {}", e);
}
});
if let Err(e) = write
.send(Message::Close(Some(CloseFrame {
code: CloseCode::Normal,
reason: Utf8Bytes::from(""),
})))
.await
{
eprintln!("Failed to send close message: {}", e);
}
});
}
Ok(Err(e)) => {
eprintln!("Failed to connect to netplay: {}", e);
}
Err(e) => {
eprintln!("Failed to connect to netplay: {}", e);
}
}
});
}
@@ -227,7 +397,8 @@ fn create_session(
game_hash: String,
game_cheats: String,
password: String,
game_settings: ui::GameSettings,
overclock: bool,
disable_expansion_pak: bool,
weak_app: slint::Weak<AppWindow>,
) {
tokio::spawn(async move {
@@ -237,10 +408,10 @@ fn create_session(
features.insert("cheats".to_string(), game_cheats);
}
features.insert("overclock".to_string(), game_settings.overclock.to_string());
features.insert("overclock".to_string(), overclock.to_string());
features.insert(
"disable_expansion_pak".to_string(),
game_settings.disable_expansion_pak.to_string(),
disable_expansion_pak.to_string(),
);
let session = NetplaySession {
@@ -257,6 +428,7 @@ fn create_session(
players: vec![player_name],
server_address: None,
input_delay: None,
ice_server_config: None,
};
let create_session = NetplayLobbyMessage {
message_type: MessageType::RequestCreateSession,
@@ -267,7 +439,7 @@ fn create_session(
netplay_write_sender.send(Some(create_session)).unwrap();
match tokio::time::timeout(
std::time::Duration::from_secs(2),
std::time::Duration::from_secs(3),
netplay_read_receiver.recv(),
)
.await
@@ -304,6 +476,7 @@ fn create_session(
close_ping_rx,
session.server_address.as_ref().unwrap().clone(),
session_name.into(),
session.ice_server_config.clone(),
session.game_name.as_ref().unwrap().into(),
handle.get_netplay_rom_path(),
ui::GameSettings {
@@ -331,11 +504,14 @@ fn create_session(
Ok(Err(err)) => {
panic!("netplay_read_receiver error: {err}");
}
Err(_) => {
Err(e) => {
weak_app
.upgrade_in_event_loop(move |handle| {
handle.set_netplay_pending_session(false);
handle.invoke_show_message("Server did not respond".into(), true);
handle.invoke_show_message(
format!("Server did not respond: {e}").into(),
true,
);
})
.unwrap();
}
@@ -364,6 +540,7 @@ fn join_session(
players: vec![player_name],
server_address: None,
input_delay: None,
ice_server_config: None,
};
let join_session = NetplayLobbyMessage {
message_type: MessageType::RequestJoinSession,
@@ -376,12 +553,22 @@ fn join_session(
fn update_ping(
server_addr: String,
ice_server_config: Option<RtcIceServerConfig>,
mut close_ping_rx: tokio::sync::broadcast::Receiver<()>,
weak_app: slint::Weak<AppWindow>,
) {
let (mut socket, loop_fut) = matchbox_socket::WebRtcSocketBuilder::new(server_addr)
.add_unreliable_channel()
.build();
let mut builder =
matchbox_socket::WebRtcSocketBuilder::new(server_addr).add_unreliable_channel();
if let Some(ice_server_config) = ice_server_config {
builder = builder.ice_server(matchbox_socket::RtcIceServerConfig {
urls: ice_server_config.urls,
username: ice_server_config.username,
credential: ice_server_config.credential,
});
} else {
eprintln!("Using default ICE config");
}
let (mut socket, loop_fut) = builder.build();
tokio::spawn(async move {
if let Err(e) = loop_fut.await {
@@ -392,9 +579,16 @@ fn update_ping(
let (mut write, mut read) = channel.split();
let mut write_clone = write.clone();
let mut close_ping_rx_clone = close_ping_rx.resubscribe();
let weak_app_clone = weak_app.clone();
tokio::spawn(async move {
loop {
socket.update_peers();
let peers_count = socket.connected_peers().count();
weak_app_clone
.upgrade_in_event_loop(move |handle| {
handle.set_netplay_connected_peers_count(peers_count as i32);
})
.unwrap();
for peer in socket.connected_peers() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -403,6 +597,7 @@ fn update_ping(
let ping_message = NetplayPingMessage {
message_type: PingType::Ping,
timestamp: now,
num_of_peers: peers_count,
};
let data = postcard::to_stdvec(&ping_message).unwrap();
let _ = write.send((peer, data.into())).await;
@@ -450,6 +645,7 @@ fn update_ping(
let pong_message = NetplayPingMessage {
message_type: PingType::Pong,
timestamp: message.timestamp,
num_of_peers: message.num_of_peers,
};
let data = postcard::to_stdvec(&pong_message).unwrap();
let _ = write_clone.send((peer, data.into())).await;
@@ -459,13 +655,13 @@ fn update_ping(
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis();
let ping = (now - message.timestamp) / 2; // calculate one-way latency
let ping = (now - message.timestamp) as f64 / 2.0;
pings.push(ping);
if pings.len() > 12 {
// once we have at least 12 samples, remove the highest 2 and return the next highest
pings.sort();
pings.truncate(pings.len() - 2);
let ping = *pings.last().unwrap();
if pings.len() > message.num_of_peers * 4 {
// once we have enough samples, average the 3 highest values
pings.sort_by(f64::total_cmp);
pings.pop(); // Remove highest value to avoid outliers
let ping_avg = pings.iter().rev().take(3).sum::<f64>() / 3.0;
pings.clear();
weak_app
.upgrade_in_event_loop(move |handle| {
@@ -474,8 +670,9 @@ fn update_ping(
} else {
60.0
};
let latency_frames = (ping_avg / (1000.0 / refresh_rate)).ceil() as i32;
let recommendation =
(ping as f64 / (1000.0 / refresh_rate)) as i32 + 1;
(latency_frames + 1).min(16);
if handle.get_netplay_recommended_delay() == 0 {
handle.set_netplay_input_delay(recommendation);
@@ -496,19 +693,42 @@ fn update_ping(
});
}
fn update_ice_config(ice_server_config: Option<RtcIceServerConfig>) {
tokio::spawn(async move {
let ice_config_path = ui::get_dirs().cache_dir.join("ice_config.json");
if let Some(ice_server_config) = ice_server_config {
tokio::fs::write(
&ice_config_path,
serde_json::to_vec(&ice_server_config).unwrap(),
)
.await
.unwrap();
} else {
let _ = tokio::fs::remove_file(&ice_config_path).await;
}
});
}
#[allow(clippy::too_many_arguments)]
fn setup_wait_window(
netplay_write_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
mut netplay_read_receiver: tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
close_ping_rx: tokio::sync::broadcast::Receiver<()>,
server_addr: String,
server_addresses: ServerAddresses,
session_name: slint::SharedString,
ice_server_config: Option<RtcIceServerConfig>,
game_name: slint::SharedString,
rom_path: slint::SharedString,
game_settings: ui::GameSettings,
app: &AppWindow,
) {
update_ping(server_addr.clone(), close_ping_rx, app.as_weak());
update_ice_config(ice_server_config.clone());
update_ping(
server_addresses.lobby.clone(),
ice_server_config,
close_ping_rx,
app.as_weak(),
);
app.set_netplay_session_name(session_name);
app.set_netplay_game_name(game_name);
@@ -522,26 +742,6 @@ fn setup_wait_window(
};
netplay_write_sender.send(Some(request_update)).unwrap();
let sender = netplay_write_sender.clone();
app.on_netplay_send_chat_message(move |message| {
let send_chat = NetplayLobbyMessage {
message_type: MessageType::SendChatMessage,
sessions: std::collections::HashMap::new(),
message: Some(message.into()),
};
sender.send(Some(send_chat)).unwrap();
});
let sender = netplay_write_sender.clone();
app.on_netplay_begin_game(move |chosen_input_delay| {
let begin_game = NetplayLobbyMessage {
message_type: MessageType::RequestBeginGame,
sessions: std::collections::HashMap::new(),
message: Some(chosen_input_delay.to_string()),
};
sender.send(Some(begin_game)).unwrap();
});
let weak_app = app.as_weak();
tokio::spawn(async move {
loop {
@@ -602,7 +802,7 @@ fn setup_wait_window(
load_savestate_slot: None,
},
Some(ui::gui::NetplayDevice {
server_addr,
server_addr: server_addresses.game.clone(),
player_number,
number_of_players: players.row_count(),
input_delay,
@@ -652,8 +852,10 @@ fn setup_join_window(
netplay_read_receiver: tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
netplay_read_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
netplay_write_receiver: tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
close_ping_tx: tokio::sync::broadcast::Sender<()>,
close_ping_rx: tokio::sync::broadcast::Receiver<()>,
) {
close_connections(&netplay_write_sender, &netplay_read_sender, &close_ping_tx);
manage_websocket(
netplay_read_sender.clone(),
netplay_write_receiver.resubscribe(),
@@ -661,22 +863,6 @@ fn setup_join_window(
app.set_netplay_pending_refresh(true);
let write_sender = netplay_write_sender.clone();
app.on_netplay_refresh_sessions(move || {
update_sessions(write_sender.clone());
});
let write_sender = netplay_write_sender.clone();
app.on_netplay_join_session(move |session_name, player_name, game_hash, password| {
join_session(
write_sender.clone(),
session_name.to_string(),
player_name.to_string(),
game_hash.to_string(),
password.to_string(),
);
});
let weak = app.as_weak();
let mut read_receiver = netplay_read_receiver.resubscribe();
let write_sender = netplay_write_sender.clone();
@@ -717,6 +903,7 @@ fn setup_join_window(
close_ping_rx.resubscribe(),
session.server_address.as_ref().unwrap().clone(),
session_name.into(),
session.ice_server_config.clone(),
session.game_name.as_ref().unwrap().into(),
handle.get_netplay_rom_path(),
ui::GameSettings {
@@ -793,10 +980,13 @@ fn setup_join_window(
Ok(None) => {
break;
}
Err(_) => {
Err(e) => {
weak.upgrade_in_event_loop(move |handle| {
handle.set_netplay_pending_session(false);
handle.invoke_show_message("Server did not respond".into(), true);
handle.invoke_show_message(
format!("Server did not respond: {e}").into(),
true,
);
})
.unwrap();
}
@@ -808,7 +998,10 @@ fn setup_join_window(
app.invoke_netplay_refresh_sessions();
}
pub fn netplay_window(app: &AppWindow) {
pub fn netplay_window(
app: &AppWindow,
no_intro_map: std::sync::Arc<tokio::sync::Mutex<rustc_hash::FxHashMap<String, String>>>,
) {
let (netplay_read_sender, netplay_read_receiver): (
tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
@@ -824,91 +1017,14 @@ pub fn netplay_window(app: &AppWindow) {
tokio::sync::broadcast::Receiver<()>,
) = tokio::sync::broadcast::channel(5);
let weak_app = app.as_weak();
let write_sender_create = netplay_write_sender.clone();
let read_receiver_create = netplay_read_receiver.resubscribe();
let read_sender_create = netplay_read_sender.clone();
let write_receiver_create = netplay_write_receiver.resubscribe();
let close_ping_create = close_ping_rx.resubscribe();
app.on_create_session_button_clicked(move || {
let write_sender = write_sender_create.clone();
let read_receiver = read_receiver_create.resubscribe();
let read_sender = read_sender_create.clone();
let write_receiver = write_receiver_create.resubscribe();
let close_ping_rx = close_ping_create.resubscribe();
weak_app
.upgrade_in_event_loop(move |handle| {
save_settings(&handle);
setup_create_window(
&handle,
ui::GameSettings {
overclock: handle.get_overclock_n64_cpu(),
disable_expansion_pak: handle.get_disable_expansion_pak(),
cheats: rustc_hash::FxHashMap::default(), // not used here
load_savestate_slot: None,
},
write_sender,
read_receiver,
read_sender,
write_receiver,
close_ping_rx,
);
})
.unwrap();
});
let weak_app = app.as_weak();
let write_sender_join = netplay_write_sender.clone();
let read_sender_join = netplay_read_sender.clone();
let close_ping_join = close_ping_rx.resubscribe();
app.on_join_session_button_clicked(move || {
let write_sender = write_sender_join.clone();
let read_receiver = netplay_read_receiver.resubscribe();
let read_sender = read_sender_join.clone();
let write_receiver = netplay_write_receiver.resubscribe();
let close_ping_rx = close_ping_join.resubscribe();
weak_app
.upgrade_in_event_loop(move |handle| {
save_settings(&handle);
setup_join_window(
&handle,
write_sender,
read_receiver,
read_sender,
write_receiver,
close_ping_rx,
);
})
.unwrap();
});
let weak_app = app.as_weak();
app.on_netplay_select_rom(move |rom_dir| {
select_rom(weak_app.clone(), rom_dir);
});
let weak_app = app.as_weak();
let write_sender = netplay_write_sender.clone();
let read_sender = netplay_read_sender.clone();
app.on_netplay_close(move || {
weak_app
.upgrade_in_event_loop(move |handle| {
handle.set_show_netplay_wait_session(false);
handle.set_show_netplay_create_session(false);
handle.set_show_netplay_join_session(false);
handle.invoke_clear_netplay_data();
handle.invoke_close_message();
})
.unwrap();
let _ = write_sender.send(None); // close current websocket if any
let _ = read_sender.send(None); // close current receiver if any
let _ = close_ping_tx.send(()); // close ping
});
app.on_netplay_discord_button_clicked(move || {
open_uri("https://discord.gg/JyW6ZgBUyS");
});
app.on_netplay_feedback_button_clicked(move || {
open_uri("https://github.com/gopher64/gopher64/discussions/453");
});
setup_callbacks(
app,
&netplay_write_sender,
&netplay_read_receiver,
&netplay_read_sender,
&netplay_write_receiver,
&close_ping_tx,
&close_ping_rx,
no_intro_map,
);
}
+1 -1
View File
@@ -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() {
+78 -79
View File
@@ -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,7 +168,7 @@ pub fn init() -> (
Some(handle),
ui::Usb {
usb_tx: Some(usb_tx_clone),
cart_rx: Some(cart_rx_clone),
cart_rx: Some(cart_rx),
},
)
}
@@ -193,6 +192,6 @@ pub fn close(
}
}
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();
}
+10 -46
View File
@@ -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");
@@ -170,7 +170,7 @@ pub fn 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 {
@@ -182,16 +182,14 @@ pub fn load_state(device: &mut device::Device, rdp_state: *const u8) {
pub fn pause_loop(ui: &mut ui::Ui, frame_time: f64) {
let mut paused = true;
let mut frame_advance = false;
let mut pause_counter = 0;
while paused && !frame_advance {
std::thread::sleep(std::time::Duration::from_secs_f64(frame_time));
ui::input::get(ui, 0, pause_counter); // to gather hotkey input
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;
frame_advance = callback.frame_advance;
pause_counter += 1;
}
}
@@ -247,7 +245,7 @@ pub fn check_callback(device: &mut device::Device) -> (bool, bool) {
if let Some(netplay) = &mut device.netplay
&& netplay.player_number == 0
{
if callback.decrease_input_delay && netplay.input_delay > 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);
@@ -276,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
View File
@@ -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();
});