Compare commits

...

30 Commits

Author SHA1 Message Date
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
31 changed files with 690 additions and 663 deletions
Generated
+30 -177
View File
@@ -370,9 +370,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]]
name = "arrayvec"
version = "0.7.6"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe"
[[package]]
name = "as-raw-xcb-connection"
@@ -728,9 +728,9 @@ dependencies = [
[[package]]
name = "auto_enums"
version = "0.8.8"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65398a2893f41bce5c9259f6e1a4f03fbae40637c1bdc755b4f387f48c613b03"
checksum = "2e4487600931c9a89f8db7ffbdf3fbdd45bb7bd85e26861f659a463cd0dff966"
dependencies = [
"derive_utils",
"proc-macro2",
@@ -981,9 +981,9 @@ dependencies = [
[[package]]
name = "borsh"
version = "1.6.1"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a"
checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145"
dependencies = [
"bytes",
"cfg_aliases",
@@ -1064,9 +1064,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.11.1"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
[[package]]
name = "bzip2"
@@ -1148,9 +1148,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.64"
version = "1.2.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f"
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -2591,24 +2591,22 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.4.2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.1",
"wasip2",
"wasip3",
"wasm-bindgen",
]
[[package]]
name = "ggrs"
version = "0.12.0"
source = "git+https://github.com/gschup/ggrs.git#1f14703a5859c2bcb0fa789beab15e80b572775f"
source = "git+https://github.com/gschup/ggrs.git#6cbcbbfa0bf799a7bf4d8a1f0401ba7a15ae6f4f"
dependencies = [
"bincode 1.3.3",
"bitfield-rle",
@@ -2773,7 +2771,7 @@ dependencies = [
[[package]]
name = "gopher64"
version = "1.1.26"
version = "1.1.30"
dependencies = [
"bindgen",
"cc",
@@ -2867,9 +2865,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.4.14"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
dependencies = [
"atomic-waker",
"bytes",
@@ -3587,12 +3585,6 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a2c462a4d927d512f5f882a033ddd62f33a05bb9f230d98f736ac3dc85938f"
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "idna"
version = "1.1.0"
@@ -3668,8 +3660,6 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
"serde",
"serde_core",
]
[[package]]
@@ -3977,12 +3967,6 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lebe"
version = "0.5.3"
@@ -4120,9 +4104,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.32"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
dependencies = [
"value-bag",
]
@@ -5739,7 +5723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20 0.10.0",
"getrandom 0.4.2",
"getrandom 0.4.3",
"rand_core 0.10.1",
"serde",
]
@@ -6648,7 +6632,7 @@ dependencies = [
"bzip2",
"cbc 0.2.1",
"crc32fast",
"getrandom 0.4.2",
"getrandom 0.4.3",
"js-sys",
"lzma-rust2",
"ppmd-rust",
@@ -7200,9 +7184,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.117"
version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
@@ -7292,7 +7276,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.2",
"getrandom 0.4.3",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.61.2",
@@ -7999,7 +7983,7 @@ version = "1.23.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
dependencies = [
"getrandom 0.4.2",
"getrandom 0.4.3",
"js-sys",
"serde_core",
"wasm-bindgen",
@@ -8103,16 +8087,7 @@ version = "1.0.4+wasi-0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
dependencies = [
"wit-bindgen 0.57.1",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen 0.51.0",
"wit-bindgen",
]
[[package]]
@@ -8170,40 +8145,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags 2.13.0",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]]
name = "wayland-backend"
version = "0.3.15"
@@ -8254,9 +8195,9 @@ dependencies = [
[[package]]
name = "wayland-protocols"
version = "0.32.12"
version = "0.32.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f"
checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6"
dependencies = [
"bitflags 2.13.0",
"wayland-backend",
@@ -9334,100 +9275,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags 2.13.0",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "write-fonts"
version = "0.45.0"
@@ -9862,7 +9715,7 @@ dependencies = [
"crc32fast",
"deflate64",
"flate2",
"getrandom 0.4.2",
"getrandom 0.4.3",
"hmac 0.13.0",
"indexmap",
"lzma-rust2",
@@ -9879,9 +9732,9 @@ dependencies = [
[[package]]
name = "zlib-rs"
version = "0.6.3"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
checksum = "977347db8caa080403f6b6b7c1cda9479a8e869316f7e13a59b19076a40f94e3"
[[package]]
name = "zmij"
+2 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "gopher64"
version = "1.1.26"
version = "1.1.30"
edition = "2024"
rust-version = "1.96.0"
@@ -19,7 +19,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-big-array = "0.5"
sha2 = "0.11"
tokio = {version = "1.46", features = ["rt-multi-thread", "macros", "fs", "process"] }
tokio = {version = "1.46", features = ["rt-multi-thread", "fs", "process"] }
spin_sleep = "1.3"
encoding_rs = "0.8"
mimalloc = "0.1"
@@ -61,7 +61,6 @@ slint-build = { version = "1.12", optional = true }
cc = { version = "1.2", features = ["parallel"] }
[profile.release]
panic = "abort"
lto = "thin"
strip = true
@@ -69,7 +68,6 @@ strip = true
inherits = "release"
debug = true
strip = false
panic = "unwind"
[package.metadata.bundle]
name = "Gopher64"
+3 -1
View File
@@ -41,7 +41,9 @@ Keys are mapped according to [these defaults](https://github.com/gopher64/gopher
## netplay
Gopher64 supports netplay (online play with others) via cloud hosted servers. You can also run the [server](https://github.com/gopher64/gopher64-netplay-server) yourself on a LAN.
Gopher64 supports P2P netplay (online play with others).
Please read the [guide](https://github.com/gopher64/gopher64/wiki/Netplay-Guide) before trying out netplay.
## portable mode
+6 -6
View File
@@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-13 07:48+0000\n"
"POT-Creation-Date: 2026-06-16 12:26+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: \n"
"Language-Team: German\n"
@@ -240,7 +240,7 @@ msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Sitzung erstellen"
#: src/ui/gui/netplay_create.slint:105
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Abbrechen"
@@ -406,22 +406,22 @@ msgctxt "NetplayWait"
msgid "Local Input Delay:"
msgstr "Lokale Eingabeverzögerung:"
#: src/ui/gui/netplay_wait.slint:117
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Empfohlen: "
#: src/ui/gui/netplay_wait.slint:126
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Spiel starten"
#: src/ui/gui/netplay_wait.slint:126
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Waiting for P2P mesh "
msgstr "Warten auf P2P-Mesh "
#: src/ui/gui/netplay_wait.slint:134
#: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait"
msgid "Cancel"
msgstr "Abbrechen"
+6 -6
View File
@@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-13 07:48+0000\n"
"POT-Creation-Date: 2026-06-16 12:26+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: Gemini\n"
"Language-Team: Spanish (Spain)\n"
@@ -240,7 +240,7 @@ msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Crear sesión"
#: src/ui/gui/netplay_create.slint:105
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Cancelar"
@@ -407,22 +407,22 @@ msgctxt "NetplayWait"
msgid "Local Input Delay:"
msgstr "Retardo de entrada local:"
#: src/ui/gui/netplay_wait.slint:117
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Recomendado: "
#: src/ui/gui/netplay_wait.slint:126
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Iniciar juego"
#: src/ui/gui/netplay_wait.slint:126
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Waiting for P2P mesh "
msgstr "Esperando malla P2P "
#: src/ui/gui/netplay_wait.slint:134
#: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait"
msgid "Cancel"
msgstr "Cancelar"
+6 -6
View File
@@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-13 07:48+0000\n"
"POT-Creation-Date: 2026-06-16 12:26+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: \n"
"Language-Team: French (France)\n"
@@ -240,7 +240,7 @@ msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Créer la session"
#: src/ui/gui/netplay_create.slint:105
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Annuler"
@@ -407,22 +407,22 @@ msgctxt "NetplayWait"
msgid "Local Input Delay:"
msgstr "Délai d'entrée local :"
#: src/ui/gui/netplay_wait.slint:117
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Recommandé : "
#: src/ui/gui/netplay_wait.slint:126
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Lancer la partie"
#: src/ui/gui/netplay_wait.slint:126
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Waiting for P2P mesh "
msgstr "En attente du maillage P2P "
#: src/ui/gui/netplay_wait.slint:134
#: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait"
msgid "Cancel"
msgstr "Annuler"
+6 -6
View File
@@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2026-06-13 07:48+0000\n"
"POT-Creation-Date: 2026-06-16 12:26+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -238,7 +238,7 @@ msgctxt "NetplayCreate"
msgid "Create Session"
msgstr ""
#: src/ui/gui/netplay_create.slint:105
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr ""
@@ -400,22 +400,22 @@ msgctxt "NetplayWait"
msgid "Local Input Delay:"
msgstr ""
#: src/ui/gui/netplay_wait.slint:117
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr ""
#: src/ui/gui/netplay_wait.slint:126
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr ""
#: src/ui/gui/netplay_wait.slint:126
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Waiting for P2P mesh "
msgstr ""
#: src/ui/gui/netplay_wait.slint:134
#: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait"
msgid "Cancel"
msgstr ""
+6 -6
View File
@@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-13 07:48+0000\n"
"POT-Creation-Date: 2026-06-16 12:26+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
@@ -238,7 +238,7 @@ msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "セッションを作成"
#: src/ui/gui/netplay_create.slint:105
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "キャンセル"
@@ -403,22 +403,22 @@ msgctxt "NetplayWait"
msgid "Local Input Delay:"
msgstr "ローカル入力遅延:"
#: src/ui/gui/netplay_wait.slint:117
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "推奨: "
#: src/ui/gui/netplay_wait.slint:126
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "ゲーム開始"
#: src/ui/gui/netplay_wait.slint:126
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Waiting for P2P mesh "
msgstr "P2Pメッシュ接続待機中 "
#: src/ui/gui/netplay_wait.slint:134
#: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait"
msgid "Cancel"
msgstr "キャンセル"
+6 -6
View File
@@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-13 07:48+0000\n"
"POT-Creation-Date: 2026-06-16 12:26+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: \n"
"Language-Team: Dutch (Netherlands)\n"
@@ -240,7 +240,7 @@ msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Sessie aanmaken"
#: src/ui/gui/netplay_create.slint:105
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Annuleren"
@@ -406,22 +406,22 @@ msgctxt "NetplayWait"
msgid "Local Input Delay:"
msgstr "Lokale invoervertraging:"
#: src/ui/gui/netplay_wait.slint:117
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Aanbevolen: "
#: src/ui/gui/netplay_wait.slint:126
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Spel starten"
#: src/ui/gui/netplay_wait.slint:126
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Waiting for P2P mesh "
msgstr "Wachten op P2P-mesh "
#: src/ui/gui/netplay_wait.slint:134
#: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait"
msgid "Cancel"
msgstr "Annuleren"
@@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2026-06-13 07:48+0000\n"
"POT-Creation-Date: 2026-06-16 12:26+0000\n"
"PO-Revision-Date: 2026-05-11 18:11-3000\n"
"Last-Translator: EDUARDO CARVALHO <eduardotcarvalho@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -240,7 +240,7 @@ msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Criar Sessão"
#: src/ui/gui/netplay_create.slint:105
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Cancelar"
@@ -407,22 +407,22 @@ msgctxt "NetplayWait"
msgid "Local Input Delay:"
msgstr "Atraso de entrada local:"
#: src/ui/gui/netplay_wait.slint:117
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Recomendado: "
#: src/ui/gui/netplay_wait.slint:126
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Iniciar jogo"
#: src/ui/gui/netplay_wait.slint:126
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Waiting for P2P mesh "
msgstr "Aguardando malha P2P "
#: src/ui/gui/netplay_wait.slint:134
#: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait"
msgid "Cancel"
msgstr "Cancelar"
+6 -6
View File
@@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-13 07:48+0000\n"
"POT-Creation-Date: 2026-06-16 12:26+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
@@ -239,7 +239,7 @@ msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Создать сессию"
#: src/ui/gui/netplay_create.slint:105
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Отмена"
@@ -405,22 +405,22 @@ msgctxt "NetplayWait"
msgid "Local Input Delay:"
msgstr "Локальная задержка ввода:"
#: src/ui/gui/netplay_wait.slint:117
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Рекомендуется: "
#: src/ui/gui/netplay_wait.slint:126
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Начать игру"
#: src/ui/gui/netplay_wait.slint:126
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Waiting for P2P mesh "
msgstr "Ожидание P2P-сети "
#: src/ui/gui/netplay_wait.slint:134
#: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait"
msgid "Cancel"
msgstr "Отмена"
+25 -26
View File
@@ -622,10 +622,10 @@ static void render_frame(Vulkan::Device &device) {
cmd->set_depth_test(false, false);
cmd->set_cull_mode(VK_CULL_MODE_NONE);
VkViewport vp = cmd->get_viewport();
// If we don't have an image, we just get a cleared screen in the render
// pass.
if (image) {
VkViewport vp = cmd->get_viewport();
calculate_viewport(&vp.x, &vp.y, &vp.width, &vp.height,
image->get_height() / gfx_info.upscale);
@@ -646,33 +646,32 @@ static void render_frame(Vulkan::Device &device) {
// The vertices are constants in the shader.
// Draws fullscreen quad using oversized triangle.
cmd->draw(3);
if (!messages.empty()) {
Message *message = &messages.front();
if (!message->image) {
message->image = create_message_image(device, vp.width, message_font,
message->message.c_str());
SDL_AddTimer(message->milliseconds, pop_message_callback, NULL);
}
cmd->set_texture(0, 0, message->image->get_view(),
Vulkan::StockSampler::NearestClamp);
vp.x = floor(vp.x + (vp.width - message->image->get_width()) / 2);
vp.y = vp.y + vp.height - message->image->get_height();
vp.height = message->image->get_height();
vp.width = message->image->get_width();
cmd->set_viewport(vp);
cmd->draw(3);
} else if (achievement_progress_indicator_image) {
draw_indicator(cmd, achievement_progress_indicator_image, vp);
} else if (achievement_challenge_indicator_image &&
display_challenge_indicator) {
draw_indicator(cmd, achievement_challenge_indicator_image, vp);
}
if (!messages.empty()) {
Message *message = &messages.front();
if (!message->image) {
message->image = create_message_image(device, vp.width, message_font,
message->message.c_str());
SDL_AddTimer(message->milliseconds, pop_message_callback, NULL);
}
cmd->set_texture(0, 0, message->image->get_view(),
Vulkan::StockSampler::NearestClamp);
vp.x = floor(vp.x + (vp.width - message->image->get_width()) / 2);
vp.y = vp.y + vp.height - message->image->get_height();
vp.height = message->image->get_height();
vp.width = message->image->get_width();
cmd->set_viewport(vp);
if (messages.empty() && display_fps && fps_image) {
draw_fps(cmd, vp);
}
cmd->draw(3);
} else if (achievement_progress_indicator_image) {
draw_indicator(cmd, achievement_progress_indicator_image, vp);
} else if (achievement_challenge_indicator_image &&
display_challenge_indicator) {
draw_indicator(cmd, achievement_challenge_indicator_image, vp);
}
if (messages.empty() && display_fps && fps_image) {
draw_fps(cmd, vp);
}
cmd->end_render_pass();
+37 -4
View File
@@ -52,6 +52,7 @@ pub fn run_game(
rom_contents: &[u8],
game_settings: ui::GameSettings,
ra_config: retroachievements::RAConfig,
netplay_config: Option<netplay::NetplayConfig>,
) {
device.cpu.overclock = game_settings.overclock;
if game_settings.disable_expansion_pak {
@@ -62,17 +63,32 @@ pub fn run_game(
device.savestate.load_state = true;
}
init_rng(device);
cart::rom::init(device, rom_contents); // cart needs to come before rdram
// rdram pointer is shared with parallel-rdp and retroachievements
rdram::init(device);
ui::video::init(device);
ui::video::init(device, netplay_config.is_some());
ui::audio::init(device);
ui::input::init(&mut device.ui);
// must be after video init
if let Some(netplay_config) = &netplay_config {
device.netplay = netplay::init(
device,
netplay_config,
cart::rom::is_system_pal(rom_contents),
);
if device.netplay.is_none() {
ui::input::close(&mut device.ui);
ui::audio::close(&mut device.ui);
ui::video::close(&device.ui);
return;
}
}
init_rng_rtc(device);
// must be after video init
let (discord_watch_tx, discord_handle) = if ra_config.enabled && device.netplay.is_none() {
retroachievements::load_game(rom_contents, rom_contents.len(), ra_config.rich_presence)
@@ -105,6 +121,11 @@ pub fn run_game(
retroachievements::unload_game(discord_watch_tx, discord_handle);
ui::storage::write_saves(device);
if let Some(netplay) = &mut device.netplay {
netplay::close(netplay);
}
ui::input::close(&mut device.ui);
ui::audio::close(&mut device.ui);
ui::video::close(&device.ui);
@@ -114,7 +135,7 @@ fn set_rng() -> rand::rngs::Xoshiro256PlusPlus {
rand::rngs::Xoshiro256PlusPlus::try_from_rng(&mut rand::rngs::SysRng).unwrap()
}
fn init_rng(device: &mut Device) {
fn init_rng_rtc(device: &mut Device) {
let mut rng_seed = set_rng().next_u64();
if let Some(netplay) = &mut device.netplay {
if netplay.player_number == 0 {
@@ -124,6 +145,18 @@ fn init_rng(device: &mut Device) {
}
}
device.rng = rand::rngs::Xoshiro256PlusPlus::seed_from_u64(rng_seed);
device.cart.rtc_timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
if let Some(netplay) = &mut device.netplay {
if netplay.player_number == 0 {
netplay::send_rtc(netplay, device.cart.rtc_timestamp);
} else {
device.cart.rtc_timestamp = netplay::receive_rtc(netplay);
}
}
}
fn swap_rom(contents: Vec<u8>) -> Option<Vec<u8>> {
-13
View File
@@ -1,5 +1,4 @@
use crate::device;
use crate::netplay;
use crate::ui;
use sha2::{Digest, Sha256};
@@ -122,18 +121,6 @@ pub fn dma_write(
}
pub fn init(device: &mut device::Device, rom_file: &[u8]) {
device.cart.rtc_timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
if let Some(netplay) = &mut device.netplay {
if netplay.player_number == 0 {
netplay::send_rtc(netplay, device.cart.rtc_timestamp);
} else {
device.cart.rtc_timestamp = netplay::receive_rtc(netplay);
}
}
device.cart.sc64.cfg[device::cart::sc64::SC64_BOOTLOADER_SWITCH as usize] = 1;
device.cart.rom = rom_file.to_vec();
+3 -10
View File
@@ -219,16 +219,9 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
data.data_size & 0xFFFFFF; // length
device.cart.sc64.usb_buffer = data.data; // store the data to be read
}
Err(err) => {
match err {
tokio::sync::broadcast::error::TryRecvError::Lagged(_) => {
panic!("cart_rx lagged: {err}");
}
_ => {
device.cart.sc64.regs[SC64_DATA0_REG] = 0; // read_status/type
device.cart.sc64.regs[SC64_DATA1_REG] = 0; // length
}
}
Err(_err) => {
device.cart.sc64.regs[SC64_DATA0_REG] = 0; // read_status/type
device.cart.sc64.regs[SC64_DATA1_REG] = 0; // length
}
}
} else {
+3 -2
View File
@@ -104,7 +104,7 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
savestates::process_savestates(device);
}
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);
}
}
}
@@ -140,7 +140,7 @@ pub fn update_screen(device: &mut device::Device) {
if !netplay::in_rollback(device.netplay.as_ref()) {
ui::video::update_screen();
device.speed_limiter.frame_counter += 1;
let _ = device.ui.video.vis_tx.as_ref().unwrap().try_send(true);
let _ = device.ui.video.vis_tx.as_ref().unwrap().send(true);
}
if device.netplay.is_some() {
@@ -221,6 +221,7 @@ fn speed_limiter(device: &mut device::Device) {
if now < deadline {
let dur = deadline - now;
spin_sleep::sleep(dur);
if dur < device.speed_limiter.min_wait_time {
device.speed_limiter.min_wait_time = dur;
}
+39 -28
View File
@@ -104,6 +104,19 @@ fn set_app_id() {
}
}
pub fn create_runtime() -> (tokio::sync::oneshot::Sender<()>, tokio::runtime::Handle) {
let (tx, rx) = std::sync::mpsc::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.recv().unwrap())
}
pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
let dirs = ui::get_dirs();
@@ -169,25 +182,7 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
let mut shutdown_tx = None;
let mut usb_handle = None;
let ice_config_path = dirs.cache_dir.join("ice_config.json");
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 {
if args.netplay_server_addr.is_none() {
for i in 0..4 {
if device.ui.config.input.transfer_pak[i]
&& !device.ui.config.input.gb_rom_path[i].is_empty()
@@ -213,7 +208,7 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
{
let username = args.ra_username.unwrap_or(ra_config.username.clone());
retroachievements::init_client(
if cfg!(ra_hardcore_enabled) && device.netplay.is_none() {
if cfg!(ra_hardcore_enabled) && args.netplay_server_addr.is_none() {
ra_config.hardcore
} else {
false
@@ -231,15 +226,29 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
tx.send(false).unwrap();
}
tokio::task::block_in_place(move || {
rx.blocking_recv().unwrap();
});
rx.blocking_recv().unwrap();
ra_config
} else {
retroachievements::RAConfig::default()
};
let netplay_config = if let Some(server_addr) = args.netplay_server_addr
&& let Some(player_number) = args.netplay_player_number
&& let Some(number_of_players) = args.netplay_number_of_players
&& let Some(input_delay) = args.netplay_input_delay
{
Some(netplay::NetplayConfig {
server_addr,
player_number,
number_of_players,
input_delay,
ice_config_path: dirs.cache_dir.join("ice_config.json"),
})
} else {
None
};
device::run_game(
&mut device,
&rom_contents,
@@ -250,15 +259,14 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
load_savestate_slot: args.load_state,
},
ra_config,
netplay_config,
);
// on Android, the client is shut down in the app_window function
#[cfg(not(target_os = "android"))]
retroachievements::shutdown_client();
if device.netplay.is_some() {
netplay::close(ice_config_path);
} else {
if device.netplay.is_none() {
for i in 0..4 {
if device.ui.config.input.transfer_pak[i]
&& !device.ui.config.input.gb_ram_path[i].is_empty()
@@ -337,8 +345,10 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
#[cfg(target_os = "android")]
#[unsafe(no_mangle)]
#[tokio::main]
async fn android_main(app: slint::android::AndroidApp) {
fn android_main(app: slint::android::AndroidApp) {
let (close_tx, handle) = create_runtime();
let _guard = handle.enter();
slint::android::init_with_event_listener(app.clone(), move |event| match event {
slint::android::android_activity::PollEvent::Main(main_event) => match main_event {
slint::android::android_activity::MainEvent::TerminateWindow { .. } => {
@@ -371,6 +381,7 @@ async fn android_main(app: slint::android::AndroidApp) {
std::fs::create_dir_all(dirs.data_dir.join("states")).unwrap();
ui::gui::app_window(&app_window, true);
close_tx.send(()).unwrap();
*android::WEAK_SLINT_WINDOW.lock().unwrap() = None;
*android::ANDROID_APP.lock().unwrap() = None;
}
+7 -3
View File
@@ -5,8 +5,12 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use clap::Parser;
#[tokio::main]
async fn main() -> std::io::Result<()> {
fn main() -> std::io::Result<()> {
let (close_tx, handle) = gopher64::create_runtime();
let _guard = handle.enter();
let args = gopher64::Args::parse();
gopher64::run(args, std::env::args().count())
let result = gopher64::run(args, std::env::args().count());
close_tx.send(()).unwrap();
result
}
+167 -79
View File
@@ -1,5 +1,4 @@
use crate::device;
use crate::savestates;
use crate::ui;
use sha2::digest::Digest;
@@ -20,15 +19,12 @@ impl ggrs::Config for GgrsConfig {
pub struct MatchboxChannel {
channel: matchbox_socket::WebRtcChannel,
disconnected_peers: tokio::sync::mpsc::Sender<matchbox_socket::PeerId>,
}
impl ggrs::NonBlockingSocket<matchbox_socket::PeerId> for MatchboxChannel {
fn send_to(&mut self, msg: &ggrs::Message, addr: &matchbox_socket::PeerId) {
let encoded = postcard::to_stdvec(msg).expect("serialization failed");
if self.channel.try_send(encoded.into(), *addr).is_err() {
self.disconnected_peers.try_send(*addr).unwrap();
}
let _ = self.channel.try_send(encoded.into(), *addr);
}
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 disconnected: bool,
pub session: ggrs::P2PSession<GgrsConfig>,
pub reliable_channel: matchbox_socket::WebRtcChannel,
pub peers: Vec<matchbox_socket::PeerId>,
@@ -55,7 +60,7 @@ pub struct Netplay {
pub inputs: Vec<(ui::input::InputData, ggrs::InputStatus)>,
pub requests: std::collections::VecDeque<ggrs::GgrsRequest<GgrsConfig>>,
pub incoming_message: Vec<u8>,
pub disconnected_peers: tokio::sync::mpsc::Receiver<matchbox_socket::PeerId>,
pub ice_config_path: std::path::PathBuf,
}
#[derive(serde::Serialize, serde::Deserialize)]
@@ -100,6 +105,7 @@ fn process_reliable_messages(netplay: &mut Netplay) {
.insert(decoded_message.name, decoded_message.data);
netplay.incoming_message.clear();
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(
channel: &mut matchbox_socket::WebRtcChannel,
peers: Vec<matchbox_socket::PeerId>,
peers: &[matchbox_socket::PeerId],
player_number: usize,
) {
let message = NetplayMessage {
@@ -133,7 +139,7 @@ fn send_player_number(
};
let data = postcard::to_stdvec(&message).unwrap();
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);
}
}
@@ -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) {
if input_delay < 1 {
return;
}
let message = NetplayMessage {
name: "input_delay".to_string(),
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 {
netplay
.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(
device: &mut device::Device,
) -> 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() {
match request {
ggrs::GgrsRequest::SaveGameState { cell, frame } => {
savestates::create_savestate(device, true, Some(frame));
//savestates::create_savestate(device, true, Some(frame));
let mut hasher = sha2::Sha256::new();
for reg in device.cpu.cop0.regs.as_ref() {
@@ -273,36 +278,35 @@ pub fn process_requests(
let hash = u128::from_be_bytes(hasher.finalize()[..16].try_into().unwrap());
cell.save(frame, Some(frame), Some(hash));
}
ggrs::GgrsRequest::LoadGameState { cell, frame: _ } => {
if let Some(frame) = cell.load() {
savestates::load_savestate(device, true, Some(frame));
}
ggrs::GgrsRequest::LoadGameState { cell: _, frame: _ } => {
// if let Some(frame) = cell.load() {
// savestates::load_savestate(device, true, Some(frame));
// }
}
ggrs::GgrsRequest::AdvanceFrame { inputs } => {
return inputs;
}
}
} else {
// unsafe { sdl3_sys::events::SDL_PumpEvents() }; // so the screen doesn't freeze
process_netplay(device);
}
}
}
fn process_netplay(device: &mut device::Device) {
let netplay = device.netplay.as_mut().unwrap();
process_disconnected_peers(netplay);
fn poll_clients(netplay: &mut Netplay) {
netplay.session.poll_remote_clients();
for event in netplay.session.events() {
match event {
ggrs::GgrsEvent::Synchronizing { .. } => {}
ggrs::GgrsEvent::Synchronized { .. } => {}
ggrs::GgrsEvent::Disconnected { .. } => {
ui::video::onscreen_message(
"Lost connection to peer",
ui::video::MESSAGE_LENGTH_MESSAGE_LONG,
);
if !netplay.disconnected {
netplay.disconnected = true;
ui::video::onscreen_message(
"Lost connection to peer(s)",
ui::video::MESSAGE_LENGTH_MESSAGE_LONG,
);
}
}
ggrs::GgrsEvent::NetworkInterrupted { .. } => {
println!("network interrupted");
@@ -322,7 +326,12 @@ 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);
advance_frame(device);
}
@@ -335,6 +344,28 @@ fn advance_frame(device: &mut device::Device) {
.session
.add_local_input(local_handle, local_input)
.unwrap();
// avoid rollback
while !netplay.disconnected
&& netplay.session.current_frame() > netplay.session.confirmed_frame()
&& netplay.session.confirmed_frame() != ggrs::NULL_FRAME
{
poll_clients(netplay);
}
if netplay.disconnected {
netplay.requests.push_back(ggrs::GgrsRequest::AdvanceFrame {
inputs: vec![
(
ui::input::InputData::default(),
ggrs::InputStatus::Disconnected
);
4
],
});
return;
}
match netplay.session.advance_frame() {
Ok(requests) => {
netplay.requests.extend(requests);
@@ -346,19 +377,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.iter() {
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(
server_addr: String,
player_number: usize,
number_of_players: usize,
input_delay: usize,
ice_config_path: std::path::PathBuf,
device: &mut device::Device,
netplay_config: &NetplayConfig,
pal: bool,
) -> 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_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)
{
builder = builder.ice_server(matchbox_socket::RtcIceServerConfig {
@@ -370,50 +422,69 @@ pub fn init(
eprintln!("Using default ICE config");
}
let (mut socket, loop_fut) = builder.build();
tokio::spawn(async move {
if let Err(e) = loop_fut.await {
eprintln!("WebRTC loop failed: {}", e);
}
});
let mut socket = create_socket(builder.clone());
let (disconnected_peers_tx, disconnected_peers_rx) = tokio::sync::mpsc::channel(100);
let matchbox_channel = MatchboxChannel {
channel: socket.take_channel(0).unwrap(),
disconnected_peers: disconnected_peers_tx,
};
let mut reliable_channel = socket.take_channel(1).unwrap();
let now = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(10);
let mut now = std::time::Instant::now();
let mut message_timer = now;
let socket_timeout = std::time::Duration::from_secs_f64(rand::random_range(8.0..10.0));
let mut player_numbers = std::collections::BTreeMap::new();
player_numbers.insert(player_number, None);
loop {
ui::video::onscreen_message(
"Connecting to netplay peers...\nPlease wait...",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
device.cpu.running = true;
while device.cpu.running {
socket.update_peers();
let peers = socket
.connected_peers()
.collect::<Vec<matchbox_socket::PeerId>>();
send_player_number(&mut reliable_channel, peers, player_number);
get_player_numbers(&mut reliable_channel, &mut player_numbers);
if player_numbers.len() == number_of_players {
break;
if peers.len() == netplay_config.number_of_players - 1 {
player_numbers.insert(netplay_config.player_number, None);
send_player_number(socket.channel_mut(1), &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(&peers, &player_numbers)
{
break;
}
} else if now.elapsed() > socket_timeout {
socket.close();
player_numbers.clear();
socket = create_socket(builder.clone());
now = std::time::Instant::now();
}
if now.elapsed() > timeout {
eprintln!("Could not connect to netplay peers");
return None;
if message_timer.elapsed() > std::time::Duration::from_secs(4) {
ui::video::onscreen_message(
"Still connecting to netplay peers...\nPlease wait...",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
message_timer = std::time::Instant::now();
}
unsafe { sdl3_sys::events::SDL_PumpEvents() };
ui::video::render_frame();
ui::video::update_screen();
std::thread::sleep(std::time::Duration::from_millis(10));
ui::video::check_callback(device);
}
if !device.cpu.running {
// user closed the window
return None;
}
device.cpu.running = false;
let mut session_builder = ggrs::SessionBuilder::<GgrsConfig>::new()
.with_num_players(number_of_players)
.with_num_players(netplay_config.number_of_players)
.unwrap()
.with_input_delay(input_delay)
.with_input_delay(netplay_config.input_delay)
.with_fps(if pal { 50 } else { 60 })
.unwrap()
.with_desync_detection_mode(ggrs::DesyncDetection::On { interval: 60 })
.with_max_prediction_window(0)
.with_max_prediction_window(16)
.with_disconnect_timeout(std::time::Duration::from_secs(if cfg!(debug_assertions) {
10
} else {
@@ -434,6 +505,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)
|| matchbox_channel.channel.config().ordered
{
@@ -443,36 +519,48 @@ pub fn init(
let mut session = session_builder.start_p2p_session(matchbox_channel).unwrap();
let now = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(10);
while session.current_state() != ggrs::SessionState::Running {
session.poll_remote_clients();
if now.elapsed() > timeout {
eprintln!("Could not start netplay session");
return None;
}
unsafe { sdl3_sys::events::SDL_PumpEvents() };
std::thread::sleep(std::time::Duration::from_millis(10));
}
Some(Netplay {
disconnected_peers: disconnected_peers_rx,
disconnected: false,
incoming_message: vec![],
input_delay,
input_delay: netplay_config.input_delay,
session,
reliable_channel,
peers,
player_number,
player_number: netplay_config.player_number,
connected: [
number_of_players > 0,
number_of_players > 1,
number_of_players > 2,
number_of_players > 3,
netplay_config.number_of_players > 0,
netplay_config.number_of_players > 1,
netplay_config.number_of_players > 2,
netplay_config.number_of_players > 3,
],
inputs: Vec::new(),
requests: std::collections::VecDeque::new(),
received_data: std::collections::VecDeque::new(),
messages: std::collections::HashMap::new(),
ice_config_path: netplay_config.ice_config_path.clone(),
})
}
pub fn close(ice_config_path: std::path::PathBuf) {
let _ = std::fs::remove_file(ice_config_path);
pub fn close(netplay: &mut Netplay) {
if !netplay.disconnected {
let message = NetplayMessage {
name: "disconnect".to_string(),
data: vec![],
};
send_message(netplay, message);
std::thread::sleep(std::time::Duration::from_millis(200)); // give the message time to be sent
}
let _ = std::fs::remove_file(&netplay.ice_config_path);
}
+2 -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;
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_image_url = std::ptr::null();
+8 -8
View File
@@ -68,10 +68,10 @@ pub struct Storage {
pub struct Video {
pub window: *mut sdl3_sys::video::SDL_Window,
pub fullscreen: bool,
pub fps_tx: Option<tokio::sync::mpsc::Sender<bool>>,
pub fps_rx: Option<tokio::sync::mpsc::Receiver<bool>>,
pub vis_tx: Option<tokio::sync::mpsc::Sender<bool>>,
pub vis_rx: Option<tokio::sync::mpsc::Receiver<bool>>,
pub fps_tx: Option<tokio::sync::mpsc::UnboundedSender<bool>>,
pub fps_rx: Option<tokio::sync::mpsc::UnboundedReceiver<bool>>,
pub vis_tx: Option<tokio::sync::mpsc::UnboundedSender<bool>>,
pub vis_rx: Option<tokio::sync::mpsc::UnboundedReceiver<bool>>,
}
unsafe impl Send for Video {}
@@ -79,8 +79,8 @@ unsafe impl Sync for Video {}
#[derive(Default)]
pub struct Usb {
pub usb_tx: Option<tokio::sync::broadcast::Sender<usb::UsbData>>,
pub cart_rx: Option<tokio::sync::broadcast::Receiver<usb::UsbData>>,
pub usb_tx: Option<tokio::sync::mpsc::UnboundedSender<usb::UsbData>>,
pub cart_rx: Option<tokio::sync::mpsc::UnboundedReceiver<usb::UsbData>>,
}
#[derive(Clone)]
@@ -191,8 +191,8 @@ impl Ui {
pub fn new() -> Ui {
let dirs = get_dirs();
let (fps_tx, fps_rx) = tokio::sync::mpsc::channel(1000);
let (vis_tx, vis_rx) = tokio::sync::mpsc::channel(1000);
let (fps_tx, fps_rx) = tokio::sync::mpsc::unbounded_channel();
let (vis_tx, vis_rx) = tokio::sync::mpsc::unbounded_channel();
Ui {
input: Input {
last_polled: 0,
+7 -2
View File
@@ -1,4 +1,5 @@
use crate::Args;
use crate::create_runtime;
use crate::run;
use crate::ui;
use clap::Parser;
@@ -158,17 +159,21 @@ fn argv_to_strings(argc: std::ffi::c_int, argv: *mut *mut std::ffi::c_char) -> V
}
#[unsafe(no_mangle)]
#[tokio::main]
pub async extern "C" fn gopher64_sdl_main(
pub extern "C" fn gopher64_sdl_main(
argc: std::ffi::c_int,
argv: *mut *mut std::ffi::c_char,
) -> std::ffi::c_int {
let (close_tx, handle) = create_runtime();
let _guard = handle.enter();
let raw = argv_to_strings(argc, argv);
let args = Args::try_parse_from(raw).unwrap();
if let Err(err) = run(args, argc as usize) {
close_tx.send(()).unwrap();
eprintln!("Error running game: {err:?}");
return 1;
}
close_tx.send(()).unwrap();
0
}
+12 -2
View File
@@ -136,17 +136,27 @@ impl Config {
let dirs = ui::get_dirs();
let file_path = dirs.config_dir.join("config.json");
let config_file = std::fs::read(file_path);
let mut input_data: Option<Input> = None;
if let Ok(config_file) = config_file {
let result = serde_json::from_slice::<Config>(config_file.as_ref());
if let Ok(mut result) = result {
result.write_to_disk = true;
return result;
}
// try to restore input data from old config file
if let Ok(value) = serde_json::from_slice::<serde_json::Value>(&config_file)
&& let Some(data) = value.get("input")
&& let Ok(data) = serde_json::from_value::<Input>(data.clone())
{
input_data = Some(data);
}
}
let mut input_profiles = std::collections::BTreeMap::new();
input_profiles.insert("default".to_string(), ui::input::get_default_profile());
Config {
input: Input {
input: input_data.unwrap_or(Input {
input_profile_binding: [
"default".to_string(),
"default".to_string(),
@@ -160,7 +170,7 @@ impl Config {
gb_rom_path: [String::new(), String::new(), String::new(), String::new()],
gb_ram_path: [String::new(), String::new(), String::new(), String::new()],
emulate_vru: false,
},
}),
video: Video {
upscale: 1,
ssaa: false,
+12 -7
View File
@@ -34,13 +34,18 @@ fn check_latest_version(weak: slint::Weak<AppWindow>) {
if let Ok(response) = response {
let data: Result<GithubData, reqwest::Error> = response.json().await;
let latest_version = if let Ok(data) = data
&& let Ok(github_version) = semver::Version::parse(&data.tag_name[1..])
{
github_version
} else {
eprintln!("Error getting latest version from GitHub");
semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap()
let latest_version = match data {
Ok(data) => match semver::Version::parse(&data.tag_name[1..]) {
Ok(github_version) => github_version,
Err(e) => {
eprintln!("Error parsing latest version from GitHub: {}", e);
semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap()
}
},
Err(e) => {
eprintln!("Error getting latest version from GitHub: {}", e);
semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap()
}
};
let current_version = semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
if current_version < latest_version {
+5 -3
View File
@@ -37,7 +37,7 @@ export component AppWindow inherits Window {
callback netplay_begin_game(int);
callback netplay_send_chat_message(string);
callback netplay_refresh_sessions;
callback netplay_create_session(string, string, string, string, string, string);
callback netplay_create_session(string, string, string, string, string, string, bool, bool);
callback netplay_select_rom(string);
callback netplay_join_session(string, string, string, string);
callback netplay_close;
@@ -256,8 +256,8 @@ export component AppWindow inherits Window {
select_rom(rom_dir) => {
netplay_select_rom(rom_dir);
}
create_session(a, b, c, d, e, f) => {
netplay_create_session(a,b,c,d,e,f);
create_session(a, b, c, d, e, f, g, h) => {
netplay_create_session(a,b,c,d,e,f,g,h);
}
close => {
netplay_close();
@@ -385,5 +385,7 @@ export component AppWindow inherits Window {
NetplayWaitData.connected_peers_count = 0;
NetplayData.pending_session = false;
NetplayData.pending_refresh = false;
NetplayData.sessions = [];
NetplayData.current_session = -1;
}
}
+4 -2
View File
@@ -6,7 +6,7 @@ import { Page } from "page.slint";
export component NetplayCreate inherits Page {
title: @tr("Netplay: Create Session");
callback select_rom(string);
callback create_session(string, string, string, string, string, string);
callback create_session(string, string, string, string, string, string, bool, bool);
callback close;
VerticalBox {
@@ -98,7 +98,9 @@ export component NetplayCreate inherits Page {
NetplayData.game_name,
NetplayData.game_hash,
NetplayData.game_cheats,
NetplayData.password);
NetplayData.password,
SettingsData.overclock_n64_cpu,
SettingsData.disable_expansion_pak);
}
}
Button {
+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 { Page } from "page.slint";
@@ -103,12 +103,13 @@ export component NetplayWait inherits Page {
vertical-alignment: center;
}
ComboBox {
model: ["0", "1", "2", "3", "4", "5", "6", "7", "8"];
current-index: NetplayWaitData.input_delay;
SpinBox {
minimum: 1;
maximum: 16;
value: NetplayWaitData.input_delay;
enabled: NetplayWaitData.recommended_delay != 0;
selected => {
NetplayWaitData.input_delay = self.current-index;
edited => {
NetplayWaitData.input_delay = self.value;
}
}
+183 -148
View File
@@ -25,6 +25,12 @@ enum MessageType {
ReceiveChatMessage,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ServerAddresses {
pub lobby: String,
pub game: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct NetplaySession {
password: Option<String>,
@@ -34,7 +40,7 @@ struct NetplaySession {
client_version: Option<String>,
features: Option<std::collections::HashMap<String, String>>,
players: Vec<String>,
server_address: Option<String>,
server_address: Option<ServerAddresses>,
input_delay: Option<usize>,
ice_server_config: Option<RtcIceServerConfig>,
}
@@ -102,39 +108,175 @@ fn select_rom(weak: slint::Weak<AppWindow>, rom_dir: slint::SharedString) {
});
}
fn setup_create_window(
fn setup_callbacks(
app: &AppWindow,
game_settings: ui::GameSettings,
netplay_write_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
netplay_read_receiver: tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
netplay_read_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
netplay_write_receiver: tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
close_ping_rx: tokio::sync::broadcast::Receiver<()>,
netplay_write_sender: &tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
netplay_read_receiver: &tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
netplay_read_sender: &tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
netplay_write_receiver: &tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
close_ping_tx: &tokio::sync::broadcast::Sender<()>,
close_ping_rx: &tokio::sync::broadcast::Receiver<()>,
) {
let weak = app.as_weak();
let netplay_write_sender_create_session = netplay_write_sender.clone();
let netplay_read_receiver_create_session = netplay_read_receiver.resubscribe();
let netplay_write_receiver_create_session = netplay_write_receiver.resubscribe();
let netplay_read_sender_create_session = netplay_read_sender.clone();
let close_ping_tx_create_session = close_ping_tx.clone();
let close_ping_rx_create_session = close_ping_rx.resubscribe();
app.on_netplay_create_session(
move |session_name, player_name, game_name, game_hash, game_cheats, password| {
move |session_name,
player_name,
game_name,
game_hash,
game_cheats,
password,
overclock,
disable_expansion_pak| {
close_connections(
&netplay_write_sender_create_session,
&netplay_read_sender_create_session,
&close_ping_tx_create_session,
);
manage_websocket(
netplay_read_sender.clone(),
netplay_write_receiver.resubscribe(),
netplay_read_sender_create_session.clone(),
netplay_write_receiver_create_session.resubscribe(),
);
create_session(
netplay_write_sender.clone(),
netplay_read_receiver.resubscribe(),
close_ping_rx.resubscribe(),
netplay_write_sender_create_session.clone(),
netplay_read_receiver_create_session.resubscribe(),
close_ping_rx_create_session.resubscribe(),
session_name.to_string(),
player_name.to_string(),
game_name.to_string(),
game_hash.to_string(),
game_cheats.to_string(),
password.to_string(),
game_settings.clone(),
overclock,
disable_expansion_pak,
weak.clone(),
);
},
);
let write_sender_chat_message = netplay_write_sender.clone();
app.on_netplay_send_chat_message(move |message| {
let send_chat = NetplayLobbyMessage {
message_type: MessageType::SendChatMessage,
sessions: std::collections::HashMap::new(),
message: Some(message.into()),
};
write_sender_chat_message.send(Some(send_chat)).unwrap();
});
let write_sender_begin_game = netplay_write_sender.clone();
app.on_netplay_begin_game(move |chosen_input_delay| {
let begin_game = NetplayLobbyMessage {
message_type: MessageType::RequestBeginGame,
sessions: std::collections::HashMap::new(),
message: Some(chosen_input_delay.to_string()),
};
write_sender_begin_game.send(Some(begin_game)).unwrap();
});
let write_sender_refresh_sessions = netplay_write_sender.clone();
app.on_netplay_refresh_sessions(move || {
update_sessions(write_sender_refresh_sessions.clone());
});
let write_sender_join_session = netplay_write_sender.clone();
app.on_netplay_join_session(move |session_name, player_name, game_hash, password| {
join_session(
write_sender_join_session.clone(),
session_name.to_string(),
player_name.to_string(),
game_hash.to_string(),
password.to_string(),
);
});
let weak_app = app.as_weak();
app.on_create_session_button_clicked(move || {
weak_app
.upgrade_in_event_loop(move |handle| {
save_settings(&handle);
setup_create_window(&handle);
})
.unwrap();
});
let weak_app = app.as_weak();
let netplay_write_sender_join_session_button = netplay_write_sender.clone();
let netplay_read_receiver_join_session_button = netplay_read_receiver.resubscribe();
let netplay_read_sender_join_session_button = netplay_read_sender.clone();
let netplay_write_receiver_join_session_button = netplay_write_receiver.resubscribe();
let close_ping_tx_join_session_button = close_ping_tx.clone();
let close_ping_rx_join_session_button = close_ping_rx.resubscribe();
app.on_join_session_button_clicked(move || {
let netplay_write_sender = netplay_write_sender_join_session_button.clone();
let netplay_read_receiver = netplay_read_receiver_join_session_button.resubscribe();
let netplay_read_sender = netplay_read_sender_join_session_button.clone();
let netplay_write_receiver = netplay_write_receiver_join_session_button.resubscribe();
let close_ping_tx = close_ping_tx_join_session_button.clone();
let close_ping_rx = close_ping_rx_join_session_button.resubscribe();
weak_app
.upgrade_in_event_loop(move |handle| {
save_settings(&handle);
setup_join_window(
&handle,
netplay_write_sender,
netplay_read_receiver,
netplay_read_sender,
netplay_write_receiver,
close_ping_tx,
close_ping_rx,
);
})
.unwrap();
});
let weak_app = app.as_weak();
app.on_netplay_select_rom(move |rom_dir| {
select_rom(weak_app.clone(), rom_dir);
});
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);
}
@@ -142,13 +284,16 @@ fn manage_websocket(
netplay_read_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
mut netplay_write_receiver: tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
) {
let mut request = "wss://netplay.gopher64.com".into_client_request().unwrap();
let mut request = std::env::var("NETPLAY_SERVER_URL")
.unwrap_or("wss://netplay.gopher64.com".to_string())
.into_client_request()
.unwrap();
request
.headers_mut()
.insert("Authorization", env!("NETPLAY_ID").parse().unwrap());
tokio::spawn(async move {
match tokio::time::timeout(
std::time::Duration::from_secs(2),
std::time::Duration::from_secs(3),
tokio_tungstenite::connect_async(request),
)
.await
@@ -246,7 +391,8 @@ fn create_session(
game_hash: String,
game_cheats: String,
password: String,
game_settings: ui::GameSettings,
overclock: bool,
disable_expansion_pak: bool,
weak_app: slint::Weak<AppWindow>,
) {
tokio::spawn(async move {
@@ -256,10 +402,10 @@ fn create_session(
features.insert("cheats".to_string(), game_cheats);
}
features.insert("overclock".to_string(), game_settings.overclock.to_string());
features.insert("overclock".to_string(), overclock.to_string());
features.insert(
"disable_expansion_pak".to_string(),
game_settings.disable_expansion_pak.to_string(),
disable_expansion_pak.to_string(),
);
let session = NetplaySession {
@@ -503,11 +649,12 @@ fn update_ping(
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.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);
if pings.len() > message.num_of_peers * 4 {
// once we have enough samples, average the 3 highest values
pings.sort_by(f64::total_cmp);
pings.pop(); // Remove highest value to avoid outliers
let ping_avg = pings.iter().rev().take(3).sum::<f64>() / 3.0;
pings.clear();
weak_app
@@ -519,7 +666,7 @@ fn update_ping(
};
let latency_frames = (ping_avg / (1000.0 / refresh_rate)).ceil() as i32;
let recommendation =
(latency_frames + 1).min(8);
(latency_frames + 1).min(16);
if handle.get_netplay_recommended_delay() == 0 {
handle.set_netplay_input_delay(recommendation);
@@ -561,7 +708,7 @@ fn setup_wait_window(
netplay_write_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
mut netplay_read_receiver: tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
close_ping_rx: tokio::sync::broadcast::Receiver<()>,
server_addr: String,
server_addresses: ServerAddresses,
session_name: slint::SharedString,
ice_server_config: Option<RtcIceServerConfig>,
game_name: slint::SharedString,
@@ -571,7 +718,7 @@ fn setup_wait_window(
) {
update_ice_config(ice_server_config.clone());
update_ping(
server_addr.clone(),
server_addresses.lobby.clone(),
ice_server_config,
close_ping_rx,
app.as_weak(),
@@ -589,26 +736,6 @@ fn setup_wait_window(
};
netplay_write_sender.send(Some(request_update)).unwrap();
let sender = netplay_write_sender.clone();
app.on_netplay_send_chat_message(move |message| {
let send_chat = NetplayLobbyMessage {
message_type: MessageType::SendChatMessage,
sessions: std::collections::HashMap::new(),
message: Some(message.into()),
};
sender.send(Some(send_chat)).unwrap();
});
let sender = netplay_write_sender.clone();
app.on_netplay_begin_game(move |chosen_input_delay| {
let begin_game = NetplayLobbyMessage {
message_type: MessageType::RequestBeginGame,
sessions: std::collections::HashMap::new(),
message: Some(chosen_input_delay.to_string()),
};
sender.send(Some(begin_game)).unwrap();
});
let weak_app = app.as_weak();
tokio::spawn(async move {
loop {
@@ -669,7 +796,7 @@ fn setup_wait_window(
load_savestate_slot: None,
},
Some(ui::gui::NetplayDevice {
server_addr,
server_addr: server_addresses.game.clone(),
player_number,
number_of_players: players.row_count(),
input_delay,
@@ -719,8 +846,10 @@ fn setup_join_window(
netplay_read_receiver: tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
netplay_read_sender: tokio::sync::broadcast::Sender<Option<NetplayLobbyMessage>>,
netplay_write_receiver: tokio::sync::broadcast::Receiver<Option<NetplayLobbyMessage>>,
close_ping_tx: tokio::sync::broadcast::Sender<()>,
close_ping_rx: tokio::sync::broadcast::Receiver<()>,
) {
close_connections(&netplay_write_sender, &netplay_read_sender, &close_ping_tx);
manage_websocket(
netplay_read_sender.clone(),
netplay_write_receiver.resubscribe(),
@@ -728,22 +857,6 @@ fn setup_join_window(
app.set_netplay_pending_refresh(true);
let write_sender = netplay_write_sender.clone();
app.on_netplay_refresh_sessions(move || {
update_sessions(write_sender.clone());
});
let write_sender = netplay_write_sender.clone();
app.on_netplay_join_session(move |session_name, player_name, game_hash, password| {
join_session(
write_sender.clone(),
session_name.to_string(),
player_name.to_string(),
game_hash.to_string(),
password.to_string(),
);
});
let weak = app.as_weak();
let mut read_receiver = netplay_read_receiver.resubscribe();
let write_sender = netplay_write_sender.clone();
@@ -895,91 +1008,13 @@ pub fn netplay_window(app: &AppWindow) {
tokio::sync::broadcast::Receiver<()>,
) = tokio::sync::broadcast::channel(5);
let weak_app = app.as_weak();
let write_sender_create = netplay_write_sender.clone();
let read_receiver_create = netplay_read_receiver.resubscribe();
let read_sender_create = netplay_read_sender.clone();
let write_receiver_create = netplay_write_receiver.resubscribe();
let close_ping_create = close_ping_rx.resubscribe();
app.on_create_session_button_clicked(move || {
let write_sender = write_sender_create.clone();
let read_receiver = read_receiver_create.resubscribe();
let read_sender = read_sender_create.clone();
let write_receiver = write_receiver_create.resubscribe();
let close_ping_rx = close_ping_create.resubscribe();
weak_app
.upgrade_in_event_loop(move |handle| {
save_settings(&handle);
setup_create_window(
&handle,
ui::GameSettings {
overclock: handle.get_overclock_n64_cpu(),
disable_expansion_pak: handle.get_disable_expansion_pak(),
cheats: rustc_hash::FxHashMap::default(), // not used here
load_savestate_slot: None,
},
write_sender,
read_receiver,
read_sender,
write_receiver,
close_ping_rx,
);
})
.unwrap();
});
let weak_app = app.as_weak();
let write_sender_join = netplay_write_sender.clone();
let read_sender_join = netplay_read_sender.clone();
let close_ping_join = close_ping_rx.resubscribe();
app.on_join_session_button_clicked(move || {
let write_sender = write_sender_join.clone();
let read_receiver = netplay_read_receiver.resubscribe();
let read_sender = read_sender_join.clone();
let write_receiver = netplay_write_receiver.resubscribe();
let close_ping_rx = close_ping_join.resubscribe();
weak_app
.upgrade_in_event_loop(move |handle| {
save_settings(&handle);
setup_join_window(
&handle,
write_sender,
read_receiver,
read_sender,
write_receiver,
close_ping_rx,
);
})
.unwrap();
});
let weak_app = app.as_weak();
app.on_netplay_select_rom(move |rom_dir| {
select_rom(weak_app.clone(), rom_dir);
});
let weak_app = app.as_weak();
let write_sender = netplay_write_sender.clone();
let read_sender = netplay_read_sender.clone();
app.on_netplay_close(move || {
weak_app
.upgrade_in_event_loop(move |handle| {
handle.set_show_netplay_wait_session(false);
handle.set_show_netplay_create_session(false);
handle.set_show_netplay_join_session(false);
handle.invoke_clear_netplay_data();
handle.invoke_close_message();
})
.unwrap();
let _ = write_sender.send(None); // close current websocket if any
let _ = read_sender.send(None); // close current receiver if any
let _ = close_ping_tx.send(()); // close ping
});
app.on_netplay_discord_button_clicked(move || {
open_uri("https://discord.gg/JyW6ZgBUyS");
});
app.on_netplay_feedback_button_clicked(move || {
open_uri("https://github.com/gopher64/gopher64/discussions/453");
});
setup_callbacks(
app,
&netplay_write_sender,
&netplay_read_receiver,
&netplay_read_sender,
&netplay_write_receiver,
&close_ping_tx,
&close_ping_rx,
);
}
+78 -79
View File
@@ -11,7 +11,7 @@ pub struct UsbData {
pub data_size: u32,
}
fn respond_to_handshake(usb_tx: &tokio::sync::broadcast::Sender<UsbData>, data: Vec<u8>) {
fn respond_to_handshake(usb_tx: &tokio::sync::mpsc::UnboundedSender<UsbData>, data: Vec<u8>) {
if let Ok(data) = String::from_utf8(data)
&& data == "N64"
{
@@ -29,98 +29,98 @@ fn respond_to_handshake(usb_tx: &tokio::sync::broadcast::Sender<UsbData>, data:
async fn handle_connection(
conn: tokio::net::TcpStream,
mut shutdown_rx: tokio::sync::watch::Receiver<()>,
mut usb_rx: tokio::sync::broadcast::Receiver<UsbData>,
usb_tx: tokio::sync::broadcast::Sender<UsbData>,
cart_tx: tokio::sync::broadcast::Sender<UsbData>,
usb_rx: &mut tokio::sync::mpsc::UnboundedReceiver<UsbData>,
usb_tx: tokio::sync::mpsc::UnboundedSender<UsbData>,
cart_tx: tokio::sync::mpsc::UnboundedSender<UsbData>,
) {
let (mut incoming, mut outgoing) = conn.into_split();
let mut shutdown_rx_clone = shutdown_rx.clone();
while usb_rx.try_recv().is_ok() {} // drain stale USB messages
tokio::spawn(async move {
let mut incoming_buffer = vec![0u8; 4096];
let mut data_type: Option<u32> = None;
let mut data_size: Option<u32> = None;
let mut usb_buffer: Vec<u8> = vec![];
loop {
tokio::select! {
data = usb_rx.recv() => {
match data {
Ok(data) => {
let mut output: Vec<u8> = vec![];
output.extend_from_slice(&data.data_type.to_be_bytes());
output.extend_from_slice(&data.data_size.to_be_bytes());
output.extend_from_slice(&data.data);
if outgoing.write_all(&output).await.is_err() {
break;
result = incoming.read(&mut incoming_buffer) => {
match result {
Ok(0) => {
break;
}
Ok(n) => {
usb_buffer.extend_from_slice(&incoming_buffer[0..n]);
if data_type.is_none() {
if usb_buffer.len() < 4 {
continue;
} else {
data_type = Some(u32::from_be_bytes(usb_buffer[0..4].try_into().unwrap()));
usb_buffer.drain(0..4);
}
}
if data_type.is_some() && data_size.is_none() {
if usb_buffer.len() < 4 {
continue;
} else {
data_size = Some(u32::from_be_bytes(usb_buffer[0..4].try_into().unwrap()));
usb_buffer.drain(0..4);
}
}
if let Some(d_type) = data_type && let Some(d_size) = data_size {
let length = d_size as usize;
if usb_buffer.len() >= length {
let usb_data = UsbData {
data: usb_buffer[0..length].to_vec(),
data_type: d_type,
data_size: d_size,
};
usb_buffer.drain(0..length);
if usb_data.data_type == DATATYPE_TCPTEST {
respond_to_handshake(&usb_tx,usb_data.data);
} else if usb_data.data_type == DATATYPE_ROMUPLOAD {
panic!("ROM upload not supported");
} else {
cart_tx.send(usb_data).unwrap();
}
data_type = None;
data_size = None;
}
}
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
panic!("usb_rx lagged");
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
break;
Err(_e) => {
break;
}
}
}
_ = shutdown_rx_clone.changed() => {
_ = shutdown_rx.changed() => {
break;
}
}
}
});
let mut incoming_buffer = vec![0u8; 4096];
let mut data_type: Option<u32> = None;
let mut data_size: Option<u32> = None;
let mut usb_buffer: Vec<u8> = vec![];
loop {
tokio::select! {
result = incoming.read(&mut incoming_buffer) => {
match result {
Ok(0) => {
break;
}
Ok(n) => {
usb_buffer.extend_from_slice(&incoming_buffer[0..n]);
if data_type.is_none() {
if usb_buffer.len() < 4 {
continue;
} else {
data_type = Some(u32::from_be_bytes(usb_buffer[0..4].try_into().unwrap()));
usb_buffer.drain(0..4);
}
}
if data_type.is_some() && data_size.is_none() {
if usb_buffer.len() < 4 {
continue;
} else {
data_size = Some(u32::from_be_bytes(usb_buffer[0..4].try_into().unwrap()));
usb_buffer.drain(0..4);
}
}
if let Some(d_type) = data_type && let Some(d_size) = data_size {
let length = d_size as usize;
if usb_buffer.len() >= length {
let usb_data = UsbData {
data: usb_buffer[0..length].to_vec(),
data_type: d_type,
data_size: d_size,
};
usb_buffer.drain(0..length);
if usb_data.data_type == DATATYPE_TCPTEST {
respond_to_handshake(&usb_tx,usb_data.data);
} else if usb_data.data_type == DATATYPE_ROMUPLOAD {
panic!("ROM upload not supported");
} else {
cart_tx.send(usb_data).unwrap();
}
data_type = None;
data_size = None;
}
data = usb_rx.recv() => {
match data {
Some(data) => {
let mut output: Vec<u8> = vec![];
output.extend_from_slice(&data.data_type.to_be_bytes());
output.extend_from_slice(&data.data_size.to_be_bytes());
output.extend_from_slice(&data.data);
if outgoing.write_all(&output).await.is_err() {
break;
}
}
Err(_e) => {
break;
None => {
break;
}
}
}
_ = shutdown_rx.changed() => {
_ = shutdown_rx_clone.changed() => {
break;
}
}
@@ -133,24 +133,23 @@ pub fn init() -> (
ui::Usb,
) {
let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(());
let (usb_tx, usb_rx): (
tokio::sync::broadcast::Sender<UsbData>,
tokio::sync::broadcast::Receiver<UsbData>,
) = tokio::sync::broadcast::channel(1024);
let (usb_tx, mut usb_rx): (
tokio::sync::mpsc::UnboundedSender<UsbData>,
tokio::sync::mpsc::UnboundedReceiver<UsbData>,
) = tokio::sync::mpsc::unbounded_channel();
let (cart_tx, cart_rx): (
tokio::sync::broadcast::Sender<UsbData>,
tokio::sync::broadcast::Receiver<UsbData>,
) = tokio::sync::broadcast::channel(1024);
tokio::sync::mpsc::UnboundedSender<UsbData>,
tokio::sync::mpsc::UnboundedReceiver<UsbData>,
) = tokio::sync::mpsc::unbounded_channel();
let usb_tx_clone = usb_tx.clone();
let cart_rx_clone = cart_rx.resubscribe();
let handle = tokio::spawn(async move {
if let Ok(listener) = tokio::net::TcpListener::bind("localhost:48646").await {
loop {
tokio::select! {
res = listener.accept() => {
if let Ok((c,_)) = res {
handle_connection(c,shutdown_rx.clone(),usb_rx.resubscribe(),usb_tx.clone(),cart_tx.clone()).await;
handle_connection(c,shutdown_rx.clone(),&mut usb_rx,usb_tx.clone(),cart_tx.clone()).await;
} else {
break;
}
@@ -169,7 +168,7 @@ pub fn init() -> (
Some(handle),
ui::Usb {
usb_tx: Some(usb_tx_clone),
cart_rx: Some(cart_rx_clone),
cart_rx: Some(cart_rx),
},
)
}
@@ -193,6 +192,6 @@ pub fn close(
}
}
pub fn send_to_usb(usb_tx: &tokio::sync::broadcast::Sender<UsbData>, buffer: UsbData) {
pub fn send_to_usb(usb_tx: &tokio::sync::mpsc::UnboundedSender<UsbData>, buffer: UsbData) {
usb_tx.send(buffer).unwrap();
}
+6 -6
View File
@@ -10,7 +10,7 @@ const NTSC_WIDESCREEN_WIDTH: i32 = 426;
const NTSC_STANDARD_WIDTH: i32 = 320;
const NTSC_HEIGHT: i32 = 240;
fn build_gfx_info(device: &mut device::Device) -> GFX_INFO {
fn build_gfx_info(device: &mut device::Device, netplay: bool) -> GFX_INFO {
GFX_INFO {
RDRAM: device.rdram.mem.as_mut_ptr(),
DMEM: device.rsp.mem.as_mut_ptr(),
@@ -22,7 +22,7 @@ fn build_gfx_info(device: &mut device::Device) -> GFX_INFO {
PAL: device.cart.pal,
widescreen: device.ui.config.video.widescreen,
fullscreen: device.ui.video.fullscreen,
vsync: if device.netplay.is_none() {
vsync: if !netplay {
device.ui.config.video.vsync
} else {
false
@@ -34,7 +34,7 @@ fn build_gfx_info(device: &mut device::Device) -> GFX_INFO {
}
}
pub fn init(device: &mut device::Device) {
pub fn init(device: &mut device::Device, netplay: bool) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
ui::ttf_init();
@@ -99,7 +99,7 @@ pub fn init(device: &mut device::Device) {
sdl3_sys::everything::SDL_HideCursor();
}
let gfx_info = build_gfx_info(device);
let gfx_info = build_gfx_info(device, netplay);
unsafe {
let font_bytes = include_bytes!("../../data/ui/RobotoMono-Regular.ttf");
@@ -170,7 +170,7 @@ pub fn idle() {
}
pub fn load_state(device: &mut device::Device, rdp_state: *const u8) {
let gfx_info = build_gfx_info(device);
let gfx_info = build_gfx_info(device, device.netplay.is_some());
unsafe {
rdp_load_state(gfx_info, rdp_state);
for reg in 0..device::vi::VI_REGS_COUNT {
@@ -247,7 +247,7 @@ pub fn check_callback(device: &mut device::Device) -> (bool, bool) {
if let Some(netplay) = &mut device.netplay
&& netplay.player_number == 0
{
if callback.decrease_input_delay && netplay.input_delay > 0 {
if callback.decrease_input_delay {
netplay::send_input_delay(netplay, netplay.input_delay - 1);
} else if callback.increase_input_delay {
netplay::send_input_delay(netplay, netplay.input_delay + 1);
+2 -2
View File
@@ -7,13 +7,13 @@ pub fn prompt_for_match(words: &[String], frame_time: f64) -> u16 {
dedup_words.sort();
dedup_words.dedup();
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let vru_dialog = gui::VruDialog::new().unwrap();
let vru_dialog_weak = vru_dialog.as_weak();
vru_dialog.on_vru_button_clicked(move |chosen_word| {
tx.try_send(chosen_word.to_string()).unwrap();
tx.send(chosen_word.to_string()).unwrap();
vru_dialog_weak.unwrap().hide().unwrap();
});