Compare commits

...

59 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
49 changed files with 25516 additions and 2749 deletions
+4 -4
View File
@@ -34,7 +34,7 @@ jobs:
arch: linux-aarch64 arch: linux-aarch64
llvm: ARM64 llvm: ARM64
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
with: with:
submodules: recursive submodules: recursive
fetch-depth: 0 fetch-depth: 0
@@ -95,7 +95,7 @@ jobs:
arch: windows-aarch64 arch: windows-aarch64
llvm: woa64 llvm: woa64
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
with: with:
submodules: recursive submodules: recursive
fetch-depth: 0 fetch-depth: 0
@@ -163,7 +163,7 @@ jobs:
arch: macos-aarch64 arch: macos-aarch64
llvm: ARM64 llvm: ARM64
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
with: with:
submodules: recursive submodules: recursive
fetch-depth: 0 fetch-depth: 0
@@ -229,7 +229,7 @@ jobs:
platform: 33 platform: 33
ndk: 29.0.14206865 ndk: 29.0.14206865
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
with: with:
submodules: recursive submodules: recursive
fetch-depth: 0 fetch-depth: 0
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
lint-linux: lint-linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
with: with:
submodules: recursive submodules: recursive
fetch-depth: 0 fetch-depth: 0
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
checks: write checks: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
with: with:
submodules: recursive submodules: recursive
- uses: rustsec/audit-check@v2 - uses: rustsec/audit-check@v2
Generated
+526 -802
View File
File diff suppressed because it is too large Load Diff
+5 -8
View File
@@ -1,8 +1,8 @@
[package] [package]
name = "gopher64" name = "gopher64"
version = "1.1.26" version = "1.1.34"
edition = "2024" edition = "2024"
rust-version = "1.96.0" rust-version = "1.97.0"
[features] [features]
default = ["gui"] default = ["gui"]
@@ -19,14 +19,14 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
serde-big-array = "0.5" serde-big-array = "0.5"
sha2 = "0.11" 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" spin_sleep = "1.3"
encoding_rs = "0.8" encoding_rs = "0.8"
mimalloc = "0.1" mimalloc = "0.1"
regex = "1.11" regex = "1.11"
tokio-tungstenite = { version = "0.29", features = ["native-tls-vendored"] } tokio-tungstenite = { version = "0.29", features = ["native-tls-vendored"] }
matchbox_socket = "0.14" 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" rustc-hash = "2.1"
futures = "0.3" futures = "0.3"
rand = { version = "0.10", features = ["serde"] } rand = { version = "0.10", features = ["serde"] }
@@ -36,6 +36,7 @@ postcard = { version = "1.1", features = ["use-std"] }
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
reqwest = { version = "0.13", default-features = false, features = ["native-tls-vendored", "charset", "http2", "system-proxy", "json", "query"] } reqwest = { version = "0.13", default-features = false, features = ["native-tls-vendored", "charset", "http2", "system-proxy", "json", "query"] }
discord-rich-presence = "1.1" discord-rich-presence = "1.1"
quick-xml = "0.41"
[target.'cfg(not(target_os = "android"))'.dependencies] [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 } 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 } open = { version = "5.3", optional = true }
sdl3-sys = { version = "0.6", features = ["build-from-source-static"] } 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-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] [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 } slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "backend-android-activity-06", "accessibility"], optional = true }
jni = "0.22" jni = "0.22"
sdl3-sys = { version = "0.6", features = ["build-from-source"] } 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-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] [build-dependencies]
winresource = "0.1" winresource = "0.1"
@@ -61,7 +60,6 @@ slint-build = { version = "1.12", optional = true }
cc = { version = "1.2", features = ["parallel"] } cc = { version = "1.2", features = ["parallel"] }
[profile.release] [profile.release]
panic = "abort"
lto = "thin" lto = "thin"
strip = true strip = true
@@ -69,7 +67,6 @@ strip = true
inherits = "release" inherits = "release"
debug = true debug = true
strip = false strip = false
panic = "unwind"
[package.metadata.bundle] [package.metadata.bundle]
name = "Gopher64" name = "Gopher64"
+6 -2
View File
@@ -41,7 +41,9 @@ Keys are mapped according to [these defaults](https://github.com/gopher64/gopher
## netplay ## 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 ## portable mode
@@ -66,7 +68,9 @@ flatpak run --filesystem=host:ro io.github.gopher64.gopher64 /path/to/rom.z64
## contributing ## 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 ## license
@@ -24,7 +24,6 @@ class N64Activity : SDLActivity() {
override fun getLibraries(): Array<String> = arrayOf( override fun getLibraries(): Array<String> = arrayOf(
"SDL3", "SDL3",
"SDL3_ttf", "SDL3_ttf",
"SDL3_image",
"gopher64" "gopher64"
) )
+56 -26
View File
@@ -2,7 +2,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gopher64\n" "Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-13 07:48+0000\n" "POT-Creation-Date: 2026-06-30 09:43+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n" "PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: German\n" "Language-Team: German\n"
@@ -47,37 +47,37 @@ msgctxt "About"
msgid "Donate (via GitHub)" msgid "Donate (via GitHub)"
msgstr "Spenden (über GitHub)" msgstr "Spenden (über GitHub)"
#: src/ui/gui/appwindow.slint:148 #: src/ui/gui/appwindow.slint:164
msgctxt "Menu" msgctxt "Menu"
msgid "Local Game" msgid "Local Game"
msgstr "Lokales Spiel" msgstr "Lokales Spiel"
#: src/ui/gui/appwindow.slint:152 #: src/ui/gui/appwindow.slint:168
msgctxt "Menu" msgctxt "Menu"
msgid "Controller Config" msgid "Controller Config"
msgstr "Controller-Konfiguration" msgstr "Controller-Konfiguration"
#: src/ui/gui/appwindow.slint:156 #: src/ui/gui/appwindow.slint:172
msgctxt "Menu" msgctxt "Menu"
msgid "Netplay" msgid "Netplay"
msgstr "Netplay" msgstr "Netplay"
#: src/ui/gui/appwindow.slint:160 #: src/ui/gui/appwindow.slint:176
msgctxt "Menu" msgctxt "Menu"
msgid "Cheats" msgid "Cheats"
msgstr "Cheats" msgstr "Cheats"
#: src/ui/gui/appwindow.slint:164 #: src/ui/gui/appwindow.slint:180
msgctxt "Menu" msgctxt "Menu"
msgid "RetroAchievements" msgid "RetroAchievements"
msgstr "RetroAchievements" msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:168 #: src/ui/gui/appwindow.slint:184
msgctxt "Menu" msgctxt "Menu"
msgid "Settings" msgid "Settings"
msgstr "Einstellungen" msgstr "Einstellungen"
#: src/ui/gui/appwindow.slint:172 #: src/ui/gui/appwindow.slint:188
msgctxt "Menu" msgctxt "Menu"
msgid "About" msgid "About"
msgstr "Über" msgstr "Über"
@@ -240,7 +240,7 @@ msgctxt "NetplayCreate"
msgid "Create Session" msgid "Create Session"
msgstr "Sitzung erstellen" msgstr "Sitzung erstellen"
#: src/ui/gui/netplay_create.slint:105 #: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate" msgctxt "NetplayCreate"
msgid "Cancel" msgid "Cancel"
msgstr "Abbrechen" msgstr "Abbrechen"
@@ -406,22 +406,22 @@ msgctxt "NetplayWait"
msgid "Local Input Delay:" msgid "Local Input Delay:"
msgstr "Lokale Eingabeverzögerung:" msgstr "Lokale Eingabeverzögerung:"
#: src/ui/gui/netplay_wait.slint:117 #: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Recommended: " msgid "Recommended: "
msgstr "Empfohlen: " msgstr "Empfohlen: "
#: src/ui/gui/netplay_wait.slint:126 #: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Start Game" msgid "Start Game"
msgstr "Spiel starten" msgstr "Spiel starten"
#: src/ui/gui/netplay_wait.slint:126 #: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Waiting for P2P mesh " msgid "Waiting for P2P mesh "
msgstr "Warten auf P2P-Mesh " msgstr "Warten auf P2P-Mesh "
#: src/ui/gui/netplay_wait.slint:134 #: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Cancel" msgid "Cancel"
msgstr "Abbrechen" msgstr "Abbrechen"
@@ -486,71 +486,101 @@ msgctxt "RetroAchievements"
msgid "View Supported Games" msgid "View Supported Games"
msgstr "Unterstützte Spiele anzeigen" msgstr "Unterstützte Spiele anzeigen"
#: src/ui/gui/settings_page.slint:24 #: src/ui/gui/settings_page.slint:25
msgctxt "Settings" msgctxt "Settings"
msgid "Settings" msgid "Settings"
msgstr "Einstellungen" 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" msgctxt "Settings"
msgid "Resolution:" msgid "Resolution:"
msgstr "Auflösung:" msgstr "Auflösung:"
#: src/ui/gui/settings_page.slint:49 #: src/ui/gui/settings_page.slint:51
msgctxt "Settings" msgctxt "Settings"
msgid "SSAA downscaling" msgid "SSAA downscaling"
msgstr "SSAA-Herunterskalierung" msgstr "SSAA-Herunterskalierung"
#: src/ui/gui/settings_page.slint:57 #: src/ui/gui/settings_page.slint:59
msgctxt "Settings" msgctxt "Settings"
msgid "Integer Scaling" msgid "Integer Scaling"
msgstr "Pixelgenaue Skalierung" msgstr "Pixelgenaue Skalierung"
#: src/ui/gui/settings_page.slint:65 #: src/ui/gui/settings_page.slint:67
msgctxt "Settings" msgctxt "Settings"
msgid "Fullscreen" msgid "Fullscreen"
msgstr "Vollbild" msgstr "Vollbild"
#: src/ui/gui/settings_page.slint:73 #: src/ui/gui/settings_page.slint:75
msgctxt "Settings" msgctxt "Settings"
msgid "Widescreen (stretch)" msgid "Widescreen (stretch)"
msgstr "16:9 (gestreckt)" msgstr "16:9 (gestreckt)"
#: src/ui/gui/settings_page.slint:81 #: src/ui/gui/settings_page.slint:83
msgctxt "Settings" msgctxt "Settings"
msgid "VSync" msgid "VSync"
msgstr "VSync" msgstr "VSync"
#: src/ui/gui/settings_page.slint:89 #: src/ui/gui/settings_page.slint:91
msgctxt "Settings" msgctxt "Settings"
msgid "Apply CRT shader" msgid "Apply CRT shader"
msgstr "CRT-Shader anwenden" msgstr "CRT-Shader anwenden"
#: src/ui/gui/settings_page.slint:101 #: src/ui/gui/settings_page.slint:103
msgctxt "Settings" msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)" msgid "Overclock N64 CPU (may cause bugs)"
msgstr "N64-CPU übertakten (kann Fehler verursachen)" msgstr "N64-CPU übertakten (kann Fehler verursachen)"
#: src/ui/gui/settings_page.slint:109 #: src/ui/gui/settings_page.slint:111
msgctxt "Settings" msgctxt "Settings"
msgid "Disable Expansion Pak" msgid "Disable Expansion Pak"
msgstr "Expansion Pak deaktivieren" msgstr "Expansion Pak deaktivieren"
#: src/ui/gui/settings_page.slint:117 #: src/ui/gui/settings_page.slint:119
msgctxt "Settings" msgctxt "Settings"
msgid "Enable Rewind" msgid "Enable Rewind"
msgstr "Rewind aktivieren" msgstr "Rewind aktivieren"
#: src/ui/gui/settings_page.slint:125 #: src/ui/gui/settings_page.slint:127
msgctxt "Settings" msgctxt "Settings"
msgid "Emulate UNFLoader" msgid "Emulate UNFLoader"
msgstr "UNFLoader emulieren" msgstr "UNFLoader emulieren"
#: src/ui/gui/settings_page.slint:133 #: src/ui/gui/settings_page.slint:135
msgctxt "Settings" msgctxt "Settings"
msgid "Emulate VRU" msgid "Emulate VRU"
msgstr "VRU emulieren" 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 #: src/ui/gui/vru_dialog.slint:7
msgctxt "VruDialog" msgctxt "VruDialog"
msgid "What Would You Like To Say?" msgid "What Would You Like To Say?"
+58 -28
View File
@@ -2,7 +2,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gopher64\n" "Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-13 07:48+0000\n" "POT-Creation-Date: 2026-06-30 09:43+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n" "PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: Gemini\n" "Last-Translator: Gemini\n"
"Language-Team: Spanish (Spain)\n" "Language-Team: Spanish (Spain)\n"
@@ -47,37 +47,37 @@ msgctxt "About"
msgid "Donate (via GitHub)" msgid "Donate (via GitHub)"
msgstr "Donar (a través de GitHub)" msgstr "Donar (a través de GitHub)"
#: src/ui/gui/appwindow.slint:148 #: src/ui/gui/appwindow.slint:164
msgctxt "Menu" msgctxt "Menu"
msgid "Local Game" msgid "Local Game"
msgstr "Juego local" msgstr "Juego local"
#: src/ui/gui/appwindow.slint:152 #: src/ui/gui/appwindow.slint:168
msgctxt "Menu" msgctxt "Menu"
msgid "Controller Config" msgid "Controller Config"
msgstr "Config. del mando" msgstr "Config. del mando"
#: src/ui/gui/appwindow.slint:156 #: src/ui/gui/appwindow.slint:172
msgctxt "Menu" msgctxt "Menu"
msgid "Netplay" msgid "Netplay"
msgstr "Netplay" msgstr "Netplay"
#: src/ui/gui/appwindow.slint:160 #: src/ui/gui/appwindow.slint:176
msgctxt "Menu" msgctxt "Menu"
msgid "Cheats" msgid "Cheats"
msgstr "Trucos" msgstr "Trucos"
#: src/ui/gui/appwindow.slint:164 #: src/ui/gui/appwindow.slint:180
msgctxt "Menu" msgctxt "Menu"
msgid "RetroAchievements" msgid "RetroAchievements"
msgstr "RetroAchievements" msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:168 #: src/ui/gui/appwindow.slint:184
msgctxt "Menu" msgctxt "Menu"
msgid "Settings" msgid "Settings"
msgstr "Ajustes" msgstr "Ajustes"
#: src/ui/gui/appwindow.slint:172 #: src/ui/gui/appwindow.slint:188
msgctxt "Menu" msgctxt "Menu"
msgid "About" msgid "About"
msgstr "Acerca de" msgstr "Acerca de"
@@ -240,7 +240,7 @@ msgctxt "NetplayCreate"
msgid "Create Session" msgid "Create Session"
msgstr "Crear sesión" msgstr "Crear sesión"
#: src/ui/gui/netplay_create.slint:105 #: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate" msgctxt "NetplayCreate"
msgid "Cancel" msgid "Cancel"
msgstr "Cancelar" msgstr "Cancelar"
@@ -399,30 +399,30 @@ msgid ""
msgstr "" msgstr ""
"Configurar un retardo de entrada demasiado bajo hará que el juego se " "Configurar un retardo de entrada demasiado bajo hará que el juego se "
"ralentice.\n" "ralentice.\n"
"Puedes reducir el retardo de entrada en el juego pulsando Alt+[ y " "Puedes reducir el retardo de entrada en el juego pulsando Alt+[ y aumentarlo "
"aumentarlo con Alt+]." "con Alt+]."
#: src/ui/gui/netplay_wait.slint:101 #: src/ui/gui/netplay_wait.slint:101
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Local Input Delay:" msgid "Local Input Delay:"
msgstr "Retardo de entrada local:" msgstr "Retardo de entrada local:"
#: src/ui/gui/netplay_wait.slint:117 #: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Recommended: " msgid "Recommended: "
msgstr "Recomendado: " msgstr "Recomendado: "
#: src/ui/gui/netplay_wait.slint:126 #: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Start Game" msgid "Start Game"
msgstr "Iniciar juego" msgstr "Iniciar juego"
#: src/ui/gui/netplay_wait.slint:126 #: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Waiting for P2P mesh " msgid "Waiting for P2P mesh "
msgstr "Esperando malla P2P " msgstr "Esperando malla P2P "
#: src/ui/gui/netplay_wait.slint:134 #: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Cancel" msgid "Cancel"
msgstr "Cancelar" msgstr "Cancelar"
@@ -487,71 +487,101 @@ msgctxt "RetroAchievements"
msgid "View Supported Games" msgid "View Supported Games"
msgstr "Ver juegos compatibles" msgstr "Ver juegos compatibles"
#: src/ui/gui/settings_page.slint:24 #: src/ui/gui/settings_page.slint:25
msgctxt "Settings" msgctxt "Settings"
msgid "Settings" msgid "Settings"
msgstr "Ajustes" 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" msgctxt "Settings"
msgid "Resolution:" msgid "Resolution:"
msgstr "Resolución:" msgstr "Resolución:"
#: src/ui/gui/settings_page.slint:49 #: src/ui/gui/settings_page.slint:51
msgctxt "Settings" msgctxt "Settings"
msgid "SSAA downscaling" msgid "SSAA downscaling"
msgstr "Reducción de escala SSAA" msgstr "Reducción de escala SSAA"
#: src/ui/gui/settings_page.slint:57 #: src/ui/gui/settings_page.slint:59
msgctxt "Settings" msgctxt "Settings"
msgid "Integer Scaling" msgid "Integer Scaling"
msgstr "Escalado por enteros" msgstr "Escalado por enteros"
#: src/ui/gui/settings_page.slint:65 #: src/ui/gui/settings_page.slint:67
msgctxt "Settings" msgctxt "Settings"
msgid "Fullscreen" msgid "Fullscreen"
msgstr "Pantalla completa" msgstr "Pantalla completa"
#: src/ui/gui/settings_page.slint:73 #: src/ui/gui/settings_page.slint:75
msgctxt "Settings" msgctxt "Settings"
msgid "Widescreen (stretch)" msgid "Widescreen (stretch)"
msgstr "Pantalla ancha (estirado)" msgstr "Pantalla ancha (estirado)"
#: src/ui/gui/settings_page.slint:81 #: src/ui/gui/settings_page.slint:83
msgctxt "Settings" msgctxt "Settings"
msgid "VSync" msgid "VSync"
msgstr "Sincronización vertical (VSync)" msgstr "Sincronización vertical (VSync)"
#: src/ui/gui/settings_page.slint:89 #: src/ui/gui/settings_page.slint:91
msgctxt "Settings" msgctxt "Settings"
msgid "Apply CRT shader" msgid "Apply CRT shader"
msgstr "Aplicar sombreador CRT" msgstr "Aplicar sombreador CRT"
#: src/ui/gui/settings_page.slint:101 #: src/ui/gui/settings_page.slint:103
msgctxt "Settings" msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)" msgid "Overclock N64 CPU (may cause bugs)"
msgstr "Overclock CPU N64 (puede causar errores)" msgstr "Overclock CPU N64 (puede causar errores)"
#: src/ui/gui/settings_page.slint:109 #: src/ui/gui/settings_page.slint:111
msgctxt "Settings" msgctxt "Settings"
msgid "Disable Expansion Pak" msgid "Disable Expansion Pak"
msgstr "Desactivar Expansion Pak" msgstr "Desactivar Expansion Pak"
#: src/ui/gui/settings_page.slint:117 #: src/ui/gui/settings_page.slint:119
msgctxt "Settings" msgctxt "Settings"
msgid "Enable Rewind" msgid "Enable Rewind"
msgstr "Activar rebobinado" msgstr "Activar rebobinado"
#: src/ui/gui/settings_page.slint:125 #: src/ui/gui/settings_page.slint:127
msgctxt "Settings" msgctxt "Settings"
msgid "Emulate UNFLoader" msgid "Emulate UNFLoader"
msgstr "Emular UNFLoader" msgstr "Emular UNFLoader"
#: src/ui/gui/settings_page.slint:133 #: src/ui/gui/settings_page.slint:135
msgctxt "Settings" msgctxt "Settings"
msgid "Emulate VRU" msgid "Emulate VRU"
msgstr "Emular 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 #: src/ui/gui/vru_dialog.slint:7
msgctxt "VruDialog" msgctxt "VruDialog"
msgid "What Would You Like To Say?" msgid "What Would You Like To Say?"
+58 -28
View File
@@ -2,7 +2,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gopher64\n" "Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-13 07:48+0000\n" "POT-Creation-Date: 2026-06-30 09:43+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n" "PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: French (France)\n" "Language-Team: French (France)\n"
@@ -47,37 +47,37 @@ msgctxt "About"
msgid "Donate (via GitHub)" msgid "Donate (via GitHub)"
msgstr "Faire un don (via GitHub)" msgstr "Faire un don (via GitHub)"
#: src/ui/gui/appwindow.slint:148 #: src/ui/gui/appwindow.slint:164
msgctxt "Menu" msgctxt "Menu"
msgid "Local Game" msgid "Local Game"
msgstr "Jeu local" msgstr "Jeu local"
#: src/ui/gui/appwindow.slint:152 #: src/ui/gui/appwindow.slint:168
msgctxt "Menu" msgctxt "Menu"
msgid "Controller Config" msgid "Controller Config"
msgstr "Configuration manette" msgstr "Configuration manette"
#: src/ui/gui/appwindow.slint:156 #: src/ui/gui/appwindow.slint:172
msgctxt "Menu" msgctxt "Menu"
msgid "Netplay" msgid "Netplay"
msgstr "Netplay" msgstr "Netplay"
#: src/ui/gui/appwindow.slint:160 #: src/ui/gui/appwindow.slint:176
msgctxt "Menu" msgctxt "Menu"
msgid "Cheats" msgid "Cheats"
msgstr "Triche" msgstr "Triche"
#: src/ui/gui/appwindow.slint:164 #: src/ui/gui/appwindow.slint:180
msgctxt "Menu" msgctxt "Menu"
msgid "RetroAchievements" msgid "RetroAchievements"
msgstr "RetroAchievements" msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:168 #: src/ui/gui/appwindow.slint:184
msgctxt "Menu" msgctxt "Menu"
msgid "Settings" msgid "Settings"
msgstr "Paramètres" msgstr "Paramètres"
#: src/ui/gui/appwindow.slint:172 #: src/ui/gui/appwindow.slint:188
msgctxt "Menu" msgctxt "Menu"
msgid "About" msgid "About"
msgstr "À propos" msgstr "À propos"
@@ -240,7 +240,7 @@ msgctxt "NetplayCreate"
msgid "Create Session" msgid "Create Session"
msgstr "Créer la session" msgstr "Créer la session"
#: src/ui/gui/netplay_create.slint:105 #: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate" msgctxt "NetplayCreate"
msgid "Cancel" msgid "Cancel"
msgstr "Annuler" msgstr "Annuler"
@@ -399,30 +399,30 @@ msgid ""
"Alt+]." "Alt+]."
msgstr "" msgstr ""
"Régler le délai d'entrée trop bas provoquera des ralentissements.\n" "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 " "Vous pouvez diminuer le délai d'entrée en jeu avec Alt+[ et l'augmenter avec "
"avec Alt+]." "Alt+]."
#: src/ui/gui/netplay_wait.slint:101 #: src/ui/gui/netplay_wait.slint:101
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Local Input Delay:" msgid "Local Input Delay:"
msgstr "Délai d'entrée local :" msgstr "Délai d'entrée local :"
#: src/ui/gui/netplay_wait.slint:117 #: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Recommended: " msgid "Recommended: "
msgstr "Recommandé : " msgstr "Recommandé : "
#: src/ui/gui/netplay_wait.slint:126 #: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Start Game" msgid "Start Game"
msgstr "Lancer la partie" msgstr "Lancer la partie"
#: src/ui/gui/netplay_wait.slint:126 #: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Waiting for P2P mesh " msgid "Waiting for P2P mesh "
msgstr "En attente du maillage P2P " msgstr "En attente du maillage P2P "
#: src/ui/gui/netplay_wait.slint:134 #: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Cancel" msgid "Cancel"
msgstr "Annuler" msgstr "Annuler"
@@ -487,71 +487,101 @@ msgctxt "RetroAchievements"
msgid "View Supported Games" msgid "View Supported Games"
msgstr "Voir les jeux pris en charge" msgstr "Voir les jeux pris en charge"
#: src/ui/gui/settings_page.slint:24 #: src/ui/gui/settings_page.slint:25
msgctxt "Settings" msgctxt "Settings"
msgid "Settings" msgid "Settings"
msgstr "Paramètres" 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" msgctxt "Settings"
msgid "Resolution:" msgid "Resolution:"
msgstr "Résolution :" msgstr "Résolution :"
#: src/ui/gui/settings_page.slint:49 #: src/ui/gui/settings_page.slint:51
msgctxt "Settings" msgctxt "Settings"
msgid "SSAA downscaling" msgid "SSAA downscaling"
msgstr "Réduction SSAA" msgstr "Réduction SSAA"
#: src/ui/gui/settings_page.slint:57 #: src/ui/gui/settings_page.slint:59
msgctxt "Settings" msgctxt "Settings"
msgid "Integer Scaling" msgid "Integer Scaling"
msgstr "Mise à l'échelle par entiers" msgstr "Mise à l'échelle par entiers"
#: src/ui/gui/settings_page.slint:65 #: src/ui/gui/settings_page.slint:67
msgctxt "Settings" msgctxt "Settings"
msgid "Fullscreen" msgid "Fullscreen"
msgstr "Plein écran" msgstr "Plein écran"
#: src/ui/gui/settings_page.slint:73 #: src/ui/gui/settings_page.slint:75
msgctxt "Settings" msgctxt "Settings"
msgid "Widescreen (stretch)" msgid "Widescreen (stretch)"
msgstr "Écran large (étiré)" msgstr "Écran large (étiré)"
#: src/ui/gui/settings_page.slint:81 #: src/ui/gui/settings_page.slint:83
msgctxt "Settings" msgctxt "Settings"
msgid "VSync" msgid "VSync"
msgstr "Synchro verticale" msgstr "Synchro verticale"
#: src/ui/gui/settings_page.slint:89 #: src/ui/gui/settings_page.slint:91
msgctxt "Settings" msgctxt "Settings"
msgid "Apply CRT shader" msgid "Apply CRT shader"
msgstr "Appliquer le filtre CRT" msgstr "Appliquer le filtre CRT"
#: src/ui/gui/settings_page.slint:101 #: src/ui/gui/settings_page.slint:103
msgctxt "Settings" msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)" msgid "Overclock N64 CPU (may cause bugs)"
msgstr "Surcharger le CPU N64 (risque de 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" msgctxt "Settings"
msgid "Disable Expansion Pak" msgid "Disable Expansion Pak"
msgstr "Désactiver l'Expansion Pak" msgstr "Désactiver l'Expansion Pak"
#: src/ui/gui/settings_page.slint:117 #: src/ui/gui/settings_page.slint:119
msgctxt "Settings" msgctxt "Settings"
msgid "Enable Rewind" msgid "Enable Rewind"
msgstr "Activer le rembobinage" msgstr "Activer le rembobinage"
#: src/ui/gui/settings_page.slint:125 #: src/ui/gui/settings_page.slint:127
msgctxt "Settings" msgctxt "Settings"
msgid "Emulate UNFLoader" msgid "Emulate UNFLoader"
msgstr "Émuler UNFLoader" msgstr "Émuler UNFLoader"
#: src/ui/gui/settings_page.slint:133 #: src/ui/gui/settings_page.slint:135
msgctxt "Settings" msgctxt "Settings"
msgid "Emulate VRU" msgid "Emulate VRU"
msgstr "Émuler le 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 #: src/ui/gui/vru_dialog.slint:7
msgctxt "VruDialog" msgctxt "VruDialog"
msgid "What Would You Like To Say?" msgid "What Would You Like To Say?"
+56 -26
View File
@@ -2,7 +2,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2026-06-13 07:48+0000\n" "POT-Creation-Date: 2026-06-30 09:43+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -47,37 +47,37 @@ msgctxt "About"
msgid "Donate (via GitHub)" msgid "Donate (via GitHub)"
msgstr "" msgstr ""
#: src/ui/gui/appwindow.slint:148 #: src/ui/gui/appwindow.slint:164
msgctxt "Menu" msgctxt "Menu"
msgid "Local Game" msgid "Local Game"
msgstr "" msgstr ""
#: src/ui/gui/appwindow.slint:152 #: src/ui/gui/appwindow.slint:168
msgctxt "Menu" msgctxt "Menu"
msgid "Controller Config" msgid "Controller Config"
msgstr "" msgstr ""
#: src/ui/gui/appwindow.slint:156 #: src/ui/gui/appwindow.slint:172
msgctxt "Menu" msgctxt "Menu"
msgid "Netplay" msgid "Netplay"
msgstr "" msgstr ""
#: src/ui/gui/appwindow.slint:160 #: src/ui/gui/appwindow.slint:176
msgctxt "Menu" msgctxt "Menu"
msgid "Cheats" msgid "Cheats"
msgstr "" msgstr ""
#: src/ui/gui/appwindow.slint:164 #: src/ui/gui/appwindow.slint:180
msgctxt "Menu" msgctxt "Menu"
msgid "RetroAchievements" msgid "RetroAchievements"
msgstr "" msgstr ""
#: src/ui/gui/appwindow.slint:168 #: src/ui/gui/appwindow.slint:184
msgctxt "Menu" msgctxt "Menu"
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
#: src/ui/gui/appwindow.slint:172 #: src/ui/gui/appwindow.slint:188
msgctxt "Menu" msgctxt "Menu"
msgid "About" msgid "About"
msgstr "" msgstr ""
@@ -238,7 +238,7 @@ msgctxt "NetplayCreate"
msgid "Create Session" msgid "Create Session"
msgstr "" msgstr ""
#: src/ui/gui/netplay_create.slint:105 #: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate" msgctxt "NetplayCreate"
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
@@ -400,22 +400,22 @@ msgctxt "NetplayWait"
msgid "Local Input Delay:" msgid "Local Input Delay:"
msgstr "" msgstr ""
#: src/ui/gui/netplay_wait.slint:117 #: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Recommended: " msgid "Recommended: "
msgstr "" msgstr ""
#: src/ui/gui/netplay_wait.slint:126 #: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Start Game" msgid "Start Game"
msgstr "" msgstr ""
#: src/ui/gui/netplay_wait.slint:126 #: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Waiting for P2P mesh " msgid "Waiting for P2P mesh "
msgstr "" msgstr ""
#: src/ui/gui/netplay_wait.slint:134 #: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
@@ -480,71 +480,101 @@ msgctxt "RetroAchievements"
msgid "View Supported Games" msgid "View Supported Games"
msgstr "" msgstr ""
#: src/ui/gui/settings_page.slint:24 #: src/ui/gui/settings_page.slint:25
msgctxt "Settings" msgctxt "Settings"
msgid "Settings" msgid "Settings"
msgstr "" 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" msgctxt "Settings"
msgid "Resolution:" msgid "Resolution:"
msgstr "" msgstr ""
#: src/ui/gui/settings_page.slint:49 #: src/ui/gui/settings_page.slint:51
msgctxt "Settings" msgctxt "Settings"
msgid "SSAA downscaling" msgid "SSAA downscaling"
msgstr "" msgstr ""
#: src/ui/gui/settings_page.slint:57 #: src/ui/gui/settings_page.slint:59
msgctxt "Settings" msgctxt "Settings"
msgid "Integer Scaling" msgid "Integer Scaling"
msgstr "" msgstr ""
#: src/ui/gui/settings_page.slint:65 #: src/ui/gui/settings_page.slint:67
msgctxt "Settings" msgctxt "Settings"
msgid "Fullscreen" msgid "Fullscreen"
msgstr "" msgstr ""
#: src/ui/gui/settings_page.slint:73 #: src/ui/gui/settings_page.slint:75
msgctxt "Settings" msgctxt "Settings"
msgid "Widescreen (stretch)" msgid "Widescreen (stretch)"
msgstr "" msgstr ""
#: src/ui/gui/settings_page.slint:81 #: src/ui/gui/settings_page.slint:83
msgctxt "Settings" msgctxt "Settings"
msgid "VSync" msgid "VSync"
msgstr "" msgstr ""
#: src/ui/gui/settings_page.slint:89 #: src/ui/gui/settings_page.slint:91
msgctxt "Settings" msgctxt "Settings"
msgid "Apply CRT shader" msgid "Apply CRT shader"
msgstr "" msgstr ""
#: src/ui/gui/settings_page.slint:101 #: src/ui/gui/settings_page.slint:103
msgctxt "Settings" msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)" msgid "Overclock N64 CPU (may cause bugs)"
msgstr "" msgstr ""
#: src/ui/gui/settings_page.slint:109 #: src/ui/gui/settings_page.slint:111
msgctxt "Settings" msgctxt "Settings"
msgid "Disable Expansion Pak" msgid "Disable Expansion Pak"
msgstr "" msgstr ""
#: src/ui/gui/settings_page.slint:117 #: src/ui/gui/settings_page.slint:119
msgctxt "Settings" msgctxt "Settings"
msgid "Enable Rewind" msgid "Enable Rewind"
msgstr "" msgstr ""
#: src/ui/gui/settings_page.slint:125 #: src/ui/gui/settings_page.slint:127
msgctxt "Settings" msgctxt "Settings"
msgid "Emulate UNFLoader" msgid "Emulate UNFLoader"
msgstr "" msgstr ""
#: src/ui/gui/settings_page.slint:133 #: src/ui/gui/settings_page.slint:135
msgctxt "Settings" msgctxt "Settings"
msgid "Emulate VRU" msgid "Emulate VRU"
msgstr "" 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 #: src/ui/gui/vru_dialog.slint:7
msgctxt "VruDialog" msgctxt "VruDialog"
msgid "What Would You Like To Say?" msgid "What Would You Like To Say?"
+56 -26
View File
@@ -2,7 +2,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gopher64\n" "Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-13 07:48+0000\n" "POT-Creation-Date: 2026-06-30 09:43+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n" "PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Japanese\n" "Language-Team: Japanese\n"
@@ -47,37 +47,37 @@ msgctxt "About"
msgid "Donate (via GitHub)" msgid "Donate (via GitHub)"
msgstr "GitHubで寄付" msgstr "GitHubで寄付"
#: src/ui/gui/appwindow.slint:148 #: src/ui/gui/appwindow.slint:164
msgctxt "Menu" msgctxt "Menu"
msgid "Local Game" msgid "Local Game"
msgstr "ローカルゲーム" msgstr "ローカルゲーム"
#: src/ui/gui/appwindow.slint:152 #: src/ui/gui/appwindow.slint:168
msgctxt "Menu" msgctxt "Menu"
msgid "Controller Config" msgid "Controller Config"
msgstr "コントローラー設定" msgstr "コントローラー設定"
#: src/ui/gui/appwindow.slint:156 #: src/ui/gui/appwindow.slint:172
msgctxt "Menu" msgctxt "Menu"
msgid "Netplay" msgid "Netplay"
msgstr "ネットプレイ" msgstr "ネットプレイ"
#: src/ui/gui/appwindow.slint:160 #: src/ui/gui/appwindow.slint:176
msgctxt "Menu" msgctxt "Menu"
msgid "Cheats" msgid "Cheats"
msgstr "チート" msgstr "チート"
#: src/ui/gui/appwindow.slint:164 #: src/ui/gui/appwindow.slint:180
msgctxt "Menu" msgctxt "Menu"
msgid "RetroAchievements" msgid "RetroAchievements"
msgstr "RetroAchievements" msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:168 #: src/ui/gui/appwindow.slint:184
msgctxt "Menu" msgctxt "Menu"
msgid "Settings" msgid "Settings"
msgstr "設定" msgstr "設定"
#: src/ui/gui/appwindow.slint:172 #: src/ui/gui/appwindow.slint:188
msgctxt "Menu" msgctxt "Menu"
msgid "About" msgid "About"
msgstr "バージョン情報" msgstr "バージョン情報"
@@ -238,7 +238,7 @@ msgctxt "NetplayCreate"
msgid "Create Session" msgid "Create Session"
msgstr "セッションを作成" msgstr "セッションを作成"
#: src/ui/gui/netplay_create.slint:105 #: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate" msgctxt "NetplayCreate"
msgid "Cancel" msgid "Cancel"
msgstr "キャンセル" msgstr "キャンセル"
@@ -403,22 +403,22 @@ msgctxt "NetplayWait"
msgid "Local Input Delay:" msgid "Local Input Delay:"
msgstr "ローカル入力遅延:" msgstr "ローカル入力遅延:"
#: src/ui/gui/netplay_wait.slint:117 #: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Recommended: " msgid "Recommended: "
msgstr "推奨: " msgstr "推奨: "
#: src/ui/gui/netplay_wait.slint:126 #: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Start Game" msgid "Start Game"
msgstr "ゲーム開始" msgstr "ゲーム開始"
#: src/ui/gui/netplay_wait.slint:126 #: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Waiting for P2P mesh " msgid "Waiting for P2P mesh "
msgstr "P2Pメッシュ接続待機中 " msgstr "P2Pメッシュ接続待機中 "
#: src/ui/gui/netplay_wait.slint:134 #: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Cancel" msgid "Cancel"
msgstr "キャンセル" msgstr "キャンセル"
@@ -483,71 +483,101 @@ msgctxt "RetroAchievements"
msgid "View Supported Games" msgid "View Supported Games"
msgstr "対応ゲームを表示" msgstr "対応ゲームを表示"
#: src/ui/gui/settings_page.slint:24 #: src/ui/gui/settings_page.slint:25
msgctxt "Settings" msgctxt "Settings"
msgid "Settings" msgid "Settings"
msgstr "設定" 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" msgctxt "Settings"
msgid "Resolution:" msgid "Resolution:"
msgstr "解像度:" msgstr "解像度:"
#: src/ui/gui/settings_page.slint:49 #: src/ui/gui/settings_page.slint:51
msgctxt "Settings" msgctxt "Settings"
msgid "SSAA downscaling" msgid "SSAA downscaling"
msgstr "SSAAダウンスケール" msgstr "SSAAダウンスケール"
#: src/ui/gui/settings_page.slint:57 #: src/ui/gui/settings_page.slint:59
msgctxt "Settings" msgctxt "Settings"
msgid "Integer Scaling" msgid "Integer Scaling"
msgstr "整数倍スケーリング" msgstr "整数倍スケーリング"
#: src/ui/gui/settings_page.slint:65 #: src/ui/gui/settings_page.slint:67
msgctxt "Settings" msgctxt "Settings"
msgid "Fullscreen" msgid "Fullscreen"
msgstr "全画面表示" msgstr "全画面表示"
#: src/ui/gui/settings_page.slint:73 #: src/ui/gui/settings_page.slint:75
msgctxt "Settings" msgctxt "Settings"
msgid "Widescreen (stretch)" msgid "Widescreen (stretch)"
msgstr "ワイド画面(伸ばす)" msgstr "ワイド画面(伸ばす)"
#: src/ui/gui/settings_page.slint:81 #: src/ui/gui/settings_page.slint:83
msgctxt "Settings" msgctxt "Settings"
msgid "VSync" msgid "VSync"
msgstr "垂直同期(VSync" msgstr "垂直同期(VSync"
#: src/ui/gui/settings_page.slint:89 #: src/ui/gui/settings_page.slint:91
msgctxt "Settings" msgctxt "Settings"
msgid "Apply CRT shader" msgid "Apply CRT shader"
msgstr "CRTシェーダーを適用" msgstr "CRTシェーダーを適用"
#: src/ui/gui/settings_page.slint:101 #: src/ui/gui/settings_page.slint:103
msgctxt "Settings" msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)" msgid "Overclock N64 CPU (may cause bugs)"
msgstr "N64のCPUをオーバークロック(不具合の原因になることがあります)" msgstr "N64のCPUをオーバークロック(不具合の原因になることがあります)"
#: src/ui/gui/settings_page.slint:109 #: src/ui/gui/settings_page.slint:111
msgctxt "Settings" msgctxt "Settings"
msgid "Disable Expansion Pak" msgid "Disable Expansion Pak"
msgstr "拡張パックを無効にする" msgstr "拡張パックを無効にする"
#: src/ui/gui/settings_page.slint:117 #: src/ui/gui/settings_page.slint:119
msgctxt "Settings" msgctxt "Settings"
msgid "Enable Rewind" msgid "Enable Rewind"
msgstr "巻き戻しを有効にする" msgstr "巻き戻しを有効にする"
#: src/ui/gui/settings_page.slint:125 #: src/ui/gui/settings_page.slint:127
msgctxt "Settings" msgctxt "Settings"
msgid "Emulate UNFLoader" msgid "Emulate UNFLoader"
msgstr "UNFLoaderをエミュレート" msgstr "UNFLoaderをエミュレート"
#: src/ui/gui/settings_page.slint:133 #: src/ui/gui/settings_page.slint:135
msgctxt "Settings" msgctxt "Settings"
msgid "Emulate VRU" msgid "Emulate VRU"
msgstr "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 #: src/ui/gui/vru_dialog.slint:7
msgctxt "VruDialog" msgctxt "VruDialog"
msgid "What Would You Like To Say?" msgid "What Would You Like To Say?"
+56 -26
View File
@@ -2,7 +2,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gopher64\n" "Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-13 07:48+0000\n" "POT-Creation-Date: 2026-06-30 09:43+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n" "PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Dutch (Netherlands)\n" "Language-Team: Dutch (Netherlands)\n"
@@ -47,37 +47,37 @@ msgctxt "About"
msgid "Donate (via GitHub)" msgid "Donate (via GitHub)"
msgstr "Doneren (via GitHub)" msgstr "Doneren (via GitHub)"
#: src/ui/gui/appwindow.slint:148 #: src/ui/gui/appwindow.slint:164
msgctxt "Menu" msgctxt "Menu"
msgid "Local Game" msgid "Local Game"
msgstr "Lokaal spel" msgstr "Lokaal spel"
#: src/ui/gui/appwindow.slint:152 #: src/ui/gui/appwindow.slint:168
msgctxt "Menu" msgctxt "Menu"
msgid "Controller Config" msgid "Controller Config"
msgstr "Controllerconfiguratie" msgstr "Controllerconfiguratie"
#: src/ui/gui/appwindow.slint:156 #: src/ui/gui/appwindow.slint:172
msgctxt "Menu" msgctxt "Menu"
msgid "Netplay" msgid "Netplay"
msgstr "Netplay" msgstr "Netplay"
#: src/ui/gui/appwindow.slint:160 #: src/ui/gui/appwindow.slint:176
msgctxt "Menu" msgctxt "Menu"
msgid "Cheats" msgid "Cheats"
msgstr "Cheats" msgstr "Cheats"
#: src/ui/gui/appwindow.slint:164 #: src/ui/gui/appwindow.slint:180
msgctxt "Menu" msgctxt "Menu"
msgid "RetroAchievements" msgid "RetroAchievements"
msgstr "RetroAchievements" msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:168 #: src/ui/gui/appwindow.slint:184
msgctxt "Menu" msgctxt "Menu"
msgid "Settings" msgid "Settings"
msgstr "Instellingen" msgstr "Instellingen"
#: src/ui/gui/appwindow.slint:172 #: src/ui/gui/appwindow.slint:188
msgctxt "Menu" msgctxt "Menu"
msgid "About" msgid "About"
msgstr "Over" msgstr "Over"
@@ -240,7 +240,7 @@ msgctxt "NetplayCreate"
msgid "Create Session" msgid "Create Session"
msgstr "Sessie aanmaken" msgstr "Sessie aanmaken"
#: src/ui/gui/netplay_create.slint:105 #: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate" msgctxt "NetplayCreate"
msgid "Cancel" msgid "Cancel"
msgstr "Annuleren" msgstr "Annuleren"
@@ -406,22 +406,22 @@ msgctxt "NetplayWait"
msgid "Local Input Delay:" msgid "Local Input Delay:"
msgstr "Lokale invoervertraging:" msgstr "Lokale invoervertraging:"
#: src/ui/gui/netplay_wait.slint:117 #: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Recommended: " msgid "Recommended: "
msgstr "Aanbevolen: " msgstr "Aanbevolen: "
#: src/ui/gui/netplay_wait.slint:126 #: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Start Game" msgid "Start Game"
msgstr "Spel starten" msgstr "Spel starten"
#: src/ui/gui/netplay_wait.slint:126 #: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Waiting for P2P mesh " msgid "Waiting for P2P mesh "
msgstr "Wachten op P2P-mesh " msgstr "Wachten op P2P-mesh "
#: src/ui/gui/netplay_wait.slint:134 #: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Cancel" msgid "Cancel"
msgstr "Annuleren" msgstr "Annuleren"
@@ -486,71 +486,101 @@ msgctxt "RetroAchievements"
msgid "View Supported Games" msgid "View Supported Games"
msgstr "Ondersteunde spellen bekijken" msgstr "Ondersteunde spellen bekijken"
#: src/ui/gui/settings_page.slint:24 #: src/ui/gui/settings_page.slint:25
msgctxt "Settings" msgctxt "Settings"
msgid "Settings" msgid "Settings"
msgstr "Instellingen" 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" msgctxt "Settings"
msgid "Resolution:" msgid "Resolution:"
msgstr "Resolutie:" msgstr "Resolutie:"
#: src/ui/gui/settings_page.slint:49 #: src/ui/gui/settings_page.slint:51
msgctxt "Settings" msgctxt "Settings"
msgid "SSAA downscaling" msgid "SSAA downscaling"
msgstr "SSAA-downscaling" msgstr "SSAA-downscaling"
#: src/ui/gui/settings_page.slint:57 #: src/ui/gui/settings_page.slint:59
msgctxt "Settings" msgctxt "Settings"
msgid "Integer Scaling" msgid "Integer Scaling"
msgstr "Schalen in hele getallen" msgstr "Schalen in hele getallen"
#: src/ui/gui/settings_page.slint:65 #: src/ui/gui/settings_page.slint:67
msgctxt "Settings" msgctxt "Settings"
msgid "Fullscreen" msgid "Fullscreen"
msgstr "Volledig scherm" msgstr "Volledig scherm"
#: src/ui/gui/settings_page.slint:73 #: src/ui/gui/settings_page.slint:75
msgctxt "Settings" msgctxt "Settings"
msgid "Widescreen (stretch)" msgid "Widescreen (stretch)"
msgstr "Breedbeeld (uitgerekt)" msgstr "Breedbeeld (uitgerekt)"
#: src/ui/gui/settings_page.slint:81 #: src/ui/gui/settings_page.slint:83
msgctxt "Settings" msgctxt "Settings"
msgid "VSync" msgid "VSync"
msgstr "VSync" msgstr "VSync"
#: src/ui/gui/settings_page.slint:89 #: src/ui/gui/settings_page.slint:91
msgctxt "Settings" msgctxt "Settings"
msgid "Apply CRT shader" msgid "Apply CRT shader"
msgstr "CRT-shader toepassen" msgstr "CRT-shader toepassen"
#: src/ui/gui/settings_page.slint:101 #: src/ui/gui/settings_page.slint:103
msgctxt "Settings" msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)" msgid "Overclock N64 CPU (may cause bugs)"
msgstr "N64-CPU overklokken (kan bugs veroorzaken)" msgstr "N64-CPU overklokken (kan bugs veroorzaken)"
#: src/ui/gui/settings_page.slint:109 #: src/ui/gui/settings_page.slint:111
msgctxt "Settings" msgctxt "Settings"
msgid "Disable Expansion Pak" msgid "Disable Expansion Pak"
msgstr "Expansion Pak uitschakelen" msgstr "Expansion Pak uitschakelen"
#: src/ui/gui/settings_page.slint:117 #: src/ui/gui/settings_page.slint:119
msgctxt "Settings" msgctxt "Settings"
msgid "Enable Rewind" msgid "Enable Rewind"
msgstr "Terugspoelen inschakelen" msgstr "Terugspoelen inschakelen"
#: src/ui/gui/settings_page.slint:125 #: src/ui/gui/settings_page.slint:127
msgctxt "Settings" msgctxt "Settings"
msgid "Emulate UNFLoader" msgid "Emulate UNFLoader"
msgstr "UNFLoader emuleren" msgstr "UNFLoader emuleren"
#: src/ui/gui/settings_page.slint:133 #: src/ui/gui/settings_page.slint:135
msgctxt "Settings" msgctxt "Settings"
msgid "Emulate VRU" msgid "Emulate VRU"
msgstr "VRU emuleren" 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 #: src/ui/gui/vru_dialog.slint:7
msgctxt "VruDialog" msgctxt "VruDialog"
msgid "What Would You Like To Say?" msgid "What Would You Like To Say?"
+62 -32
View File
@@ -2,7 +2,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2026-06-13 07:48+0000\n" "POT-Creation-Date: 2026-06-30 09:43+0000\n"
"PO-Revision-Date: 2026-05-11 18:11-3000\n" "PO-Revision-Date: 2026-05-11 18:11-3000\n"
"Last-Translator: EDUARDO CARVALHO <eduardotcarvalho@gmail.com>\n" "Last-Translator: EDUARDO CARVALHO <eduardotcarvalho@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -10,7 +10,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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 #: src/ui/gui/about_page.slint:17
msgctxt "NewVersionButton" msgctxt "NewVersionButton"
@@ -47,37 +47,37 @@ msgctxt "About"
msgid "Donate (via GitHub)" msgid "Donate (via GitHub)"
msgstr "Doações (via GitHub)" msgstr "Doações (via GitHub)"
#: src/ui/gui/appwindow.slint:148 #: src/ui/gui/appwindow.slint:164
msgctxt "Menu" msgctxt "Menu"
msgid "Local Game" msgid "Local Game"
msgstr "Jogo Local" msgstr "Jogo Local"
#: src/ui/gui/appwindow.slint:152 #: src/ui/gui/appwindow.slint:168
msgctxt "Menu" msgctxt "Menu"
msgid "Controller Config" msgid "Controller Config"
msgstr "Configurar Controle" msgstr "Configurar Controle"
#: src/ui/gui/appwindow.slint:156 #: src/ui/gui/appwindow.slint:172
msgctxt "Menu" msgctxt "Menu"
msgid "Netplay" msgid "Netplay"
msgstr "Netplay" msgstr "Netplay"
#: src/ui/gui/appwindow.slint:160 #: src/ui/gui/appwindow.slint:176
msgctxt "Menu" msgctxt "Menu"
msgid "Cheats" msgid "Cheats"
msgstr "Trapaças" msgstr "Trapaças"
#: src/ui/gui/appwindow.slint:164 #: src/ui/gui/appwindow.slint:180
msgctxt "Menu" msgctxt "Menu"
msgid "RetroAchievements" msgid "RetroAchievements"
msgstr "RetroAchievements" msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:168 #: src/ui/gui/appwindow.slint:184
msgctxt "Menu" msgctxt "Menu"
msgid "Settings" msgid "Settings"
msgstr "Configurações" msgstr "Configurações"
#: src/ui/gui/appwindow.slint:172 #: src/ui/gui/appwindow.slint:188
msgctxt "Menu" msgctxt "Menu"
msgid "About" msgid "About"
msgstr "Sobre" msgstr "Sobre"
@@ -180,7 +180,7 @@ msgstr "Abrir ROM"
#: src/ui/gui/localgame_page.slint:37 #: src/ui/gui/localgame_page.slint:37
msgctxt "LocalGame" msgctxt "LocalGame"
msgid "Open Saves Folder" msgid "Open Saves Folder"
msgstr "Abrir Partes de Saves" msgstr "Abrir Pasta de Saves"
#: src/ui/gui/localgame_page.slint:54 #: src/ui/gui/localgame_page.slint:54
msgctxt "LocalGame" msgctxt "LocalGame"
@@ -200,7 +200,7 @@ msgstr "Nome da Sessão:"
#: src/ui/gui/netplay_create.slint:26 #: src/ui/gui/netplay_create.slint:26
msgctxt "NetplayCreate" msgctxt "NetplayCreate"
msgid "Enter session name" msgid "Enter session name"
msgstr "Insira nome da sessão:" msgstr "Insira nome da sessão"
#: src/ui/gui/netplay_create.slint:36 #: src/ui/gui/netplay_create.slint:36
msgctxt "NetplayCreate" msgctxt "NetplayCreate"
@@ -240,7 +240,7 @@ msgctxt "NetplayCreate"
msgid "Create Session" msgid "Create Session"
msgstr "Criar Sessão" msgstr "Criar Sessão"
#: src/ui/gui/netplay_create.slint:105 #: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate" msgctxt "NetplayCreate"
msgid "Cancel" msgid "Cancel"
msgstr "Cancelar" msgstr "Cancelar"
@@ -364,7 +364,7 @@ msgstr "Nome do Jogo:"
#: src/ui/gui/netplay_wait.slint:45 #: src/ui/gui/netplay_wait.slint:45
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Player 1:" msgid "Player 1:"
msgstr "Jogador 1" msgstr "Jogador 1:"
#: src/ui/gui/netplay_wait.slint:49 #: src/ui/gui/netplay_wait.slint:49
msgctxt "NetplayWait" msgctxt "NetplayWait"
@@ -399,30 +399,30 @@ msgid ""
"Alt+]." "Alt+]."
msgstr "" msgstr ""
"Definir o atraso de entrada muito baixo causará lag no jogo.\n" "Definir o atraso de entrada muito baixo causará lag no jogo.\n"
"Você pode diminuir o atraso de entrada no jogo pressionando Alt+[ e " "Você pode diminuir o atraso de entrada no jogo pressionando Alt+[ e aumentá-"
"aumentá-lo com Alt+]." "lo com Alt+]."
#: src/ui/gui/netplay_wait.slint:101 #: src/ui/gui/netplay_wait.slint:101
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Local Input Delay:" msgid "Local Input Delay:"
msgstr "Atraso de entrada local:" msgstr "Atraso de entrada local:"
#: src/ui/gui/netplay_wait.slint:117 #: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Recommended: " msgid "Recommended: "
msgstr "Recomendado: " msgstr "Recomendado: "
#: src/ui/gui/netplay_wait.slint:126 #: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Start Game" msgid "Start Game"
msgstr "Iniciar jogo" msgstr "Iniciar jogo"
#: src/ui/gui/netplay_wait.slint:126 #: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Waiting for P2P mesh " msgid "Waiting for P2P mesh "
msgstr "Aguardando malha P2P " msgstr "Aguardando malha P2P "
#: src/ui/gui/netplay_wait.slint:134 #: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Cancel" msgid "Cancel"
msgstr "Cancelar" msgstr "Cancelar"
@@ -487,71 +487,101 @@ msgctxt "RetroAchievements"
msgid "View Supported Games" msgid "View Supported Games"
msgstr "Ver Jogos com Suporte" msgstr "Ver Jogos com Suporte"
#: src/ui/gui/settings_page.slint:24 #: src/ui/gui/settings_page.slint:25
msgctxt "Settings" msgctxt "Settings"
msgid "Settings" msgid "Settings"
msgstr "Configurações" 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" msgctxt "Settings"
msgid "Resolution:" msgid "Resolution:"
msgstr "Resolução:" msgstr "Resolução:"
#: src/ui/gui/settings_page.slint:49 #: src/ui/gui/settings_page.slint:51
msgctxt "Settings" msgctxt "Settings"
msgid "SSAA downscaling" msgid "SSAA downscaling"
msgstr "Redução de SSAA" msgstr "Redução de SSAA"
#: src/ui/gui/settings_page.slint:57 #: src/ui/gui/settings_page.slint:59
msgctxt "Settings" msgctxt "Settings"
msgid "Integer Scaling" msgid "Integer Scaling"
msgstr "Proporção Íntegra" msgstr "Proporção Íntegra"
#: src/ui/gui/settings_page.slint:65 #: src/ui/gui/settings_page.slint:67
msgctxt "Settings" msgctxt "Settings"
msgid "Fullscreen" msgid "Fullscreen"
msgstr "Tela Cheia" msgstr "Tela Cheia"
#: src/ui/gui/settings_page.slint:73 #: src/ui/gui/settings_page.slint:75
msgctxt "Settings" msgctxt "Settings"
msgid "Widescreen (stretch)" msgid "Widescreen (stretch)"
msgstr "Widescreen (esticar)" msgstr "Widescreen (esticar)"
#: src/ui/gui/settings_page.slint:81 #: src/ui/gui/settings_page.slint:83
msgctxt "Settings" msgctxt "Settings"
msgid "VSync" msgid "VSync"
msgstr "VSync" msgstr "VSync"
#: src/ui/gui/settings_page.slint:89 #: src/ui/gui/settings_page.slint:91
msgctxt "Settings" msgctxt "Settings"
msgid "Apply CRT shader" msgid "Apply CRT shader"
msgstr "Aplicar filtro de CRT" msgstr "Aplicar filtro de CRT"
#: src/ui/gui/settings_page.slint:101 #: src/ui/gui/settings_page.slint:103
msgctxt "Settings" msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)" msgid "Overclock N64 CPU (may cause bugs)"
msgstr "Overclock da CPU do N64 (pode causar 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" msgctxt "Settings"
msgid "Disable Expansion Pak" msgid "Disable Expansion Pak"
msgstr "Desabilitar Expansion Pak" msgstr "Desabilitar Expansion Pak"
#: src/ui/gui/settings_page.slint:117 #: src/ui/gui/settings_page.slint:119
msgctxt "Settings" msgctxt "Settings"
msgid "Enable Rewind" msgid "Enable Rewind"
msgstr "Habilitar rebobinamento" msgstr "Habilitar rebobinamento"
#: src/ui/gui/settings_page.slint:125 #: src/ui/gui/settings_page.slint:127
msgctxt "Settings" msgctxt "Settings"
msgid "Emulate UNFLoader" msgid "Emulate UNFLoader"
msgstr "Emular UNFLoader" msgstr "Emular UNFLoader"
#: src/ui/gui/settings_page.slint:133 #: src/ui/gui/settings_page.slint:135
msgctxt "Settings" msgctxt "Settings"
msgid "Emulate VRU" msgid "Emulate VRU"
msgstr "Emular 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 #: src/ui/gui/vru_dialog.slint:7
msgctxt "VruDialog" msgctxt "VruDialog"
msgid "What Would You Like To Say?" msgid "What Would You Like To Say?"
+58 -27
View File
@@ -2,7 +2,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gopher64\n" "Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-13 07:48+0000\n" "POT-Creation-Date: 2026-06-30 09:43+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n" "PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Russian\n" "Language-Team: Russian\n"
@@ -10,7 +10,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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 #: src/ui/gui/about_page.slint:17
msgctxt "NewVersionButton" msgctxt "NewVersionButton"
@@ -47,37 +48,37 @@ msgctxt "About"
msgid "Donate (via GitHub)" msgid "Donate (via GitHub)"
msgstr "Пожертвовать (через GitHub)" msgstr "Пожертвовать (через GitHub)"
#: src/ui/gui/appwindow.slint:148 #: src/ui/gui/appwindow.slint:164
msgctxt "Menu" msgctxt "Menu"
msgid "Local Game" msgid "Local Game"
msgstr "Локальная игра" msgstr "Локальная игра"
#: src/ui/gui/appwindow.slint:152 #: src/ui/gui/appwindow.slint:168
msgctxt "Menu" msgctxt "Menu"
msgid "Controller Config" msgid "Controller Config"
msgstr "Настройка контроллера" msgstr "Настройка контроллера"
#: src/ui/gui/appwindow.slint:156 #: src/ui/gui/appwindow.slint:172
msgctxt "Menu" msgctxt "Menu"
msgid "Netplay" msgid "Netplay"
msgstr "Сетевая игра" msgstr "Сетевая игра"
#: src/ui/gui/appwindow.slint:160 #: src/ui/gui/appwindow.slint:176
msgctxt "Menu" msgctxt "Menu"
msgid "Cheats" msgid "Cheats"
msgstr "Читы" msgstr "Читы"
#: src/ui/gui/appwindow.slint:164 #: src/ui/gui/appwindow.slint:180
msgctxt "Menu" msgctxt "Menu"
msgid "RetroAchievements" msgid "RetroAchievements"
msgstr "RetroAchievements" msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:168 #: src/ui/gui/appwindow.slint:184
msgctxt "Menu" msgctxt "Menu"
msgid "Settings" msgid "Settings"
msgstr "Настройки" msgstr "Настройки"
#: src/ui/gui/appwindow.slint:172 #: src/ui/gui/appwindow.slint:188
msgctxt "Menu" msgctxt "Menu"
msgid "About" msgid "About"
msgstr "О программе" msgstr "О программе"
@@ -239,7 +240,7 @@ msgctxt "NetplayCreate"
msgid "Create Session" msgid "Create Session"
msgstr "Создать сессию" msgstr "Создать сессию"
#: src/ui/gui/netplay_create.slint:105 #: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate" msgctxt "NetplayCreate"
msgid "Cancel" msgid "Cancel"
msgstr "Отмена" msgstr "Отмена"
@@ -405,22 +406,22 @@ msgctxt "NetplayWait"
msgid "Local Input Delay:" msgid "Local Input Delay:"
msgstr "Локальная задержка ввода:" msgstr "Локальная задержка ввода:"
#: src/ui/gui/netplay_wait.slint:117 #: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Recommended: " msgid "Recommended: "
msgstr "Рекомендуется: " msgstr "Рекомендуется: "
#: src/ui/gui/netplay_wait.slint:126 #: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Start Game" msgid "Start Game"
msgstr "Начать игру" msgstr "Начать игру"
#: src/ui/gui/netplay_wait.slint:126 #: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Waiting for P2P mesh " msgid "Waiting for P2P mesh "
msgstr "Ожидание P2P-сети " msgstr "Ожидание P2P-сети "
#: src/ui/gui/netplay_wait.slint:134 #: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait" msgctxt "NetplayWait"
msgid "Cancel" msgid "Cancel"
msgstr "Отмена" msgstr "Отмена"
@@ -485,71 +486,101 @@ msgctxt "RetroAchievements"
msgid "View Supported Games" msgid "View Supported Games"
msgstr "Просмотреть поддерживаемые игры" msgstr "Просмотреть поддерживаемые игры"
#: src/ui/gui/settings_page.slint:24 #: src/ui/gui/settings_page.slint:25
msgctxt "Settings" msgctxt "Settings"
msgid "Settings" msgid "Settings"
msgstr "Настройки" 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" msgctxt "Settings"
msgid "Resolution:" msgid "Resolution:"
msgstr "Разрешение:" msgstr "Разрешение:"
#: src/ui/gui/settings_page.slint:49 #: src/ui/gui/settings_page.slint:51
msgctxt "Settings" msgctxt "Settings"
msgid "SSAA downscaling" msgid "SSAA downscaling"
msgstr "SSAA даунскейлинг" msgstr "SSAA даунскейлинг"
#: src/ui/gui/settings_page.slint:57 #: src/ui/gui/settings_page.slint:59
msgctxt "Settings" msgctxt "Settings"
msgid "Integer Scaling" msgid "Integer Scaling"
msgstr "Целочисленное масштабирование" msgstr "Целочисленное масштабирование"
#: src/ui/gui/settings_page.slint:65 #: src/ui/gui/settings_page.slint:67
msgctxt "Settings" msgctxt "Settings"
msgid "Fullscreen" msgid "Fullscreen"
msgstr "Полноэкранный режим" msgstr "Полноэкранный режим"
#: src/ui/gui/settings_page.slint:73 #: src/ui/gui/settings_page.slint:75
msgctxt "Settings" msgctxt "Settings"
msgid "Widescreen (stretch)" msgid "Widescreen (stretch)"
msgstr "Широкоэкранный режим (растянуто)" msgstr "Широкоэкранный режим (растянуто)"
#: src/ui/gui/settings_page.slint:81 #: src/ui/gui/settings_page.slint:83
msgctxt "Settings" msgctxt "Settings"
msgid "VSync" msgid "VSync"
msgstr "Вертикальная синхронизация" msgstr "Вертикальная синхронизация"
#: src/ui/gui/settings_page.slint:89 #: src/ui/gui/settings_page.slint:91
msgctxt "Settings" msgctxt "Settings"
msgid "Apply CRT shader" msgid "Apply CRT shader"
msgstr "Применить шейдер ЭЛТ" msgstr "Применить шейдер ЭЛТ"
#: src/ui/gui/settings_page.slint:101 #: src/ui/gui/settings_page.slint:103
msgctxt "Settings" msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)" msgid "Overclock N64 CPU (may cause bugs)"
msgstr "Разгон процессора N64 (может вызвать ошибки)" msgstr "Разгон процессора N64 (может вызвать ошибки)"
#: src/ui/gui/settings_page.slint:109 #: src/ui/gui/settings_page.slint:111
msgctxt "Settings" msgctxt "Settings"
msgid "Disable Expansion Pak" msgid "Disable Expansion Pak"
msgstr "Отключить Expansion Pak" msgstr "Отключить Expansion Pak"
#: src/ui/gui/settings_page.slint:117 #: src/ui/gui/settings_page.slint:119
msgctxt "Settings" msgctxt "Settings"
msgid "Enable Rewind" msgid "Enable Rewind"
msgstr "Включить перемотку назад" msgstr "Включить перемотку назад"
#: src/ui/gui/settings_page.slint:125 #: src/ui/gui/settings_page.slint:127
msgctxt "Settings" msgctxt "Settings"
msgid "Emulate UNFLoader" msgid "Emulate UNFLoader"
msgstr "Эмулировать UNFLoader" msgstr "Эмулировать UNFLoader"
#: src/ui/gui/settings_page.slint:133 #: src/ui/gui/settings_page.slint:135
msgctxt "Settings" msgctxt "Settings"
msgid "Emulate VRU" msgid "Emulate VRU"
msgstr "Эмулировать 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 #: src/ui/gui/vru_dialog.slint:7
msgctxt "VruDialog" msgctxt "VruDialog"
msgid "What Would You Like To Say?" 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); gfx_info.RDRAM_SIZE / 2, flags);
g_tmem = processor->get_tmem(); g_tmem = processor->get_tmem();
if (!g_tmem) { if (!g_tmem) {
printf("Failed to get tmem\n"); LOGE("Failed to get tmem\n");
} }
g_hidden_rdram = processor->begin_read_hidden_rdram(); g_hidden_rdram = processor->begin_read_hidden_rdram();
if (!g_hidden_rdram) { if (!g_hidden_rdram) {
printf("Failed to get hidden_rdram\n"); LOGE("Failed to get hidden_rdram\n");
} }
sync_signal = 0; sync_signal = 0;
@@ -376,7 +376,7 @@ void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
SDL_SyncWindow(window); SDL_SyncWindow(window);
bool result = SDL_AddEventWatch(sdl_event_filter, nullptr); bool result = SDL_AddEventWatch(sdl_event_filter, nullptr);
if (!result) { if (!result) {
printf("Could not add event watch.\n"); LOGE("Could not add event watch.\n");
return; return;
} }
@@ -622,10 +622,10 @@ static void render_frame(Vulkan::Device &device) {
cmd->set_depth_test(false, false); cmd->set_depth_test(false, false);
cmd->set_cull_mode(VK_CULL_MODE_NONE); 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 // If we don't have an image, we just get a cleared screen in the render
// pass. // pass.
if (image) { if (image) {
VkViewport vp = cmd->get_viewport();
calculate_viewport(&vp.x, &vp.y, &vp.width, &vp.height, calculate_viewport(&vp.x, &vp.y, &vp.width, &vp.height,
image->get_height() / gfx_info.upscale); image->get_height() / gfx_info.upscale);
@@ -646,33 +646,32 @@ static void render_frame(Vulkan::Device &device) {
// The vertices are constants in the shader. // The vertices are constants in the shader.
// Draws fullscreen quad using oversized triangle. // Draws fullscreen quad using oversized triangle.
cmd->draw(3); cmd->draw(3);
}
if (!messages.empty()) { if (!messages.empty()) {
Message *message = &messages.front(); Message *message = &messages.front();
if (!message->image) { if (!message->image) {
message->image = create_message_image(device, vp.width, message_font, message->image = create_message_image(device, vp.width, message_font,
message->message.c_str()); message->message.c_str());
SDL_AddTimer(message->milliseconds, pop_message_callback, NULL); 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);
} }
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) { cmd->draw(3);
draw_fps(cmd, vp); } 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(); cmd->end_render_pass();
@@ -693,7 +692,10 @@ void rdp_update_screen() {
if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED) { if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED) {
return; return;
} }
wsi->end_frame(); if (!wsi->end_frame()) {
LOGE("End frame failed\n");
SDL_PumpEvents(); // For Android to trigger pause event
}
wsi->begin_frame(); wsi->begin_frame();
} }
@@ -788,7 +790,7 @@ uint32_t pixel_size(uint32_t pixel_type, uint32_t area) {
case 3: case 3:
return area * 4; return area * 4;
default: default:
printf("Invalid pixel size: %u\n", pixel_type); LOGE("Invalid pixel size: %u\n", pixel_type);
return 0; 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; } 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) {} 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( int message_length = snprintf(
buffer, sizeof(buffer), "%s\nMode: %s\n", game->title, 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) { if (summary.num_core_achievements != 0) {
snprintf(buffer + message_length, sizeof(buffer) - message_length, snprintf(buffer + message_length, sizeof(buffer) - message_length,
+1 -1
View File
@@ -1,2 +1,2 @@
[toolchain] [toolchain]
channel = "1.96.0" channel = "1.97.0"
+38 -5
View File
@@ -52,6 +52,7 @@ pub fn run_game(
rom_contents: &[u8], rom_contents: &[u8],
game_settings: ui::GameSettings, game_settings: ui::GameSettings,
ra_config: retroachievements::RAConfig, ra_config: retroachievements::RAConfig,
netplay_config: Option<netplay::NetplayConfig>,
) { ) {
device.cpu.overclock = game_settings.overclock; device.cpu.overclock = game_settings.overclock;
if game_settings.disable_expansion_pak { if game_settings.disable_expansion_pak {
@@ -62,17 +63,32 @@ pub fn run_game(
device.savestate.load_state = true; device.savestate.load_state = true;
} }
init_rng(device);
cart::rom::init(device, rom_contents); // cart needs to come before rdram cart::rom::init(device, rom_contents); // cart needs to come before rdram
// rdram pointer is shared with parallel-rdp and retroachievements // rdram pointer is shared with parallel-rdp and retroachievements
rdram::init(device); rdram::init(device);
ui::video::init(device); ui::video::init(device, netplay_config.is_some());
ui::audio::init(device); ui::audio::init(device);
ui::input::init(&mut device.ui); 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 // must be after video init
let (discord_watch_tx, discord_handle) = if ra_config.enabled && device.netplay.is_none() { 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) 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); retroachievements::unload_game(discord_watch_tx, discord_handle);
ui::storage::write_saves(device); ui::storage::write_saves(device);
if let Some(netplay) = &mut device.netplay {
netplay::close(netplay);
}
ui::input::close(&mut device.ui); ui::input::close(&mut device.ui);
ui::audio::close(&mut device.ui); ui::audio::close(&mut device.ui);
ui::video::close(&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() 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(); let mut rng_seed = set_rng().next_u64();
if let Some(netplay) = &mut device.netplay { if let Some(netplay) = &mut device.netplay {
if netplay.player_number == 0 { 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.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>> { fn swap_rom(contents: Vec<u8>) -> Option<Vec<u8>> {
@@ -316,7 +349,7 @@ impl Device {
load_state: false, load_state: false,
load_rewind: false, load_rewind: false,
last_rewind_saved: 0.0, 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(), std::collections::BTreeMap::new(),
)), )),
}, },
-13
View File
@@ -1,5 +1,4 @@
use crate::device; use crate::device;
use crate::netplay;
use crate::ui; use crate::ui;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@@ -122,18 +121,6 @@ pub fn dma_write(
} }
pub fn init(device: &mut device::Device, rom_file: &[u8]) { 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.sc64.cfg[device::cart::sc64::SC64_BOOTLOADER_SWITCH as usize] = 1;
device.cart.rom = rom_file.to_vec(); 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 data.data_size & 0xFFFFFF; // length
device.cart.sc64.usb_buffer = data.data; // store the data to be read device.cart.sc64.usb_buffer = data.data; // store the data to be read
} }
Err(err) => { Err(_err) => {
match err { device.cart.sc64.regs[SC64_DATA0_REG] = 0; // read_status/type
tokio::sync::broadcast::error::TryRecvError::Lagged(_) => { device.cart.sc64.regs[SC64_DATA1_REG] = 0; // length
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
}
}
} }
} }
} else { } 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 { let input = if let Some(netplay) = &mut device.netplay {
netplay.inputs[channel].0 netplay.inputs[channel].0
} else { } 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()); device.pif.ram[offset..offset + 4].copy_from_slice(&input.data.to_ne_bytes());
+3 -3
View File
@@ -104,7 +104,7 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
savestates::process_savestates(device); savestates::process_savestates(device);
} }
if !netplay::in_rollback(device.netplay.as_ref()) { if !netplay::in_rollback(device.netplay.as_ref()) {
let _ = device.ui.video.fps_tx.as_ref().unwrap().try_send(true); let _ = device.ui.video.fps_tx.as_ref().unwrap().send(true);
} }
} }
} }
@@ -118,7 +118,6 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
pub fn update_screen(device: &mut device::Device) { pub fn update_screen(device: &mut device::Device) {
if !netplay::in_rollback(device.netplay.as_ref()) { if !netplay::in_rollback(device.netplay.as_ref()) {
ui::video::render_frame(); ui::video::render_frame();
unsafe { sdl3_sys::events::SDL_PumpEvents() };
} }
let (speed_limiter_toggled, paused) = ui::video::check_callback(device); let (speed_limiter_toggled, paused) = ui::video::check_callback(device);
@@ -140,7 +139,7 @@ pub fn update_screen(device: &mut device::Device) {
if !netplay::in_rollback(device.netplay.as_ref()) { if !netplay::in_rollback(device.netplay.as_ref()) {
ui::video::update_screen(); ui::video::update_screen();
device.speed_limiter.frame_counter += 1; device.speed_limiter.frame_counter += 1;
let _ = device.ui.video.vis_tx.as_ref().unwrap().try_send(true); let _ = device.ui.video.vis_tx.as_ref().unwrap().send(true);
} }
if device.netplay.is_some() { if device.netplay.is_some() {
@@ -221,6 +220,7 @@ fn speed_limiter(device: &mut device::Device) {
if now < deadline { if now < deadline {
let dur = deadline - now; let dur = deadline - now;
spin_sleep::sleep(dur); spin_sleep::sleep(dur);
if dur < device.speed_limiter.min_wait_time { if dur < device.speed_limiter.min_wait_time {
device.speed_limiter.min_wait_time = dur; device.speed_limiter.min_wait_time = dur;
} }
+59 -37
View File
@@ -22,6 +22,8 @@ pub struct Args {
#[arg(short, long)] #[arg(short, long)]
pub fullscreen: bool, pub fullscreen: bool,
#[arg(long)] #[arg(long)]
pub widescreen: bool,
#[arg(long)]
pub overclock: Option<bool>, pub overclock: Option<bool>,
#[arg(long)] #[arg(long)]
pub disable_expansion_pak: Option<bool>, 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<()> { pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
let dirs = ui::get_dirs(); let dirs = ui::get_dirs();
@@ -115,8 +130,6 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
ui::sdl_hints(); ui::sdl_hints();
if let Some(game) = args.game { if let Some(game) = args.game {
ui::disable_auto_update_joysticks();
let file_path = std::path::Path::new(&game).to_path_buf(); let file_path = std::path::Path::new(&game).to_path_buf();
let Some(rom_contents) = device::get_rom_contents(&file_path) else { let Some(rom_contents) = device::get_rom_contents(&file_path) else {
return Err(Error::other(format!( 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); let mut device = device::Device::new(true);
device.ui.config.recent_roms.retain(|x| *x != game); device.ui.config.ui.recent_roms.retain(|x| *x != game);
device.ui.config.recent_roms.insert(0, game); device.ui.config.ui.recent_roms.insert(0, game);
device.ui.config.recent_roms.truncate(5); device.ui.config.ui.recent_roms.truncate(5);
if args.fullscreen { if args.fullscreen {
device.ui.video.fullscreen = true; device.ui.video.fullscreen = true;
} else { } else {
device.ui.video.fullscreen = device.ui.config.video.fullscreen; 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 let overclock = args
.overclock .overclock
.unwrap_or(device.ui.config.emulation.overclock); .unwrap_or(device.ui.config.emulation.overclock);
@@ -169,25 +189,7 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
let mut shutdown_tx = None; let mut shutdown_tx = None;
let mut usb_handle = None; let mut usb_handle = None;
let ice_config_path = dirs.cache_dir.join("ice_config.json"); if args.netplay_server_addr.is_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,
ice_config_path.clone(),
device::cart::rom::is_system_pal(&rom_contents),
);
if device.netplay.is_none() {
return Err(Error::other("Failed to connect to netplay server"));
}
} else {
for i in 0..4 { for i in 0..4 {
if device.ui.config.input.transfer_pak[i] if device.ui.config.input.transfer_pak[i]
&& !device.ui.config.input.gb_rom_path[i].is_empty() && !device.ui.config.input.gb_rom_path[i].is_empty()
@@ -213,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()); let username = args.ra_username.unwrap_or(ra_config.username.clone());
retroachievements::init_client( 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 ra_config.hardcore
} else { } else {
false false
@@ -231,15 +233,29 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
tx.send(false).unwrap(); tx.send(false).unwrap();
} }
tokio::task::block_in_place(move || { rx.blocking_recv().unwrap();
rx.blocking_recv().unwrap();
});
ra_config ra_config
} else { } else {
retroachievements::RAConfig::default() 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( device::run_game(
&mut device, &mut device,
&rom_contents, &rom_contents,
@@ -250,15 +266,14 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
load_savestate_slot: args.load_state, load_savestate_slot: args.load_state,
}, },
ra_config, ra_config,
netplay_config,
); );
// on Android, the client is shut down in the app_window function // on Android, the client is shut down in the app_window function
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
retroachievements::shutdown_client(); retroachievements::shutdown_client();
if device.netplay.is_some() { if device.netplay.is_none() {
netplay::close(ice_config_path);
} else {
for i in 0..4 { for i in 0..4 {
if device.ui.config.input.transfer_pak[i] if device.ui.config.input.transfer_pak[i]
&& !device.ui.config.input.gb_ram_path[i].is_empty() && !device.ui.config.input.gb_ram_path[i].is_empty()
@@ -278,11 +293,11 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
let mut config = ui::config::Config::new(); let mut config = ui::config::Config::new();
if let Some(profile) = args.configure_input_profile { if let Some(profile) = args.configure_input_profile {
ui::input::configure_input_profile( ui::input_profile::configure_input_profile(
&mut config, &mut config,
profile, profile,
args.use_dinput, args.use_dinput,
args.deadzone.unwrap_or(ui::input::DEADZONE_DEFAULT), args.deadzone.unwrap_or(ui::input_profile::DEADZONE_DEFAULT),
); );
ui::sdl_close(); ui::sdl_close();
@@ -326,8 +341,10 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
{ {
let app = ui::gui::AppWindow::new().unwrap(); 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(); set_app_id();
ui::gui::app_window(&app, false); ui::gui::app_window(&app, false, no_intro_map);
} }
} }
@@ -337,8 +354,10 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
#[tokio::main] fn android_main(app: slint::android::AndroidApp) {
async 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::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::PollEvent::Main(main_event) => match main_event {
slint::android::android_activity::MainEvent::TerminateWindow { .. } => { slint::android::android_activity::MainEvent::TerminateWindow { .. } => {
@@ -370,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("saves")).unwrap();
std::fs::create_dir_all(dirs.data_dir.join("states")).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::WEAK_SLINT_WINDOW.lock().unwrap() = None;
*android::ANDROID_APP.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; use clap::Parser;
#[tokio::main] fn main() -> std::io::Result<()> {
async fn main() -> std::io::Result<()> { let (close_tx, handle) = gopher64::create_runtime();
let _guard = handle.enter();
let args = gopher64::Args::parse(); 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
} }
+189 -81
View File
@@ -1,5 +1,4 @@
use crate::device; use crate::device;
use crate::savestates;
use crate::ui; use crate::ui;
use sha2::digest::Digest; use sha2::digest::Digest;
@@ -20,15 +19,12 @@ impl ggrs::Config for GgrsConfig {
pub struct MatchboxChannel { pub struct MatchboxChannel {
channel: matchbox_socket::WebRtcChannel, channel: matchbox_socket::WebRtcChannel,
disconnected_peers: tokio::sync::mpsc::Sender<matchbox_socket::PeerId>,
} }
impl ggrs::NonBlockingSocket<matchbox_socket::PeerId> for MatchboxChannel { impl ggrs::NonBlockingSocket<matchbox_socket::PeerId> for MatchboxChannel {
fn send_to(&mut self, msg: &ggrs::Message, addr: &matchbox_socket::PeerId) { fn send_to(&mut self, msg: &ggrs::Message, addr: &matchbox_socket::PeerId) {
let encoded = postcard::to_stdvec(msg).expect("serialization failed"); let encoded = postcard::to_stdvec(msg).expect("serialization failed");
if self.channel.try_send(encoded.into(), *addr).is_err() { let _ = self.channel.try_send(encoded.into(), *addr);
self.disconnected_peers.try_send(*addr).unwrap();
}
} }
fn receive_all_messages(&mut self) -> Vec<(matchbox_socket::PeerId, ggrs::Message)> { fn receive_all_messages(&mut self) -> Vec<(matchbox_socket::PeerId, ggrs::Message)> {
@@ -43,7 +39,16 @@ impl ggrs::NonBlockingSocket<matchbox_socket::PeerId> for MatchboxChannel {
} }
} }
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 struct Netplay {
pub disconnected: bool,
pub session: ggrs::P2PSession<GgrsConfig>, pub session: ggrs::P2PSession<GgrsConfig>,
pub reliable_channel: matchbox_socket::WebRtcChannel, pub reliable_channel: matchbox_socket::WebRtcChannel,
pub peers: Vec<matchbox_socket::PeerId>, pub peers: Vec<matchbox_socket::PeerId>,
@@ -55,7 +60,7 @@ pub struct Netplay {
pub inputs: Vec<(ui::input::InputData, ggrs::InputStatus)>, pub inputs: Vec<(ui::input::InputData, ggrs::InputStatus)>,
pub requests: std::collections::VecDeque<ggrs::GgrsRequest<GgrsConfig>>, pub requests: std::collections::VecDeque<ggrs::GgrsRequest<GgrsConfig>>,
pub incoming_message: Vec<u8>, pub incoming_message: Vec<u8>,
pub disconnected_peers: tokio::sync::mpsc::Receiver<matchbox_socket::PeerId>, pub ice_config_path: std::path::PathBuf,
} }
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
@@ -100,6 +105,7 @@ fn process_reliable_messages(netplay: &mut Netplay) {
.insert(decoded_message.name, decoded_message.data); .insert(decoded_message.name, decoded_message.data);
netplay.incoming_message.clear(); netplay.incoming_message.clear();
check_input_delay(netplay); check_input_delay(netplay);
check_disconnect(netplay);
} }
} }
} }
@@ -124,7 +130,7 @@ fn receive_message(netplay: &mut Netplay, name: &str) -> Vec<u8> {
fn send_player_number( fn send_player_number(
channel: &mut matchbox_socket::WebRtcChannel, channel: &mut matchbox_socket::WebRtcChannel,
peers: Vec<matchbox_socket::PeerId>, peers: &[matchbox_socket::PeerId],
player_number: usize, player_number: usize,
) { ) {
let message = NetplayMessage { let message = NetplayMessage {
@@ -133,7 +139,7 @@ fn send_player_number(
}; };
let data = postcard::to_stdvec(&message).unwrap(); let data = postcard::to_stdvec(&message).unwrap();
for peer in peers { for peer in peers {
if let Err(e) = channel.try_send(data.clone().into(), peer) { if let Err(e) = channel.try_send(data.clone().into(), *peer) {
eprintln!("Failed to send message: {}", e); eprintln!("Failed to send message: {}", e);
} }
} }
@@ -195,6 +201,10 @@ 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) { pub fn send_input_delay(netplay: &mut Netplay, input_delay: usize) {
if input_delay < 1 {
return;
}
let message = NetplayMessage { let message = NetplayMessage {
name: "input_delay".to_string(), name: "input_delay".to_string(),
data: (input_delay as u64).to_be_bytes().to_vec(), data: (input_delay as u64).to_be_bytes().to_vec(),
@@ -226,6 +236,16 @@ fn check_input_delay(netplay: &mut Netplay) {
} }
} }
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 { fn pending_frames(netplay: &Netplay) -> usize {
netplay netplay
.requests .requests
@@ -242,21 +262,6 @@ pub fn in_rollback(netplay: Option<&Netplay>) -> bool {
} }
} }
fn process_disconnected_peers(netplay: &mut Netplay) {
while let Ok(addr) = netplay.disconnected_peers.try_recv() {
for handle in netplay.session.handles_by_address(addr) {
if let Err(e) = netplay.session.disconnect_player(handle) {
eprintln!("Error disconnecting player: {}", e);
} else {
ui::video::onscreen_message(
&format!("Player {} disconnected", handle + 1),
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
}
}
}
}
pub fn process_requests( pub fn process_requests(
device: &mut device::Device, device: &mut device::Device,
) -> Vec<(ui::input::InputData, ggrs::InputStatus)> { ) -> Vec<(ui::input::InputData, ggrs::InputStatus)> {
@@ -264,7 +269,7 @@ pub fn process_requests(
if let Some(request) = device.netplay.as_mut().unwrap().requests.pop_front() { if let Some(request) = device.netplay.as_mut().unwrap().requests.pop_front() {
match request { match request {
ggrs::GgrsRequest::SaveGameState { cell, frame } => { 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(); let mut hasher = sha2::Sha256::new();
for reg in device.cpu.cop0.regs.as_ref() { for reg in device.cpu.cop0.regs.as_ref() {
@@ -274,8 +279,9 @@ pub fn process_requests(
cell.save(frame, Some(frame), Some(hash)); cell.save(frame, Some(frame), Some(hash));
} }
ggrs::GgrsRequest::LoadGameState { cell, frame: _ } => { ggrs::GgrsRequest::LoadGameState { cell, frame: _ } => {
if let Some(frame) = cell.load() { eprintln!("attempting to load game state");
savestates::load_savestate(device, true, Some(frame)); if let Some(_frame) = cell.load() {
// savestates::load_savestate(device, true, Some(frame));
} }
} }
ggrs::GgrsRequest::AdvanceFrame { inputs } => { ggrs::GgrsRequest::AdvanceFrame { inputs } => {
@@ -283,26 +289,25 @@ pub fn process_requests(
} }
} }
} else { } else {
// unsafe { sdl3_sys::events::SDL_PumpEvents() }; // so the screen doesn't freeze
process_netplay(device); process_netplay(device);
} }
} }
} }
fn process_netplay(device: &mut device::Device) { fn poll_clients(netplay: &mut Netplay) {
let netplay = device.netplay.as_mut().unwrap();
process_disconnected_peers(netplay);
netplay.session.poll_remote_clients(); netplay.session.poll_remote_clients();
for event in netplay.session.events() { for event in netplay.session.events() {
match event { match event {
ggrs::GgrsEvent::Synchronizing { .. } => {} ggrs::GgrsEvent::Synchronizing { .. } => {}
ggrs::GgrsEvent::Synchronized { .. } => {} ggrs::GgrsEvent::Synchronized { .. } => {}
ggrs::GgrsEvent::Disconnected { .. } => { ggrs::GgrsEvent::Disconnected { .. } => {
ui::video::onscreen_message( if !netplay.disconnected {
"Lost connection to peer", netplay.disconnected = true;
ui::video::MESSAGE_LENGTH_MESSAGE_LONG, ui::video::onscreen_message(
); "Lost connection to peer(s)",
ui::video::MESSAGE_LENGTH_MESSAGE_LONG,
);
}
} }
ggrs::GgrsEvent::NetworkInterrupted { .. } => { ggrs::GgrsEvent::NetworkInterrupted { .. } => {
println!("network interrupted"); println!("network interrupted");
@@ -322,19 +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); process_reliable_messages(netplay);
advance_frame(device); advance_frame(device);
} }
fn advance_frame(device: &mut device::Device) { fn advance_frame(device: &mut device::Device) {
let netplay = device.netplay.as_mut().unwrap(); 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(); let local_handle = *netplay.session.local_player_handles().first().unwrap();
netplay netplay
.session .session
.add_local_input(local_handle, local_input) .add_local_input(local_handle, local_input)
.unwrap(); .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() { match netplay.session.advance_frame() {
Ok(requests) => { Ok(requests) => {
netplay.requests.extend(requests); netplay.requests.extend(requests);
@@ -346,19 +383,40 @@ fn advance_frame(device: &mut device::Device) {
} }
} }
fn verify_peers(
peers: &[matchbox_socket::PeerId],
player_numbers: &std::collections::BTreeMap<usize, Option<matchbox_socket::PeerId>>,
) -> bool {
for peer in player_numbers.values() {
if let Some(peer) = peer
&& !peers.contains(peer)
{
return false;
}
}
true
}
fn create_socket(builder: matchbox_socket::WebRtcSocketBuilder) -> matchbox_socket::WebRtcSocket {
let (socket, loop_fut) = builder.build();
tokio::spawn(async move {
if let Err(e) = loop_fut.await {
eprintln!("WebRTC loop failed: {}", e);
}
});
socket
}
pub fn init( pub fn init(
server_addr: String, device: &mut device::Device,
player_number: usize, netplay_config: &NetplayConfig,
number_of_players: usize,
input_delay: usize,
ice_config_path: std::path::PathBuf,
pal: bool, pal: bool,
) -> Option<Netplay> { ) -> Option<Netplay> {
let mut builder = matchbox_socket::WebRtcSocketBuilder::new(server_addr) let mut builder = matchbox_socket::WebRtcSocketBuilder::new(&netplay_config.server_addr)
.add_unreliable_channel() .add_unreliable_channel()
.add_reliable_channel(); .add_reliable_channel();
if let Ok(ice_config) = std::fs::read(&ice_config_path) if let Ok(ice_config) = std::fs::read(&netplay_config.ice_config_path)
&& let Ok(ice_config) = serde_json::from_slice::<RtcIceServerConfig>(&ice_config) && let Ok(ice_config) = serde_json::from_slice::<RtcIceServerConfig>(&ice_config)
{ {
builder = builder.ice_server(matchbox_socket::RtcIceServerConfig { builder = builder.ice_server(matchbox_socket::RtcIceServerConfig {
@@ -370,50 +428,83 @@ pub fn init(
eprintln!("Using default ICE config"); eprintln!("Using default ICE config");
} }
let (mut socket, loop_fut) = builder.build(); let mut socket = create_socket(builder.clone());
tokio::spawn(async move {
if let Err(e) = loop_fut.await {
eprintln!("WebRTC loop failed: {}", e);
}
});
let (disconnected_peers_tx, disconnected_peers_rx) = tokio::sync::mpsc::channel(100); let mut now = std::time::Instant::now();
let matchbox_channel = MatchboxChannel { let mut message_timer = now;
channel: socket.take_channel(0).unwrap(), let socket_timeout = std::time::Duration::from_secs_f64(rand::random_range(8.0..10.0));
disconnected_peers: disconnected_peers_tx, let mut player_numbers =
}; std::collections::BTreeMap::from([(netplay_config.player_number, None)]);
let mut reliable_channel = socket.take_channel(1).unwrap();
let now = std::time::Instant::now(); ui::video::onscreen_message(
let timeout = std::time::Duration::from_secs(10); "Connecting to netplay peers...\nPlease wait...",
let mut player_numbers = std::collections::BTreeMap::new(); ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
player_numbers.insert(player_number, None); );
loop {
socket.update_peers(); device.cpu.running = true;
let peers = socket 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() .connected_peers()
.collect::<Vec<matchbox_socket::PeerId>>(); .collect::<Vec<matchbox_socket::PeerId>>();
send_player_number(&mut reliable_channel, peers, player_number); if connected_peers.len() == netplay_config.number_of_players - 1 {
get_player_numbers(&mut reliable_channel, &mut player_numbers); send_player_number(
if player_numbers.len() == number_of_players { socket.channel_mut(1),
break; &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 { if message_timer.elapsed() > std::time::Duration::from_secs(4) {
eprintln!("Could not connect to netplay peers"); ui::video::onscreen_message(
return None; &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)); 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() let mut session_builder = ggrs::SessionBuilder::<GgrsConfig>::new()
.with_num_players(number_of_players) .with_num_players(netplay_config.number_of_players)
.unwrap() .unwrap()
.with_input_delay(input_delay) .with_input_delay(netplay_config.input_delay)
.with_fps(if pal { 50 } else { 60 }) .with_fps(if pal { 50 } else { 60 })
.unwrap() .unwrap()
.with_desync_detection_mode(ggrs::DesyncDetection::On { interval: 60 }) .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) { .with_disconnect_timeout(std::time::Duration::from_secs(if cfg!(debug_assertions) {
10 10
} else { } else {
@@ -434,6 +525,11 @@ pub fn init(
} }
} }
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) if matchbox_channel.channel.config().max_retransmits != Some(0)
|| matchbox_channel.channel.config().ordered || matchbox_channel.channel.config().ordered
{ {
@@ -443,36 +539,48 @@ pub fn init(
let mut session = session_builder.start_p2p_session(matchbox_channel).unwrap(); let mut session = session_builder.start_p2p_session(matchbox_channel).unwrap();
let now = std::time::Instant::now(); let now = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(10);
while session.current_state() != ggrs::SessionState::Running { while session.current_state() != ggrs::SessionState::Running {
session.poll_remote_clients(); session.poll_remote_clients();
if now.elapsed() > timeout { if now.elapsed() > timeout {
eprintln!("Could not start netplay session"); eprintln!("Could not start netplay session");
return None; return None;
} }
unsafe { sdl3_sys::events::SDL_PumpEvents() };
std::thread::sleep(std::time::Duration::from_millis(10)); std::thread::sleep(std::time::Duration::from_millis(10));
} }
Some(Netplay { Some(Netplay {
disconnected_peers: disconnected_peers_rx, disconnected: false,
incoming_message: vec![], incoming_message: vec![],
input_delay, input_delay: netplay_config.input_delay,
session, session,
reliable_channel, reliable_channel,
peers, peers,
player_number, player_number: netplay_config.player_number,
connected: [ connected: [
number_of_players > 0, netplay_config.number_of_players > 0,
number_of_players > 1, netplay_config.number_of_players > 1,
number_of_players > 2, netplay_config.number_of_players > 2,
number_of_players > 3, netplay_config.number_of_players > 3,
], ],
inputs: Vec::new(), inputs: Vec::new(),
requests: std::collections::VecDeque::new(), requests: std::collections::VecDeque::new(),
received_data: std::collections::VecDeque::new(), received_data: std::collections::VecDeque::new(),
messages: std::collections::HashMap::new(), messages: std::collections::HashMap::new(),
ice_config_path: netplay_config.ice_config_path.clone(),
}) })
} }
pub fn close(ice_config_path: std::path::PathBuf) { pub fn close(netplay: &mut Netplay) {
let _ = std::fs::remove_file(ice_config_path); 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 -3
View File
@@ -150,9 +150,8 @@ pub fn load_game(
let tx_ptr = Box::into_raw(Box::new(tx)) as *mut std::ffi::c_void; 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); ra_load_game(rom.as_ptr(), rom_size, tx_ptr);
}; };
tokio::task::block_in_place(move || {
rx.blocking_recv().unwrap(); rx.blocking_recv().unwrap();
});
let mut c_title = std::ptr::null(); let mut c_title = std::ptr::null();
let mut c_image_url = std::ptr::null(); let mut c_image_url = std::ptr::null();
+40 -52
View File
@@ -78,7 +78,7 @@ pub struct Savestate {
pub last_rewind_saved: f64, pub last_rewind_saved: f64,
#[serde(skip)] #[serde(skip)]
pub rewind_pool: 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 { pub struct SavestateData {
@@ -88,11 +88,11 @@ pub struct SavestateData {
ra_state: Vec<u8>, ra_state: Vec<u8>,
} }
static DEVICE_CLONE: std::sync::LazyLock<std::sync::Mutex<Box<device::Device>>> = static DEVICE_CLONE: std::sync::LazyLock<tokio::sync::Mutex<Box<device::Device>>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(device::Device::new(false))); std::sync::LazyLock::new(|| tokio::sync::Mutex::new(device::Device::new(false)));
static SAVES_CLONE: std::sync::LazyLock<std::sync::Mutex<ui::storage::Saves>> = static SAVES_CLONE: std::sync::LazyLock<tokio::sync::Mutex<ui::storage::Saves>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(ui::storage::Saves::default())); std::sync::LazyLock::new(|| tokio::sync::Mutex::new(ui::storage::Saves::default()));
pub fn process_savestates(device: &mut device::Device) { pub fn process_savestates(device: &mut device::Device) {
let save_rewind = if device.netplay.is_none() let save_rewind = if device.netplay.is_none()
@@ -126,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()]; let mut ra_state: Vec<u8> = vec![0; retroachievements::state_size()];
retroachievements::save_state(ra_state.as_mut_ptr(), ra_state.len()); retroachievements::save_state(ra_state.as_mut_ptr(), ra_state.len());
if let Ok(mut device_clone) = DEVICE_CLONE.lock() DEVICE_CLONE.blocking_lock().clone_state(device);
&& let Ok(mut saves_clone) = SAVES_CLONE.lock() SAVES_CLONE
{ .blocking_lock()
device_clone.clone_state(device); .clone_from(&device.ui.storage.saves);
saves_clone.clone_from(&device.ui.storage.saves);
} else {
ui::video::onscreen_message(
"Failed to create savestate",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
return;
}
let save_path = device.ui.storage.paths.savestate_file_path.clone(); let save_path = device.ui.storage.paths.savestate_file_path.clone();
let save_state_slot = device.ui.storage.save_state_slot; let save_state_slot = device.ui.storage.save_state_slot;
@@ -147,39 +139,32 @@ pub fn create_savestate(device: &mut device::Device, rewind: bool, rewind_frame:
let mut error = false; let mut error = false;
if rewind { if rewind {
if let Ok(device_clone) = DEVICE_CLONE.lock() let mut pool = rewind_pool.lock().await;
&& let Ok(saves_clone) = SAVES_CLONE.lock() let mut state = device::Device::new(false);
&& let Ok(mut pool) = rewind_pool.lock() state.clone_state(DEVICE_CLONE.lock().await.deref());
{ let key = if let Some(key) = rewind_frame {
let mut state = device::Device::new(false); key
state.clone_state(device_clone.deref()); } else if let Some(key) = pool.keys().last() {
let key = if let Some(key) = rewind_frame { key + 1
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();
}
} else { } 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 { } else {
let compressed_file = if let Ok(device_clone) = DEVICE_CLONE.lock() let compressed_file = if let Ok(device_data) =
&& let Ok(saves_clone) = SAVES_CLONE.lock() postcard::to_stdvec(DEVICE_CLONE.lock().await.deref())
&& let Ok(device_data) = postcard::to_stdvec(device_clone.deref()) && let Ok(saves_data) = postcard::to_stdvec(SAVES_CLONE.lock().await.deref())
&& let Ok(saves_data) = postcard::to_stdvec(saves_clone.deref())
&& let Ok(compressed_file) = ui::storage::compress_file(&[ && let Ok(compressed_file) = ui::storage::compress_file(&[
(&device_data, "device"), (&device_data, "device"),
(&saves_data, "saves"), (&saves_data, "saves"),
@@ -229,17 +214,20 @@ pub fn load_savestate(device: &mut device::Device, rewind: bool, rewind_frame: O
let timeout = std::time::Duration::from_secs(1); let timeout = std::time::Duration::from_secs(1);
let now = std::time::Instant::now(); let now = std::time::Instant::now();
loop { loop {
if let Ok(mut pool) = device.savestate.rewind_pool.lock() if let Some(state) = device
&& pool.contains_key(&rewind_frame) .savestate
.rewind_pool
.blocking_lock()
.remove(&rewind_frame)
{ {
break pool.remove(&rewind_frame); break Some(state);
} }
if now.elapsed() > timeout { if now.elapsed() > timeout {
break None; break None;
} }
std::thread::sleep(std::time::Duration::from_millis(1));
} }
} else if let Ok(mut pool) = device.savestate.rewind_pool.lock() } else if let Some((_key, state)) = device.savestate.rewind_pool.blocking_lock().pop_last()
&& let Some((_key, state)) = pool.pop_last()
{ {
Some(state) Some(state)
} else { } else {
+11 -20
View File
@@ -7,6 +7,7 @@ pub mod config;
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
pub mod gui; pub mod gui;
pub mod input; pub mod input;
pub mod input_profile;
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
pub mod netplay; pub mod netplay;
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
@@ -49,7 +50,6 @@ unsafe impl Sync for Audio {}
#[derive(Default)] #[derive(Default)]
pub struct Input { pub struct Input {
pub keyboard_state: *const bool, pub keyboard_state: *const bool,
pub last_polled: u64,
pub controllers: [input::Controllers; 4], pub controllers: [input::Controllers; 4],
} }
@@ -68,10 +68,11 @@ pub struct Storage {
pub struct Video { pub struct Video {
pub window: *mut sdl3_sys::video::SDL_Window, pub window: *mut sdl3_sys::video::SDL_Window,
pub fullscreen: bool, pub fullscreen: bool,
pub fps_tx: Option<tokio::sync::mpsc::Sender<bool>>, pub widescreen: bool,
pub fps_rx: Option<tokio::sync::mpsc::Receiver<bool>>, pub fps_tx: Option<tokio::sync::mpsc::UnboundedSender<bool>>,
pub vis_tx: Option<tokio::sync::mpsc::Sender<bool>>, pub fps_rx: Option<tokio::sync::mpsc::UnboundedReceiver<bool>>,
pub vis_rx: Option<tokio::sync::mpsc::Receiver<bool>>, pub vis_tx: Option<tokio::sync::mpsc::UnboundedSender<bool>>,
pub vis_rx: Option<tokio::sync::mpsc::UnboundedReceiver<bool>>,
} }
unsafe impl Send for Video {} unsafe impl Send for Video {}
@@ -79,8 +80,8 @@ unsafe impl Sync for Video {}
#[derive(Default)] #[derive(Default)]
pub struct Usb { pub struct Usb {
pub usb_tx: Option<tokio::sync::broadcast::Sender<usb::UsbData>>, pub usb_tx: Option<tokio::sync::mpsc::UnboundedSender<usb::UsbData>>,
pub cart_rx: Option<tokio::sync::broadcast::Receiver<usb::UsbData>>, pub cart_rx: Option<tokio::sync::mpsc::UnboundedReceiver<usb::UsbData>>,
} }
#[derive(Clone)] #[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) { pub fn sdl_init(flag: sdl3_sys::init::SDL_InitFlags) {
unsafe { unsafe {
if sdl3_sys::init::SDL_WasInit(flag) == 0 && !sdl3_sys::init::SDL_InitSubSystem(flag) { if sdl3_sys::init::SDL_WasInit(flag) == 0 && !sdl3_sys::init::SDL_InitSubSystem(flag) {
@@ -191,11 +182,10 @@ impl Ui {
pub fn new() -> Ui { pub fn new() -> Ui {
let dirs = get_dirs(); let dirs = get_dirs();
let (fps_tx, fps_rx) = tokio::sync::mpsc::channel(1000); let (fps_tx, fps_rx) = tokio::sync::mpsc::unbounded_channel();
let (vis_tx, vis_rx) = tokio::sync::mpsc::channel(1000); let (vis_tx, vis_rx) = tokio::sync::mpsc::unbounded_channel();
Ui { Ui {
input: Input { input: Input {
last_polled: 0,
controllers: [ controllers: [
input::Controllers { input::Controllers {
game_controller: std::ptr::null_mut(), game_controller: std::ptr::null_mut(),
@@ -278,6 +268,7 @@ impl Ui {
video: Video { video: Video {
window: std::ptr::null_mut(), window: std::ptr::null_mut(),
fullscreen: false, fullscreen: false,
widescreen: false,
fps_tx: Some(fps_tx), fps_tx: Some(fps_tx),
fps_rx: Some(fps_rx), fps_rx: Some(fps_rx),
vis_tx: Some(vis_tx), vis_tx: Some(vis_tx),
+192 -196
View File
@@ -1,4 +1,5 @@
use crate::Args; use crate::Args;
use crate::create_runtime;
use crate::run; use crate::run;
use crate::ui; use crate::ui;
use clap::Parser; use clap::Parser;
@@ -15,9 +16,9 @@ const RUN_ROM: jint = 3;
pub static ANDROID_APP: std::sync::Mutex<Option<slint::android::AndroidApp>> = pub static ANDROID_APP: std::sync::Mutex<Option<slint::android::AndroidApp>> =
std::sync::Mutex::new(None); 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>>>, 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>>> = pub static WEAK_SLINT_WINDOW: std::sync::Mutex<Option<slint::Weak<ui::gui::AppWindow>>> =
std::sync::Mutex::new(None); 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)] #[unsafe(no_mangle)]
#[tokio::main] pub extern "C" fn gopher64_sdl_main(
pub async extern "C" fn gopher64_sdl_main(
argc: std::ffi::c_int, argc: std::ffi::c_int,
argv: *mut *mut std::ffi::c_char, argv: *mut *mut std::ffi::c_char,
) -> std::ffi::c_int { ) -> std::ffi::c_int {
let (close_tx, handle) = create_runtime();
let _guard = handle.enter();
let raw = argv_to_strings(argc, argv); let raw = argv_to_strings(argc, argv);
let args = Args::try_parse_from(raw).unwrap(); let args = Args::try_parse_from(raw).unwrap();
if let Err(err) = run(args, argc as usize) { if let Err(err) = run(args, argc as usize) {
close_tx.send(()).unwrap();
eprintln!("Error running game: {err:?}"); eprintln!("Error running game: {err:?}");
return 1; return 1;
} }
close_tx.send(()).unwrap();
0 0
} }
@@ -177,9 +182,17 @@ pub fn spawn_configure_input_profile(
dinput: bool, dinput: bool,
deadzone: i32, deadzone: i32,
) { ) {
if let Some(vm) = get_vm() { if let Ok(app) = ANDROID_APP.lock()
if let Err(err) = vm.attach_current_thread(|env| { && let Some(app) = app.as_ref()
start_configure_input_profile_on_jvm(env, profile_name.to_string(), dinput, deadzone) {
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:?}"); eprintln!("JNI error while starting N64Activity: {err:?}");
} }
@@ -188,45 +201,40 @@ pub fn spawn_configure_input_profile(
fn start_configure_input_profile_on_jvm( fn start_configure_input_profile_on_jvm(
env: &mut Env<'_>, env: &mut Env<'_>,
app: &slint::android::AndroidApp,
profile_name: String, profile_name: String,
dinput: bool, dinput: bool,
deadzone: i32, deadzone: i32,
) -> jni::errors::Result<()> { ) -> jni::errors::Result<()> {
if let Ok(app) = ANDROID_APP.lock() let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
&& let Some(app) = app.as_ref() 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 package_name = JString::from_str(env, "io.github.gopher64.gopher64")?;
let class_name = JString::from_str(env, "io.github.gopher64.gopher64.N64Activity")?; let class_name = JString::from_str(env, "io.github.gopher64.gopher64.N64Activity")?;
let args_key = JString::from_str(env, "args")?; let args_key = JString::from_str(env, "args")?;
let mut args = vec![ let mut args = vec![
JString::from_str(env, "--configure-input-profile")?, JString::from_str(env, "--configure-input-profile")?,
JString::from_str(env, &profile_name)?, JString::from_str(env, &profile_name)?,
JString::from_str(env, "--deadzone")?, JString::from_str(env, "--deadzone")?,
JString::from_str(env, &deadzone.to_string())?, JString::from_str(env, &deadzone.to_string())?,
]; ];
if dinput { if dinput {
args.push(JString::from_str(env, "--use-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 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( pub fn run_rom(
@@ -235,9 +243,11 @@ pub fn run_rom(
netplay: Option<ui::gui::NetplayDevice>, netplay: Option<ui::gui::NetplayDevice>,
weak: slint::Weak<ui::gui::AppWindow>, weak: slint::Weak<ui::gui::AppWindow>,
) { ) {
if let Some(vm) = get_vm() { if let Ok(app) = ANDROID_APP.lock()
if let Err(err) = vm.attach_current_thread(|env| { && let Some(app) = app.as_ref()
start_run_rom_on_jvm(env, file_path, game_settings, netplay, weak) {
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:?}"); eprintln!("JNI error while starting N64Activity: {err:?}");
} }
@@ -246,97 +256,88 @@ pub fn run_rom(
fn start_run_rom_on_jvm( fn start_run_rom_on_jvm(
env: &mut Env<'_>, env: &mut Env<'_>,
app: &slint::android::AndroidApp,
file_path: std::path::PathBuf, file_path: std::path::PathBuf,
game_settings: ui::GameSettings, game_settings: ui::GameSettings,
netplay: Option<ui::gui::NetplayDevice>, netplay: Option<ui::gui::NetplayDevice>,
weak: slint::Weak<ui::gui::AppWindow>, weak: slint::Weak<ui::gui::AppWindow>,
) -> jni::errors::Result<()> { ) -> jni::errors::Result<()> {
if let Ok(app) = ANDROID_APP.lock() let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
&& let Some(app) = app.as_ref() 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 package_name = JString::from_str(env, "io.github.gopher64.gopher64")?;
let class_name = JString::from_str(env, "io.github.gopher64.gopher64.N64Activity")?; 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_key = JString::from_str(env, "file_path")?;
let file_path = file_path.to_str().unwrap(); let file_path = file_path.to_str().unwrap();
let cheats_path_key = JString::from_str(env, "cheats_path")?; let cheats_path_key = JString::from_str(env, "cheats_path")?;
let cheats_path = app let cheats_path = app
.internal_data_path() .internal_data_path()
.unwrap() .unwrap()
.join("cache") .join("cache")
.join("cheats.json"); .join("cheats.json");
let args_key = JString::from_str(env, "args")?; let args_key = JString::from_str(env, "args")?;
let mut args = vec![ let mut args = vec![
JString::from_str(env, file_path)?, JString::from_str(env, file_path)?,
JString::from_str(env, "--fullscreen")?, JString::from_str(env, "--fullscreen")?,
JString::from_str(env, "--overclock")?, JString::from_str(env, "--overclock")?,
JString::from_str(env, &game_settings.overclock.to_string())?, JString::from_str(env, &game_settings.overclock.to_string())?,
JString::from_str(env, "--disable-expansion-pak")?, JString::from_str(env, "--disable-expansion-pak")?,
JString::from_str(env, &game_settings.disable_expansion_pak.to_string())?, JString::from_str(env, &game_settings.disable_expansion_pak.to_string())?,
]; ];
if let Some(netplay) = netplay { 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.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")?);
args.push(JString::from_str(env, &netplay.player_number.to_string())?); 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")?);
args.push(JString::from_str( args.push(JString::from_str(
env, env,
&netplay.number_of_players.to_string(), &netplay.number_of_players.to_string(),
)?); )?);
args.push(JString::from_str(env, "--netplay-input-delay")?); 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, &netplay.input_delay.to_string())?);
args.push(JString::from_str(env, "--cheats")?); args.push(JString::from_str(env, "--cheats")?);
args.push(JString::from_str(env, cheats_path.to_str().unwrap())?); args.push(JString::from_str(env, cheats_path.to_str().unwrap())?);
let f = std::fs::File::create(&cheats_path).unwrap(); let f = std::fs::File::create(&cheats_path).unwrap();
serde_json::to_writer_pretty(f, &game_settings.cheats).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 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> { fn get_vm(app: &slint::android::AndroidApp) -> JavaVM {
if let Ok(app) = ANDROID_APP.lock() unsafe { JavaVM::from_raw(app.vm_as_ptr().cast()) }
&& let Some(app) = app.as_ref()
{
Some(unsafe { JavaVM::from_raw(app.vm_as_ptr().cast()) })
} else {
None
}
} }
pub fn decode_path(path: &str) -> String { pub fn decode_path(path: &str) -> String {
if let Some(vm) = get_vm() { if let Ok(app) = ANDROID_APP.lock()
match vm.attach_current_thread(|env| decode_path_on_jvm(env, path)) { && 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, Ok(decoded_path) => decoded_path,
Err(err) => { Err(err) => {
eprintln!("JNI error while decoding path: {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. /// Lists connected gamepads and joysticks using the Android framework.
pub fn list_controllers() -> Vec<ControllerInfo> { pub fn list_controllers() -> Vec<ControllerInfo> {
if let Some(vm) = get_vm() { if let Ok(app) = ANDROID_APP.lock()
match vm.attach_current_thread(list_controllers_on_jvm) { && let Some(app) = app.as_ref()
{
match get_vm(app).attach_current_thread(list_controllers_on_jvm) {
Ok(controllers) => controllers, Ok(controllers) => controllers,
Err(err) => { Err(err) => {
eprintln!("JNI error while listing controllers: {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). /// 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) { 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(); 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:?}"); eprintln!("JNI error while opening URI: {err:?}");
} }
} }
} }
fn open_uri_on_jvm(env: &mut Env<'_>, path: &str) -> jni::errors::Result<()> { fn open_uri_on_jvm(
if let Ok(app) = ANDROID_APP.lock() env: &mut Env<'_>,
&& let Some(app) = app.as_ref() app: &slint::android::AndroidApp,
{ path: &str,
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject; ) -> jni::errors::Result<()> {
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 uri_string = JString::from_str(env, path)?; let uri_string = JString::from_str(env, path)?;
let uri = AndroidUri::parse(env, &uri_string)?; let uri = AndroidUri::parse(env, &uri_string)?;
let action_view = AndroidIntent::ACTION_VIEW(env)?; let action_view = AndroidIntent::ACTION_VIEW(env)?;
let intent = AndroidIntent::with_action(env, &action_view)?.set_data(env, &uri)?; let intent = AndroidIntent::with_action(env, &action_view)?.set_data(env, &uri)?;
activity.as_ref().start_activity(env, &intent)?; activity.as_ref().start_activity(env, &intent)?;
Ok(()) Ok(())
} else {
Err(jni::errors::Error::UninitializedJavaVM)
}
} }
pub async fn select_rom(rom_dir: slint::SharedString) -> Option<std::path::PathBuf> { pub async fn select_rom(rom_dir: slint::SharedString) -> Option<std::path::PathBuf> {
if let Some(vm) = get_vm() { let (tx, rx) = tokio::sync::oneshot::channel::<Option<std::path::PathBuf>>();
if let Err(err) = SELECT_ROM_TX.lock().await.replace(tx);
vm.attach_current_thread(|env| select_rom_on_jvm(env, rom_dir.to_string()))
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:?}"); eprintln!("JNI error while opening URI: {err:?}");
return None; 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 { } else {
eprintln!("Android app not initialized"); 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> { 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 select_rom(slint::SharedString::new()).await
} }
fn select_rom_on_jvm(env: &mut Env<'_>, rom_dir: String) -> jni::errors::Result<()> { fn select_rom_on_jvm(
if let Ok(app) = ANDROID_APP.lock() env: &mut Env<'_>,
&& let Some(app) = app.as_ref() app: &slint::android::AndroidApp,
{ rom_dir: String,
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject; ) -> jni::errors::Result<()> {
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 action = AndroidIntent::ACTION_OPEN_DOCUMENT(env)?; let action = AndroidIntent::ACTION_OPEN_DOCUMENT(env)?;
let category = AndroidIntent::CATEGORY_OPENABLE(env)?; let category = AndroidIntent::CATEGORY_OPENABLE(env)?;
let mime_type = JString::from_str(env, "*/*")?; let mime_type = JString::from_str(env, "*/*")?;
let mut intent = AndroidIntent::with_action(env, &action)? let mut intent = AndroidIntent::with_action(env, &action)?
.set_type(env, &mime_type)? .set_type(env, &mime_type)?
.add_category(env, &category)?; .add_category(env, &category)?;
if !rom_dir.is_empty() { if !rom_dir.is_empty() {
let start_dir = JString::from_str(env, rom_dir)?; let start_dir = JString::from_str(env, rom_dir)?;
let extra_initial_uri = DocumentsContract::EXTRA_INITIAL_URI(env)?; let extra_initial_uri = DocumentsContract::EXTRA_INITIAL_URI(env)?;
intent = intent.put_extra_string(env, &extra_initial_uri, &start_dir)?; 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)
} }
activity.start_activity_for_result(env, &intent, REQUEST_SELECT_ROM)?;
Ok(())
} }
pub fn get_dirs() -> ui::Dirs { 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> { 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(); 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, Ok(file) => file,
Err(err) => { Err(err) => {
eprintln!("JNI error while opening URI: {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( fn get_file_from_uri_on_jvm(
env: &mut Env<'_>, env: &mut Env<'_>,
app: &slint::android::AndroidApp,
path: String, path: String,
) -> jni::errors::Result<Option<std::fs::File>> { ) -> jni::errors::Result<Option<std::fs::File>> {
if let Ok(app) = ANDROID_APP.lock() let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
&& let Some(app) = app.as_ref() let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? };
{ let path = JString::from_str(env, path)?;
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject; let mode = JString::from_str(env, "r")?;
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? }; let uri = AndroidUri::parse(env, &path)?;
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 content_resolver = activity.as_ref().get_content_resolver(env)?;
let parcel_file_descriptor = content_resolver.open_file_descriptor(env, &uri, &mode); let parcel_file_descriptor = content_resolver.open_file_descriptor(env, &uri, &mode);
if let Ok(descriptor) = parcel_file_descriptor if let Ok(descriptor) = parcel_file_descriptor
&& !descriptor.is_null() && !descriptor.is_null()
{ {
let owned_fd = unsafe { std::os::fd::OwnedFd::from_raw_fd(descriptor.detach_fd(env)?) }; let owned_fd = unsafe { std::os::fd::OwnedFd::from_raw_fd(descriptor.detach_fd(env)?) };
let file = std::fs::File::from(owned_fd); let file = std::fs::File::from(owned_fd);
descriptor.close(env)?; descriptor.close(env)?;
return Ok(Some(file)); return Ok(Some(file));
} else {
return Ok(None);
}
} else { } else {
eprintln!("Android app not initialized"); return Ok(None);
return Err(jni::errors::Error::UninitializedJavaVM);
} }
} }
@@ -595,8 +592,7 @@ pub extern "system" fn Java_io_github_gopher64_gopher64_SlintActivity_nativeOnAc
if result_code == AndroidActivity::RESULT_OK(env)? { if result_code == AndroidActivity::RESULT_OK(env)? {
match request_code { match request_code {
REQUEST_SELECT_ROM => { REQUEST_SELECT_ROM => {
if let Ok(mut tx_lock) = SELECT_ROM_TX.lock() if let Some(tx) = SELECT_ROM_TX.blocking_lock().take()
&& let Some(tx) = tx_lock.take()
&& !intent_data.is_null() && !intent_data.is_null()
{ {
let result_intent = let result_intent =
+6 -2
View File
@@ -3,17 +3,21 @@ use crate::ui::gui::AppWindow;
use crate::{cheats, device}; use crate::{cheats, device};
use slint::ComponentHandle; 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(); let weak = app.as_weak();
app.on_cheats_select_rom_clicked(move |rom_dir| { app.on_cheats_select_rom_clicked(move |rom_dir| {
let select_rom = ui::gui::select_rom(rom_dir); let select_rom = ui::gui::select_rom(rom_dir);
let weak = weak.clone(); let weak = weak.clone();
let no_intro_map = no_intro_map.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Some(file) = select_rom.await if let Some(file) = select_rom.await
&& let Some(rom_contents) = device::get_rom_contents(&file) && let Some(rom_contents) = device::get_rom_contents(&file)
{ {
let game_crc = ui::storage::get_game_crc(&rom_contents); 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| { weak.upgrade_in_event_loop(move |handle| {
handle.set_cheat_game_name(game_name.into()); handle.set_cheat_game_name(game_name.into());
}) })
+44 -20
View File
@@ -1,34 +1,36 @@
use crate::ui; use crate::ui;
#[derive(Copy, Clone, serde::Serialize, serde::Deserialize)] #[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct InputKeyButton { pub struct InputKeyButton {
pub enabled: bool,
pub id: i32, pub id: i32,
} }
#[derive(PartialEq, Copy, Clone, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Clone, serde::Serialize, serde::Deserialize)]
pub struct InputControllerAxis { pub struct InputControllerAxis {
pub enabled: bool,
pub id: i32, pub id: i32,
pub axis: i16, pub axis: i16,
pub initial_state: i16, pub initial_state: i16,
} }
#[derive(Copy, Clone, serde::Serialize, serde::Deserialize)] #[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct InputJoystickHat { pub struct InputJoystickHat {
pub enabled: bool,
pub id: i32, pub id: i32,
pub direction: u8, 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 struct InputProfile {
pub keys: [InputKeyButton; ui::input::PROFILE_SIZE], pub inputs: [[Option<InputItem>; 2]; ui::input_profile::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 dinput: bool, pub dinput: bool,
pub deadzone: i32, pub deadzone: i32,
} }
@@ -69,13 +71,19 @@ pub struct Cheats {
pub cheats: rustc_hash::FxHashMap<String, rustc_hash::FxHashMap<String, Option<String>>>, 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)] #[derive(Default, serde::Serialize, serde::Deserialize)]
pub struct Config { pub struct Config {
pub input: Input, pub input: Input,
pub video: Video, pub video: Video,
pub emulation: Emulation, pub emulation: Emulation,
pub rom_dir: std::path::PathBuf, pub ui: Ui,
pub recent_roms: Vec<String>,
#[serde(skip)] #[serde(skip)]
write_to_disk: bool, write_to_disk: bool,
} }
@@ -136,17 +144,30 @@ impl Config {
let dirs = ui::get_dirs(); let dirs = ui::get_dirs();
let file_path = dirs.config_dir.join("config.json"); let file_path = dirs.config_dir.join("config.json");
let config_file = std::fs::read(file_path); let config_file = std::fs::read(file_path);
let mut input_data: Option<Input> = None;
if let Ok(config_file) = config_file { if let Ok(config_file) = config_file {
let result = serde_json::from_slice::<Config>(config_file.as_ref()); let result = serde_json::from_slice::<Config>(config_file.as_ref());
if let Ok(mut result) = result { if let Ok(mut result) = result {
result.write_to_disk = true; result.write_to_disk = true;
return result; 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(); 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 { Config {
input: Input { input: input_data.unwrap_or(Input {
input_profile_binding: [ input_profile_binding: [
"default".to_string(), "default".to_string(),
"default".to_string(), "default".to_string(),
@@ -160,7 +181,7 @@ impl Config {
gb_rom_path: [String::new(), String::new(), String::new(), String::new()], gb_rom_path: [String::new(), String::new(), String::new(), String::new()],
gb_ram_path: [String::new(), String::new(), String::new(), String::new()], gb_ram_path: [String::new(), String::new(), String::new(), String::new()],
emulate_vru: false, emulate_vru: false,
}, }),
video: Video { video: Video {
upscale: 1, upscale: 1,
ssaa: false, ssaa: false,
@@ -176,8 +197,11 @@ impl Config {
usb: false, usb: false,
rewind: false, rewind: false,
}, },
rom_dir: std::path::PathBuf::new(), ui: Ui {
recent_roms: Vec::new(), theme: 0,
rom_dir: std::path::PathBuf::new(),
recent_roms: Vec::new(),
},
write_to_disk: true, write_to_disk: true,
} }
} }
+91 -13
View File
@@ -1,3 +1,4 @@
use crate::device;
use crate::retroachievements; use crate::retroachievements;
use crate::ui; use crate::ui;
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
@@ -34,13 +35,18 @@ fn check_latest_version(weak: slint::Weak<AppWindow>) {
if let Ok(response) = response { if let Ok(response) = response {
let data: Result<GithubData, reqwest::Error> = response.json().await; let data: Result<GithubData, reqwest::Error> = response.json().await;
let latest_version = if let Ok(data) = data let latest_version = match data {
&& let Ok(github_version) = semver::Version::parse(&data.tag_name[1..]) Ok(data) => match semver::Version::parse(&data.tag_name[1..]) {
{ Ok(github_version) => github_version,
github_version Err(e) => {
} else { eprintln!("Error parsing latest version from GitHub: {}", e);
eprintln!("Error getting latest version from GitHub"); semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap()
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(); let current_version = semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
if current_version < latest_version { 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( app.set_recent_roms(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from( slint::VecModel::from(
config config
.ui
.recent_roms .recent_roms
.iter() .iter()
.filter(|x| rom_exists(x)) .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_widescreen(config.video.widescreen);
app.set_vsync(config.video.vsync); app.set_vsync(config.video.vsync);
app.set_apply_crt_shader(config.video.crt); app.set_apply_crt_shader(config.video.crt);
app.set_theme(config.ui.theme);
app.set_overclock_n64_cpu(config.emulation.overclock); app.set_overclock_n64_cpu(config.emulation.overclock);
app.set_disable_expansion_pak(config.emulation.disable_expansion_pak); app.set_disable_expansion_pak(config.emulation.disable_expansion_pak);
app.set_emulate_usb(config.emulation.usb); 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); 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()); 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 || { app.on_input_profile_button_clicked(move || {
weak_app weak_app
.upgrade_in_event_loop(move |handle| { .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_profile_name(String::new().into());
handle.set_input_dinput(false); handle.set_input_dinput(false);
handle.set_show_input_profile(true); 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) { pub fn save_settings(app: &AppWindow) {
let mut config = ui::config::Config::new(); 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.integer_scaling = app.get_integer_scaling();
config.video.ssaa = app.get_ssaa(); config.video.ssaa = app.get_ssaa();
config.video.fullscreen = app.get_fullscreen(); config.video.fullscreen = app.get_fullscreen();
config.video.widescreen = app.get_widescreen(); config.video.widescreen = app.get_widescreen();
config.video.vsync = app.get_vsync(); config.video.vsync = app.get_vsync();
config.video.crt = app.get_apply_crt_shader(); config.video.crt = app.get_apply_crt_shader();
config.ui.theme = app.get_theme();
config.emulation.overclock = app.get_overclock_n64_cpu(); config.emulation.overclock = app.get_overclock_n64_cpu();
config.emulation.disable_expansion_pak = app.get_disable_expansion_pak(); config.emulation.disable_expansion_pak = app.get_disable_expansion_pak();
config.emulation.usb = app.get_emulate_usb(); 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); retroachievements::init_client(false, false, false);
app.set_is_android(is_android); app.set_is_android(is_android);
about_window(app); about_window(app);
@@ -527,8 +545,8 @@ pub fn app_window(app: &AppWindow, is_android: bool) {
controller_window(app, &config); controller_window(app, &config);
local_game_window(app, &config); local_game_window(app, &config);
} }
ui::netplay::netplay_window(app); ui::netplay::netplay_window(app, no_intro_map.clone());
ui::cheats::cheats_window(app); ui::cheats::cheats_window(app, no_intro_map);
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
{ {
@@ -652,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))); 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> { pub async fn select_rom(rom_dir: slint::SharedString) -> Option<std::path::PathBuf> {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
return ui::android::select_rom(rom_dir).await; return ui::android::select_rom(rom_dir).await;
+23 -5
View File
@@ -37,7 +37,7 @@ export component AppWindow inherits Window {
callback netplay_begin_game(int); callback netplay_begin_game(int);
callback netplay_send_chat_message(string); callback netplay_send_chat_message(string);
callback netplay_refresh_sessions; 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_select_rom(string);
callback netplay_join_session(string, string, string, string); callback netplay_join_session(string, string, string, string);
callback netplay_close; callback netplay_close;
@@ -61,10 +61,11 @@ export component AppWindow inherits Window {
in-out property ra_challenge <=> RAData.challenge; in-out property ra_challenge <=> RAData.challenge;
in-out property ra_leaderboard <=> RAData.leaderboard; in-out property ra_leaderboard <=> RAData.leaderboard;
in-out property ra_rich_presence <=> RAData.rich_presence; 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; out property ra_not_logged_in <=> RAData.not_logged_in;
in-out property integer_scaling <=> SettingsData.integer_scaling; in-out property integer_scaling <=> SettingsData.integer_scaling;
in-out property ssaa <=> SettingsData.ssaa; in-out property ssaa <=> SettingsData.ssaa;
in-out property theme <=> SettingsData.theme;
in-out property fullscreen <=> SettingsData.fullscreen; in-out property fullscreen <=> SettingsData.fullscreen;
in-out property widescreen <=> SettingsData.widescreen; in-out property widescreen <=> SettingsData.widescreen;
in-out property vsync <=> SettingsData.vsync; in-out property vsync <=> SettingsData.vsync;
@@ -131,11 +132,26 @@ export component AppWindow inherits Window {
private property <length> safe-bottom: root.safe-area-insets.bottom; 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-width: root.width - root.safe-left - root.safe-right;
private property <length> safe-height: root.height - root.safe-top - root.safe-bottom; 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-width: 320px;
min-height: 240px; min-height: 240px;
preferred-width: 800px; preferred-width: 800px;
preferred-height: 600px; preferred-height: 650px;
title: "Gopher64"; title: "Gopher64";
icon: @image-url("../../../data/icon/gopher64.png"); icon: @image-url("../../../data/icon/gopher64.png");
@@ -256,8 +272,8 @@ export component AppWindow inherits Window {
select_rom(rom_dir) => { select_rom(rom_dir) => {
netplay_select_rom(rom_dir); netplay_select_rom(rom_dir);
} }
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); netplay_create_session(a,b,c,d,e,f,g,h);
} }
close => { close => {
netplay_close(); netplay_close();
@@ -385,5 +401,7 @@ export component AppWindow inherits Window {
NetplayWaitData.connected_peers_count = 0; NetplayWaitData.connected_peers_count = 0;
NetplayData.pending_session = false; NetplayData.pending_session = false;
NetplayData.pending_refresh = 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 { export component NetplayCreate inherits Page {
title: @tr("Netplay: Create Session"); title: @tr("Netplay: Create Session");
callback select_rom(string); 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; callback close;
VerticalBox { VerticalBox {
@@ -98,7 +98,9 @@ export component NetplayCreate inherits Page {
NetplayData.game_name, NetplayData.game_name,
NetplayData.game_hash, NetplayData.game_hash,
NetplayData.game_cheats, NetplayData.game_cheats,
NetplayData.password); NetplayData.password,
SettingsData.overclock_n64_cpu,
SettingsData.disable_expansion_pak);
} }
} }
Button { Button {
+7 -6
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 { NetplayData } from "netplay_page.slint";
import { Page } from "page.slint"; import { Page } from "page.slint";
@@ -103,12 +103,13 @@ export component NetplayWait inherits Page {
vertical-alignment: center; vertical-alignment: center;
} }
ComboBox { SpinBox {
model: ["0", "1", "2", "3", "4", "5", "6", "7", "8"]; minimum: 1;
current-index: NetplayWaitData.input_delay; maximum: 16;
value: NetplayWaitData.input_delay;
enabled: NetplayWaitData.recommended_delay != 0; enabled: NetplayWaitData.recommended_delay != 0;
selected => { edited => {
NetplayWaitData.input_delay = self.current-index; NetplayWaitData.input_delay = self.value;
} }
} }
+2 -2
View File
@@ -12,7 +12,7 @@ export global RAData {
in-out property <bool> rich_presence; in-out property <bool> rich_presence;
in-out property <bool> ra_logging_in; in-out property <bool> ra_logging_in;
in-out property <bool> ra_show_profile; 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_username;
in-out property <string> ra_password; in-out property <string> ra_password;
in-out property <string> current_user_message; 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); 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"); text: @tr("Hardcore mode");
checked: RAData.hardcore; checked: RAData.hardcore;
enabled: RAData.enabled; enabled: RAData.enabled;
+131 -105
View File
@@ -1,7 +1,7 @@
// Copyright © SixtyFPS GmbH <info@slint.dev> // Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT // 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"; import { Page } from "page.slint";
export global SettingsData { export global SettingsData {
@@ -14,6 +14,7 @@ export global SettingsData {
in-out property <bool> overclock_n64_cpu; in-out property <bool> overclock_n64_cpu;
in-out property <bool> disable_expansion_pak; in-out property <bool> disable_expansion_pak;
in-out property <int> resolution; in-out property <int> resolution;
in-out property <int> theme;
in-out property <bool> emulate_usb; in-out property <bool> emulate_usb;
in-out property <bool> rewind; in-out property <bool> rewind;
in-out property <bool> emulate_vru; in-out property <bool> emulate_vru;
@@ -23,117 +24,142 @@ export global SettingsData {
export component Settings inherits Page { export component Settings inherits Page {
title: @tr("Settings"); title: @tr("Settings");
VerticalBox { HorizontalBox {
alignment: start; alignment: center;
HorizontalBox { VerticalBox {
alignment: center; alignment: start;
VerticalBox { GroupBox {
alignment: start; title: @tr("Game");
HorizontalBox { VerticalBox {
alignment: start; alignment: start;
Text { HorizontalBox {
text: @tr("Resolution:"); alignment: start;
vertical-alignment: center; Text {
text: @tr("Resolution:");
vertical-alignment: center;
}
ComboBox {
model: ["1x", "2x", "4x", "8x"];
current-index: SettingsData.resolution;
selected => {
SettingsData.resolution = self.current-index;
}
}
} }
ComboBox { Switch {
model: ["1x", "2x", "4x", "8x"]; text: @tr("SSAA downscaling");
current-index: SettingsData.resolution; checked: SettingsData.ssaa;
selected => { toggled => {
SettingsData.resolution = self.current-index; 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 { GroupBox {
text: @tr("SSAA downscaling"); title: @tr("UI");
checked: SettingsData.ssaa; VerticalBox {
toggled => { alignment: start;
SettingsData.ssaa = self.checked; 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;
}
}
} }
} Rectangle {
height: 40px; // so the combobox isn't cut off
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;
} }
} }
} }
+5
View File
@@ -82,6 +82,11 @@ component SideBarItem inherits Rectangle {
} }
button := SideBarButton { button := SideBarButton {
Tooltip {
Text {
text: root.label;
}
}
icon: root.icon; icon: root.icon;
selected: root.selected; selected: root.selected;
has-focus: root.has-focus; 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,
}
}
+196 -151
View File
@@ -25,6 +25,12 @@ enum MessageType {
ReceiveChatMessage, ReceiveChatMessage,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ServerAddresses {
pub lobby: String,
pub game: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct NetplaySession { struct NetplaySession {
password: Option<String>, password: Option<String>,
@@ -34,7 +40,7 @@ struct NetplaySession {
client_version: Option<String>, client_version: Option<String>,
features: Option<std::collections::HashMap<String, String>>, features: Option<std::collections::HashMap<String, String>>,
players: Vec<String>, players: Vec<String>,
server_address: Option<String>, server_address: Option<ServerAddresses>,
input_delay: Option<usize>, input_delay: Option<usize>,
ice_server_config: Option<RtcIceServerConfig>, ice_server_config: Option<RtcIceServerConfig>,
} }
@@ -59,13 +65,17 @@ struct NetplayPingMessage {
num_of_peers: usize, 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); let select_rom = ui::gui::select_rom(rom_dir);
tokio::spawn(async move { tokio::spawn(async move {
if let Some(file) = select_rom.await { if let Some(file) = select_rom.await {
if let Some(rom_contents) = device::get_rom_contents(&file) { if let Some(rom_contents) = device::get_rom_contents(&file) {
let hash = device::cart::rom::calculate_hash(&rom_contents); 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 pal = device::cart::rom::is_system_pal(&rom_contents);
let game_crc = ui::storage::get_game_crc(&rom_contents); let game_crc = ui::storage::get_game_crc(&rom_contents);
let cheats = ui::config::Cheats::new(); let cheats = ui::config::Cheats::new();
@@ -102,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, app: &AppWindow,
game_settings: ui::GameSettings, netplay_write_sender: &tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
netplay_write_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>, netplay_read_receiver: &tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
netplay_read_receiver: tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>, netplay_read_sender: &tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
netplay_read_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>, netplay_write_receiver: &tokio::sync::broadcast::Receiver<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_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 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( 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( manage_websocket(
netplay_read_sender.clone(), netplay_read_sender_create_session.clone(),
netplay_write_receiver.resubscribe(), netplay_write_receiver_create_session.resubscribe(),
); );
create_session( create_session(
netplay_write_sender.clone(), netplay_write_sender_create_session.clone(),
netplay_read_receiver.resubscribe(), netplay_read_receiver_create_session.resubscribe(),
close_ping_rx.resubscribe(), close_ping_rx_create_session.resubscribe(),
session_name.to_string(), session_name.to_string(),
player_name.to_string(), player_name.to_string(),
game_name.to_string(), game_name.to_string(),
game_hash.to_string(), game_hash.to_string(),
game_cheats.to_string(), game_cheats.to_string(),
password.to_string(), password.to_string(),
game_settings.clone(), overclock,
disable_expansion_pak,
weak.clone(), 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); app.set_show_netplay_create_session(true);
} }
@@ -142,13 +290,16 @@ fn manage_websocket(
netplay_read_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>, netplay_read_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
mut netplay_write_receiver: tokio::sync::broadcast::Receiver<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 request
.headers_mut() .headers_mut()
.insert("Authorization", env!("NETPLAY_ID").parse().unwrap()); .insert("Authorization", env!("NETPLAY_ID").parse().unwrap());
tokio::spawn(async move { tokio::spawn(async move {
match tokio::time::timeout( match tokio::time::timeout(
std::time::Duration::from_secs(2), std::time::Duration::from_secs(3),
tokio_tungstenite::connect_async(request), tokio_tungstenite::connect_async(request),
) )
.await .await
@@ -246,7 +397,8 @@ fn create_session(
game_hash: String, game_hash: String,
game_cheats: String, game_cheats: String,
password: String, password: String,
game_settings: ui::GameSettings, overclock: bool,
disable_expansion_pak: bool,
weak_app: slint::Weak<AppWindow>, weak_app: slint::Weak<AppWindow>,
) { ) {
tokio::spawn(async move { tokio::spawn(async move {
@@ -256,10 +408,10 @@ fn create_session(
features.insert("cheats".to_string(), game_cheats); 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( features.insert(
"disable_expansion_pak".to_string(), "disable_expansion_pak".to_string(),
game_settings.disable_expansion_pak.to_string(), disable_expansion_pak.to_string(),
); );
let session = NetplaySession { let session = NetplaySession {
@@ -503,11 +655,12 @@ fn update_ping(
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap() .unwrap()
.as_millis(); .as_millis();
let ping = (now - message.timestamp) as f64 / 2.0; // calculate one-way latency let ping = (now - message.timestamp) as f64 / 2.0;
pings.push(ping); pings.push(ping);
if pings.len() > message.num_of_peers * 4 { if pings.len() > message.num_of_peers * 4 {
// once we have enough samples, average the 3 highest values // once we have enough samples, average the 3 highest values
pings.sort_by(f64::total_cmp); 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; let ping_avg = pings.iter().rev().take(3).sum::<f64>() / 3.0;
pings.clear(); pings.clear();
weak_app weak_app
@@ -519,7 +672,7 @@ fn update_ping(
}; };
let latency_frames = (ping_avg / (1000.0 / refresh_rate)).ceil() as i32; let latency_frames = (ping_avg / (1000.0 / refresh_rate)).ceil() as i32;
let recommendation = let recommendation =
(latency_frames + 1).min(8); (latency_frames + 1).min(16);
if handle.get_netplay_recommended_delay() == 0 { if handle.get_netplay_recommended_delay() == 0 {
handle.set_netplay_input_delay(recommendation); handle.set_netplay_input_delay(recommendation);
@@ -561,7 +714,7 @@ fn setup_wait_window(
netplay_write_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>, netplay_write_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
mut netplay_read_receiver: tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>, mut netplay_read_receiver: tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
close_ping_rx: tokio::sync::broadcast::Receiver<()>, close_ping_rx: tokio::sync::broadcast::Receiver<()>,
server_addr: String, server_addresses: ServerAddresses,
session_name: slint::SharedString, session_name: slint::SharedString,
ice_server_config: Option<RtcIceServerConfig>, ice_server_config: Option<RtcIceServerConfig>,
game_name: slint::SharedString, game_name: slint::SharedString,
@@ -571,7 +724,7 @@ fn setup_wait_window(
) { ) {
update_ice_config(ice_server_config.clone()); update_ice_config(ice_server_config.clone());
update_ping( update_ping(
server_addr.clone(), server_addresses.lobby.clone(),
ice_server_config, ice_server_config,
close_ping_rx, close_ping_rx,
app.as_weak(), app.as_weak(),
@@ -589,26 +742,6 @@ fn setup_wait_window(
}; };
netplay_write_sender.send(Some(request_update)).unwrap(); 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(); let weak_app = app.as_weak();
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {
@@ -669,7 +802,7 @@ fn setup_wait_window(
load_savestate_slot: None, load_savestate_slot: None,
}, },
Some(ui::gui::NetplayDevice { Some(ui::gui::NetplayDevice {
server_addr, server_addr: server_addresses.game.clone(),
player_number, player_number,
number_of_players: players.row_count(), number_of_players: players.row_count(),
input_delay, input_delay,
@@ -719,8 +852,10 @@ fn setup_join_window(
netplay_read_receiver: tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>, netplay_read_receiver: tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
netplay_read_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>, netplay_read_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
netplay_write_receiver: tokio::sync::broadcast::Receiver<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_ping_rx: tokio::sync::broadcast::Receiver<()>,
) { ) {
close_connections(&netplay_write_sender, &netplay_read_sender, &close_ping_tx);
manage_websocket( manage_websocket(
netplay_read_sender.clone(), netplay_read_sender.clone(),
netplay_write_receiver.resubscribe(), netplay_write_receiver.resubscribe(),
@@ -728,22 +863,6 @@ fn setup_join_window(
app.set_netplay_pending_refresh(true); 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 weak = app.as_weak();
let mut read_receiver = netplay_read_receiver.resubscribe(); let mut read_receiver = netplay_read_receiver.resubscribe();
let write_sender = netplay_write_sender.clone(); let write_sender = netplay_write_sender.clone();
@@ -879,7 +998,10 @@ fn setup_join_window(
app.invoke_netplay_refresh_sessions(); 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): ( let (netplay_read_sender, netplay_read_receiver): (
tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>, tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>, tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
@@ -895,91 +1017,14 @@ pub fn netplay_window(app: &AppWindow) {
tokio::sync::broadcast::Receiver<()>, tokio::sync::broadcast::Receiver<()>,
) = tokio::sync::broadcast::channel(5); ) = tokio::sync::broadcast::channel(5);
let weak_app = app.as_weak(); setup_callbacks(
let write_sender_create = netplay_write_sender.clone(); app,
let read_receiver_create = netplay_read_receiver.resubscribe(); &netplay_write_sender,
let read_sender_create = netplay_read_sender.clone(); &netplay_read_receiver,
let write_receiver_create = netplay_write_receiver.resubscribe(); &netplay_read_sender,
let close_ping_create = close_ping_rx.resubscribe(); &netplay_write_receiver,
app.on_create_session_button_clicked(move || { &close_ping_tx,
let write_sender = write_sender_create.clone(); &close_ping_rx,
let read_receiver = read_receiver_create.resubscribe(); no_intro_map,
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");
});
} }
+1 -1
View File
@@ -20,7 +20,7 @@ pub fn ra_window(app: &ui::gui::AppWindow) {
} }
if !cfg!(ra_hardcore_enabled) { 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() { 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, 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) if let Ok(data) = String::from_utf8(data)
&& data == "N64" && data == "N64"
{ {
@@ -29,98 +29,98 @@ fn respond_to_handshake(usb_tx: &tokio::sync::broadcast::Sender<UsbData>, data:
async fn handle_connection( async fn handle_connection(
conn: tokio::net::TcpStream, conn: tokio::net::TcpStream,
mut shutdown_rx: tokio::sync::watch::Receiver<()>, mut shutdown_rx: tokio::sync::watch::Receiver<()>,
mut usb_rx: tokio::sync::broadcast::Receiver<UsbData>, usb_rx: &mut tokio::sync::mpsc::UnboundedReceiver<UsbData>,
usb_tx: tokio::sync::broadcast::Sender<UsbData>, usb_tx: tokio::sync::mpsc::UnboundedSender<UsbData>,
cart_tx: tokio::sync::broadcast::Sender<UsbData>, cart_tx: tokio::sync::mpsc::UnboundedSender<UsbData>,
) { ) {
let (mut incoming, mut outgoing) = conn.into_split(); let (mut incoming, mut outgoing) = conn.into_split();
let mut shutdown_rx_clone = shutdown_rx.clone(); let mut shutdown_rx_clone = shutdown_rx.clone();
while usb_rx.try_recv().is_ok() {} // drain stale USB messages
tokio::spawn(async move { 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 { loop {
tokio::select! { tokio::select! {
data = usb_rx.recv() => { result = incoming.read(&mut incoming_buffer) => {
match data { match result {
Ok(data) => { Ok(0) => {
let mut output: Vec<u8> = vec![]; break;
output.extend_from_slice(&data.data_type.to_be_bytes()); }
output.extend_from_slice(&data.data_size.to_be_bytes()); Ok(n) => {
output.extend_from_slice(&data.data); usb_buffer.extend_from_slice(&incoming_buffer[0..n]);
if outgoing.write_all(&output).await.is_err() { if data_type.is_none() {
break; 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(_)) => { Err(_e) => {
panic!("usb_rx lagged"); break;
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
break;
} }
} }
} }
_ = shutdown_rx_clone.changed() => { _ = shutdown_rx.changed() => {
break; 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 { loop {
tokio::select! { tokio::select! {
result = incoming.read(&mut incoming_buffer) => { data = usb_rx.recv() => {
match result { match data {
Ok(0) => { Some(data) => {
break; let mut output: Vec<u8> = vec![];
} output.extend_from_slice(&data.data_type.to_be_bytes());
Ok(n) => { output.extend_from_slice(&data.data_size.to_be_bytes());
usb_buffer.extend_from_slice(&incoming_buffer[0..n]); output.extend_from_slice(&data.data);
if data_type.is_none() { if outgoing.write_all(&output).await.is_err() {
if usb_buffer.len() < 4 { break;
continue;
} else {
data_type = Some(u32::from_be_bytes(usb_buffer[0..4].try_into().unwrap()));
usb_buffer.drain(0..4);
}
}
if data_type.is_some() && data_size.is_none() {
if usb_buffer.len() < 4 {
continue;
} else {
data_size = Some(u32::from_be_bytes(usb_buffer[0..4].try_into().unwrap()));
usb_buffer.drain(0..4);
}
}
if let Some(d_type) = data_type && let Some(d_size) = data_size {
let length = d_size as usize;
if usb_buffer.len() >= length {
let usb_data = UsbData {
data: usb_buffer[0..length].to_vec(),
data_type: d_type,
data_size: d_size,
};
usb_buffer.drain(0..length);
if usb_data.data_type == DATATYPE_TCPTEST {
respond_to_handshake(&usb_tx,usb_data.data);
} else if usb_data.data_type == DATATYPE_ROMUPLOAD {
panic!("ROM upload not supported");
} else {
cart_tx.send(usb_data).unwrap();
}
data_type = None;
data_size = None;
}
} }
} }
Err(_e) => { None => {
break; break;
} }
} }
} }
_ = shutdown_rx.changed() => { _ = shutdown_rx_clone.changed() => {
break; break;
} }
} }
@@ -133,24 +133,23 @@ pub fn init() -> (
ui::Usb, ui::Usb,
) { ) {
let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(()); let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(());
let (usb_tx, usb_rx): ( let (usb_tx, mut usb_rx): (
tokio::sync::broadcast::Sender<UsbData>, tokio::sync::mpsc::UnboundedSender<UsbData>,
tokio::sync::broadcast::Receiver<UsbData>, tokio::sync::mpsc::UnboundedReceiver<UsbData>,
) = tokio::sync::broadcast::channel(1024); ) = tokio::sync::mpsc::unbounded_channel();
let (cart_tx, cart_rx): ( let (cart_tx, cart_rx): (
tokio::sync::broadcast::Sender<UsbData>, tokio::sync::mpsc::UnboundedSender<UsbData>,
tokio::sync::broadcast::Receiver<UsbData>, tokio::sync::mpsc::UnboundedReceiver<UsbData>,
) = tokio::sync::broadcast::channel(1024); ) = tokio::sync::mpsc::unbounded_channel();
let usb_tx_clone = usb_tx.clone(); let usb_tx_clone = usb_tx.clone();
let cart_rx_clone = cart_rx.resubscribe();
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
if let Ok(listener) = tokio::net::TcpListener::bind("localhost:48646").await { if let Ok(listener) = tokio::net::TcpListener::bind("localhost:48646").await {
loop { loop {
tokio::select! { tokio::select! {
res = listener.accept() => { res = listener.accept() => {
if let Ok((c,_)) = res { 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 { } else {
break; break;
} }
@@ -169,7 +168,7 @@ pub fn init() -> (
Some(handle), Some(handle),
ui::Usb { ui::Usb {
usb_tx: Some(usb_tx_clone), 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(); 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_STANDARD_WIDTH: i32 = 320;
const NTSC_HEIGHT: i32 = 240; 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 { GFX_INFO {
RDRAM: device.rdram.mem.as_mut_ptr(), RDRAM: device.rdram.mem.as_mut_ptr(),
DMEM: device.rsp.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_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], DPC_STATUS_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_STATUS_REG],
PAL: device.cart.pal, PAL: device.cart.pal,
widescreen: device.ui.config.video.widescreen,
fullscreen: device.ui.video.fullscreen, fullscreen: device.ui.video.fullscreen,
vsync: if device.netplay.is_none() { widescreen: device.ui.video.widescreen,
vsync: if !netplay {
device.ui.config.video.vsync device.ui.config.video.vsync
} else { } else {
false 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::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
ui::ttf_init(); ui::ttf_init();
@@ -56,14 +56,14 @@ pub fn init(device: &mut device::Device) {
2 2
}; };
if device.cart.pal { if device.cart.pal {
window_width = if device.ui.config.video.widescreen { window_width = if device.ui.video.widescreen {
PAL_WIDESCREEN_WIDTH * scale PAL_WIDESCREEN_WIDTH * scale
} else { } else {
PAL_STANDARD_WIDTH * scale PAL_STANDARD_WIDTH * scale
}; };
window_height = PAL_HEIGHT * scale; window_height = PAL_HEIGHT * scale;
} else { } else {
window_width = if device.ui.config.video.widescreen { window_width = if device.ui.video.widescreen {
NTSC_WIDESCREEN_WIDTH * scale NTSC_WIDESCREEN_WIDTH * scale
} else { } else {
NTSC_STANDARD_WIDTH * scale NTSC_STANDARD_WIDTH * scale
@@ -99,7 +99,7 @@ pub fn init(device: &mut device::Device) {
sdl3_sys::everything::SDL_HideCursor(); sdl3_sys::everything::SDL_HideCursor();
} }
let gfx_info = build_gfx_info(device); let gfx_info = build_gfx_info(device, netplay);
unsafe { unsafe {
let font_bytes = include_bytes!("../../data/ui/RobotoMono-Regular.ttf"); 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) { 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 { unsafe {
rdp_load_state(gfx_info, rdp_state); rdp_load_state(gfx_info, rdp_state);
for reg in 0..device::vi::VI_REGS_COUNT { 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) { pub fn pause_loop(ui: &mut ui::Ui, frame_time: f64) {
let mut paused = true; let mut paused = true;
let mut frame_advance = false; let mut frame_advance = false;
let mut pause_counter = 0;
while paused && !frame_advance { while paused && !frame_advance {
std::thread::sleep(std::time::Duration::from_secs_f64(frame_time)); 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() }; unsafe { sdl3_sys::events::SDL_PumpEvents() };
ui::input::get(ui, 0); // to gather hotkey input
retroachievements::do_idle(); retroachievements::do_idle();
let callback = unsafe { rdp_check_callback() }; let callback = unsafe { rdp_check_callback() };
paused = callback.paused; paused = callback.paused;
frame_advance = callback.frame_advance; 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 if let Some(netplay) = &mut device.netplay
&& netplay.player_number == 0 && 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); netplay::send_input_delay(netplay, netplay.input_delay - 1);
} else if callback.increase_input_delay { } else if callback.increase_input_delay {
netplay::send_input_delay(netplay, netplay.input_delay + 1); 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) 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.sort();
dedup_words.dedup(); 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 = gui::VruDialog::new().unwrap();
let vru_dialog_weak = vru_dialog.as_weak(); let vru_dialog_weak = vru_dialog.as_weak();
vru_dialog.on_vru_button_clicked(move |chosen_word| { 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(); vru_dialog_weak.unwrap().hide().unwrap();
}); });