Compare commits

..

41 Commits

Author SHA1 Message Date
Logan McNaughton e4b7e291dd update socket2 (#469) 2025-07-05 17:52:29 +02:00
Logan McNaughton 16732d3d3c use is_multiple_of (#468) 2025-07-05 09:31:03 +02:00
Logan McNaughton 5857da0404 add cheats enabled var 2025-07-05 09:25:31 +02:00
Logan McNaughton 6c6d0f32a7 fix cheats 2025-07-04 22:33:25 +02:00
Logan McNaughton 7b400a0edc Update cheats (#467) 2025-07-04 22:13:52 +02:00
Logan McNaughton 8f6c6f1b94 Cheat support (#459)
* Cheat support

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2025-07-04 16:46:03 +02:00
Logan McNaughton 7fd43886de add a few vru phrases 2025-07-03 13:27:33 +02:00
Logan McNaughton 572807def0 add missing disable_expansion_pak in netplay features 2025-07-02 22:26:01 +02:00
Logan McNaughton 10a1e3a556 rename netplaydialog 2025-07-02 22:22:42 +02:00
Logan McNaughton c12dc460a7 update tokio 2025-07-02 22:17:34 +02:00
Logan McNaughton 72456b48e5 some code cleanup 2025-07-02 22:15:15 +02:00
Logan McNaughton 6ecb81fda1 move netplay_window to netplay.rs 2025-07-02 22:08:46 +02:00
Logan McNaughton 31b4e7944b non-empty default value for ram (#465) 2025-07-02 14:33:19 +02:00
Logan McNaughton dc1535ea04 better RAM access checks (#464)
* better RAM access checks

* more
2025-07-02 14:25:03 +02:00
Logan McNaughton 7a59714417 Make expansion pak optional (#463)
* Make expansion pak optional

* more
2025-07-02 09:59:01 +02:00
Logan McNaughton ead561933b Don't adjust variable limit after toggle (#458)
* Don't adjust variable limit after toggle

* more
2025-06-30 10:55:34 +02:00
Logan McNaughton 1d331e73fb cleanup aarch64 flags (#455) 2025-06-28 18:51:12 +02:00
Logan McNaughton 25aaa985f1 netplay feedback button (#454) 2025-06-28 16:50:59 +02:00
Logan McNaughton 11830b0222 add button for netplay discord (#452) 2025-06-28 14:19:50 +02:00
Logan McNaughton 3183a5eb0b add some more about buttons (#451) 2025-06-28 10:43:09 +02:00
Logan McNaughton 5d8ac37a0f Add new version button to main screen (#450) 2025-06-27 13:09:22 +02:00
Logan McNaughton baa6a6c635 Update download links 2025-06-27 10:15:25 +02:00
Logan McNaughton f389be0773 Fix netplay server url (#449) 2025-06-26 21:04:54 +02:00
Logan McNaughton 30eee885e4 Update to 1.0.19 (#448) 2025-06-26 20:58:41 +02:00
Logan McNaughton 3da98c58c5 use rust 1.88 (#447)
* Use Rust 1.88

* add win aarch64 (commented out)

* Clippy fixes
2025-06-26 20:36:10 +02:00
Logan McNaughton 6e9b8ae126 Don't enable VRU during netplay (#445) 2025-06-26 11:44:56 +02:00
Logan McNaughton ada6936b9a print message when creating/loading savestate 2025-06-26 09:50:22 +02:00
Logan McNaughton 0bb3d2cd5d Slint fixes (#433)
* Slint fixes

* more

* more

* more

* more

* more

* more

* more

* more

* more
2025-06-26 09:00:23 +02:00
Logan McNaughton f213b0b9a9 Use Slint for GUI (#432)
* Use Slint for GUI

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2025-06-25 12:38:33 +02:00
Logan McNaughton 69029f3a02 Don't panic on invalid transferpak access (#430) 2025-06-23 16:41:37 +02:00
dependabot[bot] 0c2b4b2aac Bump sevenz-rust2 from 0.14.1 to 0.15.1 (#428)
Bumps [sevenz-rust2](https://github.com/hasenbanck/sevenz-rust) from 0.14.1 to 0.15.1.
- [Changelog](https://github.com/hasenbanck/sevenz-rust2/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hasenbanck/sevenz-rust/compare/v0.14.1...v0.15.1)

---
updated-dependencies:
- dependency-name: sevenz-rust2
  dependency-version: 0.15.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-23 07:10:51 +02:00
Logan McNaughton c7680f10a0 fix compiler warning 2025-06-16 06:31:23 +02:00
dependabot[bot] a28ed6766b Bump tungstenite from 0.26.2 to 0.27.0 (#426)
Bumps [tungstenite](https://github.com/snapview/tungstenite-rs) from 0.26.2 to 0.27.0.
- [Changelog](https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/snapview/tungstenite-rs/compare/v0.26.2...v0.27.0)

---
updated-dependencies:
- dependency-name: tungstenite
  dependency-version: 0.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 05:51:54 +02:00
dependabot[bot] 6e6b1d463d Bump zip from 4.0.0 to 4.1.0 (#424)
Bumps [zip](https://github.com/zip-rs/zip2) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/compare/v4.0.0...v4.1.0)

---
updated-dependencies:
- dependency-name: zip
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 05:44:13 +02:00
dependabot[bot] 662c0060a1 Bump cc from 1.2.25 to 1.2.27 (#425)
Bumps [cc](https://github.com/rust-lang/cc-rs) from 1.2.25 to 1.2.27.
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.25...cc-v1.2.27)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.27
  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>
2025-06-16 05:36:29 +02:00
Logan McNaughton 79e4cb1807 Update mac runner 2025-06-13 11:04:05 +02:00
Logan McNaughton f3e819158a fix formatting 2025-06-12 18:41:44 +02:00
Logan McNaughton e83bcbb4ce remove session reference for netplay 2025-06-12 18:39:31 +02:00
Logan McNaughton eb5d255fd7 move get_controller_names 2025-06-12 13:34:59 +02:00
dependabot[bot] 776d127a84 Bump bindgen from 0.71.1 to 0.72.0 (#420)
Bumps [bindgen](https://github.com/rust-lang/rust-bindgen) from 0.71.1 to 0.72.0.
- [Release notes](https://github.com/rust-lang/rust-bindgen/releases)
- [Changelog](https://github.com/rust-lang/rust-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/rust-bindgen/compare/v0.71.1...v0.72.0)

---
updated-dependencies:
- dependency-name: bindgen
  dependency-version: 0.72.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-09 06:56:56 +02:00
Logan McNaughton af5f292459 Bump to 1.0.18 (#418) 2025-06-05 12:34:20 +02:00
55 changed files with 5855 additions and 2296 deletions
+7 -3
View File
@@ -56,13 +56,17 @@ jobs:
path: output/gopher64-${{ matrix.arch }}
build-windows:
runs-on: windows-2025
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-pc-windows-msvc
- os: windows-2025
target: x86_64-pc-windows-msvc
arch: windows-x86_64
#- os: windows-11-arm
# target: aarch64-pc-windows-msvc
# arch: windows-aarch64
steps:
- uses: actions/checkout@v4
with:
@@ -83,7 +87,7 @@ jobs:
path: output/gopher64-${{ matrix.arch }}.exe
build-macos:
runs-on: macos-14
runs-on: macos-15
strategy:
fail-fast: false
matrix:
Generated
+2273 -450
View File
File diff suppressed because it is too large Load Diff
+12 -9
View File
@@ -1,32 +1,34 @@
[package]
name = "gopher64"
version = "1.0.17"
version = "1.0.19"
edition = "2024"
rust-version = "1.87"
rust-version = "1.88"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dirs = "6.0"
zip = "4.0"
zip = "4.2"
governor = "0.10"
sevenz-rust2 = { version = "0.14", default-features = false }
sevenz-rust2 = { version = "0.16", default-features = false }
chrono = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-big-array = "0.5"
eframe = { version = "0.31", default-features = false, features = ["wayland", "x11", "glow"] }
slint = { version = "1.12", default-features = false, features = ["compat-1-2", "std", "backend-winit", "renderer-femtovg-wgpu"] }
open = "5.3"
sha2 = "0.10"
ab_glyph = "0.2"
sdl3-sys = { version = "0.5", features = ["build-from-source-static"] }
rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "tokio"] }
tokio = {version = "1.45", features = ["rt-multi-thread", "macros"] }
tokio = {version = "1.46", features = ["rt-multi-thread", "macros"] }
spin_sleep = "1.3"
encoding_rs = "0.8"
mimalloc = "0.1"
regex = "1.11"
tungstenite = "0.26"
socket2 = "0.5"
tokio-tungstenite = "0.27"
futures = "0.3"
socket2 = { version = "0.6", features = ["all"] }
rand_chacha = "0.9"
fatfs = "0.3"
semver = "1.0"
@@ -36,7 +38,8 @@ reqwest = { version = "0.12", default-features = false, features = ["http2","rus
[build-dependencies]
winresource = "0.1"
bindgen = "0.71"
bindgen = "0.72"
slint-build = "1.12"
cc = { version = "1.2", features = ["parallel"] }
[profile.release]
+3 -1
View File
@@ -1,7 +1,9 @@
# gopher64
## download
https://github.com/gopher64/gopher64/releases
Windows: https://github.com/gopher64/gopher64/releases/latest/download/gopher64-windows-x86_64.exe
Linux: https://github.com/gopher64/gopher64/releases/latest/download/gopher64-linux-x86_64
## wiki
+15 -4
View File
@@ -2,6 +2,9 @@ fn main() {
println!("cargo::rerun-if-changed=parallel-rdp");
println!("cargo::rerun-if-changed=src/compat");
let slint_config = slint_build::CompilerConfiguration::new().with_style("cosmic".into());
slint_build::compile_with_config("src/ui/gui/appwindow.slint", slint_config).unwrap();
let mut simd_build = cc::Build::new();
let mut build = cc::Build::new();
build
@@ -70,7 +73,15 @@ fn main() {
panic!("unknown env")
}
} else if arch == "aarch64" {
panic!("unsupported platform")
if env == "msvc" {
build.flag("/arch:armv8.2");
simd_build.flag("/arch:armv8.2");
} else if env == "gnu" {
build.flag("-march=armv8.2-a");
simd_build.flag("-march=armv8.2-a");
} else {
panic!("unknown env")
}
} else {
panic!("unknown arch")
}
@@ -86,8 +97,6 @@ fn main() {
} else if arch == "aarch64" {
build.flag("-march=armv8.2-a");
simd_build.flag("-march=armv8.2-a");
simd_build.flag("-DSSE2NEON_SUPPRESS_WARNINGS");
simd_build.file("src/compat/aarch64.c");
} else {
panic!("unknown arch")
}
@@ -166,6 +175,8 @@ fn main() {
.write_to_file(out_path.join("simd_bindings.rs"))
.expect("Couldn't write bindings!");
simd_build.flag("-DSSE2NEON_SUPPRESS_WARNINGS");
simd_build.file("src/compat/aarch64.c");
simd_build.file(std::env::temp_dir().join("bindgen").join("extern.c"));
simd_build.include(".");
simd_build.compile("simd");
@@ -177,7 +188,7 @@ fn main() {
.unwrap();
let git_hash = String::from_utf8(git_output.stdout).unwrap();
println!("cargo:rustc-env=GIT_HASH={}", git_hash);
println!("cargo:rustc-env=GIT_HASH={git_hash}");
println!("cargo:rustc-env=N64_STACK_SIZE={}", 8 * 1024 * 1024);
}
+1
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -547,6 +547,8 @@ uint64_t rdp_process_commands()
if (interrupt_timer == 0)
interrupt_timer = 5000;
break;
default:
break;
}
rdp_device.cmd_cur += cmd_length;
+1 -1
View File
@@ -1,2 +1,2 @@
[toolchain]
channel = "1.87.0"
channel = "1.88.0"
+189
View File
@@ -0,0 +1,189 @@
use crate::device;
use crate::ui;
#[derive(serde::Serialize, serde::Deserialize, Copy, Clone)]
pub struct DecodedCheat {
code_type: u8,
address: u32,
data: u16,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Cheats {
pub cheats: Vec<Vec<DecodedCheat>>,
pub boot: bool,
pub enabled: bool,
}
pub fn init(
device: &mut device::Device,
cheat_settings: std::collections::HashMap<String, Option<String>>,
) {
let cheats =
serde_json::from_slice::<ui::cheats::Cheats>(include_bytes!("../data/cheats.json"))
.unwrap()
.get(&ui::storage::get_game_crc(&device.cart.rom))
.cloned()
.unwrap_or_default();
let re = regex::Regex::new(r"\?+").unwrap();
for cheat_setting in cheat_settings.iter() {
if let Some(cheat_data) = cheats.get(cheat_setting.0) {
let mut option_value = None;
if let Some(option) = cheat_setting.1 {
if let Some(found_option_value) = cheat_data
.options
.as_ref()
.cloned()
.unwrap_or_default()
.get(option)
{
option_value = Some(found_option_value.to_string());
} else {
panic!("Cheat option: {option} not found");
}
}
println!(
"Cheat: {} Option: {}",
cheat_setting.0,
cheat_setting.1.clone().unwrap_or("none".to_string())
);
let mut decoded_cheat: Vec<DecodedCheat> = vec![];
for code in cheat_data.data.iter() {
let mut result = code.clone();
if let Some(option_value) = option_value.as_ref() {
result = re.replace_all(code, option_value).into_owned();
}
let mut split = result.split_whitespace();
let first_part = u32::from_str_radix(split.next().unwrap(), 16).unwrap();
decoded_cheat.push(DecodedCheat {
code_type: (first_part >> 24) as u8,
address: first_part & 0x00FFFFFF,
data: u16::from_str_radix(split.next().unwrap(), 16).unwrap(),
});
}
device.cheats.cheats.push(decoded_cheat);
} else {
println!("Could not find cheat: {}", cheat_setting.0);
}
}
if !device.cheats.cheats.is_empty() {
device.cheats.enabled = true;
ui::audio::play_cheat_event(&device.ui);
}
}
fn write_byte(device: &mut device::Device, cheat_line: &DecodedCheat) {
*device
.rdram
.mem
.get_mut(cheat_line.address as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = cheat_line.data as u8;
}
fn write_half(device: &mut device::Device, cheat_line: &DecodedCheat) {
*device
.rdram
.mem
.get_mut(cheat_line.address as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = (cheat_line.data >> 8) as u8;
*device
.rdram
.mem
.get_mut((cheat_line.address + 1) as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = cheat_line.data as u8;
}
fn equal_byte(device: &mut device::Device, cheat_line: &DecodedCheat) -> bool {
let current_value = *device
.rdram
.mem
.get(cheat_line.address as usize ^ device.byte_swap)
.unwrap_or(&0);
current_value == (cheat_line.data as u8)
}
fn equal_half(device: &mut device::Device, cheat_line: &DecodedCheat) -> bool {
let current_value1 = *device
.rdram
.mem
.get(cheat_line.address as usize ^ device.byte_swap)
.unwrap_or(&0);
let current_value2 = *device
.rdram
.mem
.get((cheat_line.address + 1) as usize ^ device.byte_swap)
.unwrap_or(&0);
current_value1 == (cheat_line.data >> 8) as u8 && current_value2 == cheat_line.data as u8
}
pub fn execute_cheats(device: &mut device::Device, cheats: Vec<Vec<DecodedCheat>>) {
for cheat in cheats.iter() {
let mut valid = true;
let mut cheat_iter = cheat.iter().peekable();
while let Some(cheat_line) = cheat_iter.next() {
match cheat_line.code_type {
0x50 => {
if valid {
let mut expanded_cheat: Vec<DecodedCheat> = vec![];
let compressed_cheat = *cheat_iter.peek().unwrap();
let count = (cheat_line.address & 0xFF00) >> 8;
let offset = cheat_line.address & 0xFF;
for i in 0..count {
let line = DecodedCheat {
code_type: compressed_cheat.code_type,
address: compressed_cheat.address + (i * offset),
data: compressed_cheat.data
+ (i as i16 * cheat_line.data as i16) as u16,
};
expanded_cheat.push(line);
}
execute_cheats(device, [expanded_cheat].to_vec());
}
valid = false; // skip the next line
}
0x80 | 0xA0 => {
if valid {
write_byte(device, cheat_line);
}
valid = true;
}
0x81 | 0xA1 => {
if valid {
write_half(device, cheat_line);
}
valid = true;
}
0xF0 => {
if device.cheats.boot && valid {
write_byte(device, cheat_line);
valid = true;
}
}
0xF1 => {
if device.cheats.boot && valid {
write_half(device, cheat_line);
valid = true;
}
}
0xD0 => {
valid = equal_byte(device, cheat_line);
}
0xD1 => {
valid = equal_half(device, cheat_line);
}
0xD2 => {
valid = !equal_byte(device, cheat_line);
}
0xD3 => {
valid = !equal_half(device, cheat_line);
}
0x88 | 0x89 | 0xD8 | 0xD9 | 0xDA | 0xDB => { /* GS button not emulated */ }
_ => panic!("Unknown cheat code type: {:X}", cheat_line.code_type),
}
}
}
device.cheats.boot = false;
}
+19 -9
View File
@@ -12,7 +12,7 @@ use std::arch::x86_64::*;
use rand_chacha::rand_core::{RngCore, SeedableRng};
use crate::{netplay, ui};
use crate::{cheats, netplay, ui};
use std::{collections::HashMap, fs, io::Read};
pub mod ai;
@@ -44,9 +44,12 @@ pub mod tlb;
pub mod unmapped;
pub mod vi;
pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, fullscreen: bool, overclock: bool) {
device.ui.video.fullscreen = fullscreen;
device.cpu.overclock = overclock;
pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, game_settings: ui::gui::GameSettings) {
device.ui.video.fullscreen = game_settings.fullscreen;
device.cpu.overclock = game_settings.overclock;
if game_settings.disable_expansion_pak {
device.rdram.size = 0x400000;
}
init_rng(device);
@@ -61,7 +64,7 @@ pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, fullscreen: bool, ov
mi::init(device);
pif::init(device);
if device.ui.config.input.emulate_vru {
if device.ui.config.input.emulate_vru && device.netplay.is_none() {
controller::vru::init(device);
}
memory::init(device);
@@ -74,6 +77,8 @@ pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, fullscreen: bool, ov
ui::storage::init(&mut device.ui, &device.cart.rom);
ui::storage::load_saves(&mut device.ui, &mut device.netplay);
cheats::init(device, game_settings.cheats);
cpu::run(device);
ui::input::close(&mut device.ui);
@@ -146,12 +151,12 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
}
} else if file_path.extension().unwrap().eq_ignore_ascii_case("7z") {
let mut archive =
sevenz_rust2::SevenZReader::open(file_path, sevenz_rust2::Password::empty()).unwrap();
sevenz_rust2::ArchiveReader::open(file_path, sevenz_rust2::Password::empty()).unwrap();
let mut found = false;
archive
.for_each_entries(
&mut |entry: &sevenz_rust2::SevenZArchiveEntry, reader: &mut dyn std::io::Read| {
&mut |entry: &sevenz_rust2::ArchiveEntry, reader: &mut dyn std::io::Read| {
let name = entry.name().to_ascii_lowercase();
if !found
&& (name.ends_with("z64") || name.ends_with("n64") || name.ends_with("v64"))
@@ -185,7 +190,7 @@ pub struct Device {
pub netplay: Option<netplay::Netplay>,
#[serde(skip, default = "ui::Ui::default")]
pub ui: ui::Ui,
byte_swap: usize,
pub byte_swap: usize,
pub save_state: bool,
pub load_state: bool,
pub cpu: cpu::Cpu,
@@ -206,6 +211,7 @@ pub struct Device {
pub vru: controller::vru::Vru,
pub vru_window: controller::vru::VruWindow,
pub transferpaks: [controller::transferpak::TransferPak; 4],
pub cheats: cheats::Cheats,
}
pub fn zero_m128i() -> __m128i {
@@ -479,7 +485,6 @@ impl Device {
vru_window: controller::vru::VruWindow {
window_notifier: None,
word_receiver: None,
gui_ctx: None,
},
vru: controller::vru::Vru {
status: 0,
@@ -498,6 +503,11 @@ impl Device {
controller::transferpak::TransferPak::default(),
controller::transferpak::TransferPak::default(),
],
cheats: cheats::Cheats {
cheats: vec![],
boot: true,
enabled: false,
},
}
}
}
+2 -2
View File
@@ -146,8 +146,8 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
AI_DACRATE_REG => {
if device.ai.regs[reg as usize] != value & mask {
device.ai.freq = device.vi.clock / (1 + (value & mask)) as u64;
ui::audio::close(&mut device.ui);
ui::audio::init(&mut device.ui, device.ai.freq)
ui::audio::close_game_audio(&mut device.ui);
ui::audio::init_game_audio(&mut device.ui, device.ai.freq)
}
device::memory::masked_write_32(&mut device.ai.regs[reg as usize], value, mask)
}
+15 -4
View File
@@ -91,7 +91,11 @@ pub fn dma_read(
for i in 0..length {
device.ui.storage.saves.romsave.data.insert(
cart_addr + i,
device.rdram.mem[(dram_addr + i) as usize ^ device.byte_swap],
*device
.rdram
.mem
.get((dram_addr + i) as usize ^ device.byte_swap)
.unwrap_or(&0),
);
}
device.ui.storage.saves.romsave.written = true;
@@ -113,10 +117,17 @@ pub fn dma_write(
let mut j = cart_addr;
while i < dram_addr + length {
if let Some(value) = device.ui.storage.saves.romsave.data.get(&j) {
device.rdram.mem[i as usize ^ device.byte_swap] = *value;
*device
.rdram
.mem
.get_mut(i as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = *value;
} else {
device.rdram.mem[i as usize ^ device.byte_swap] =
*device.cart.rom.get(j as usize).unwrap_or(&0);
*device
.rdram
.mem
.get_mut(i as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = *device.cart.rom.get(j as usize).unwrap_or(&0);
}
i += 1;
j += 1;
+13 -5
View File
@@ -51,7 +51,7 @@ pub fn read_regs(
match reg as u32 {
SC64_SCR_REG | SC64_DATA0_REG | SC64_DATA1_REG => device.cart.sc64.regs[reg as usize],
SC64_IDENTIFIER_REG => 0x53437632,
_ => panic!("unknown read reg {} address {:#x}", reg, address),
_ => panic!("unknown read reg {reg} address {address:#x}"),
}
}
@@ -308,8 +308,12 @@ pub fn dma_read(
let mut i = dram_addr;
let mut j = cart_addr;
while i < dram_addr + length && i < device.rdram.size {
buffer[j as usize] = device.rdram.mem[i as usize ^ device.byte_swap];
while i < dram_addr + length {
buffer[j as usize] = *device
.rdram
.mem
.get(i as usize ^ device.byte_swap)
.unwrap_or(&0);
i += 1;
j += 1;
}
@@ -335,8 +339,12 @@ pub fn dma_write(
let mut i = dram_addr;
let mut j = cart_addr;
while i < dram_addr + length && i < device.rdram.size {
device.rdram.mem[i as usize ^ device.byte_swap] = buffer[j as usize];
while i < dram_addr + length {
*device
.rdram
.mem
.get_mut(i as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = buffer[j as usize];
i += 1;
j += 1;
}
+33 -13
View File
@@ -66,7 +66,7 @@ fn read_mem_flash(device: &device::Device, address: u64) -> u32 {
{
/* flashram MMIO read are not supported except for the "dummy" read @0x0000 done before DMA.
* returns a "dummy" value. */
return 0;
0
} else {
/* other accesses are not implemented */
panic!("unknown flashram read")
@@ -150,9 +150,12 @@ fn dma_read_sram(device: &mut device::Device, mut cart_addr: u32, mut dram_addr:
format_sram(device);
while i < dram_addr + length && i < device.rdram.size {
device.ui.storage.saves.sram.data[j as usize] =
device.rdram.mem[i as usize ^ device.byte_swap];
while i < dram_addr + length {
device.ui.storage.saves.sram.data[j as usize] = *device
.rdram
.mem
.get(i as usize ^ device.byte_swap)
.unwrap_or(&0);
i += 1;
j += 1;
}
@@ -169,8 +172,11 @@ fn dma_read_flash(device: &mut device::Device, cart_addr: u32, dram_addr: u32, l
{
/* load page buf using DMA */
for i in 0..length {
device.cart.flashram.page_buf[i as usize] =
device.rdram.mem[(dram_addr + i) as usize ^ device.byte_swap];
device.cart.flashram.page_buf[i as usize] = *device
.rdram
.mem
.get((dram_addr + i) as usize ^ device.byte_swap)
.unwrap_or(&0);
}
} else {
/* other accesses are not implemented */
@@ -206,9 +212,12 @@ fn dma_write_sram(
format_sram(device);
while i < dram_addr + length && i < device.rdram.size {
device.rdram.mem[i as usize ^ device.byte_swap] =
device.ui.storage.saves.sram.data[j as usize];
while i < dram_addr + length {
*device
.rdram
.mem
.get_mut(i as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = device.ui.storage.saves.sram.data[j as usize];
i += 1;
j += 1;
}
@@ -227,10 +236,18 @@ fn dma_write_flash(
&& device.cart.flashram.mode == FlashramMode::ReadSiliconId
{
/* read Silicon ID using DMA */
device.rdram.mem[dram_addr as usize..dram_addr as usize + 4]
device
.rdram
.mem
.get_mut(dram_addr as usize..dram_addr as usize + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&device.cart.flashram.silicon_id[0].to_ne_bytes());
dram_addr += 4;
device.rdram.mem[dram_addr as usize..dram_addr as usize + 4]
device
.rdram
.mem
.get_mut(dram_addr as usize..dram_addr as usize + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&device.cart.flashram.silicon_id[1].to_ne_bytes());
} else if (cart_addr & 0x1ffff) < 0x10000
&& device.cart.flashram.mode == FlashramMode::ReadArray
@@ -250,8 +267,11 @@ fn dma_write_flash(
/* do actual DMA */
for i in 0..length {
device.rdram.mem[(dram_addr + i) as usize ^ device.byte_swap] =
device.ui.storage.saves.flash.data[(cart_addr + i) as usize];
*device
.rdram
.mem
.get_mut((dram_addr + i) as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = device.ui.storage.saves.flash.data[(cart_addr + i) as usize];
}
} else {
/* other accesses are not implemented */
+5 -5
View File
@@ -96,7 +96,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
device.pif.channels[channel].rx_buf.unwrap(),
channel,
),
_ => println!("unknown controller command {}", cmd),
_ => println!("unknown controller command {cmd}"),
}
}
@@ -111,8 +111,8 @@ fn pak_read_block(
((device.pif.ram[addr_acrc] as u16) << 8) | (device.pif.ram[addr_acrc + 1] & 0xe0) as u16;
let handler = device.pif.channels[channel].pak_handler;
if handler.is_some() {
(handler.unwrap().read)(device, channel, address, data, PAK_CHUNK_SIZE);
if let Some(handler) = handler {
(handler.read)(device, channel, address, data, PAK_CHUNK_SIZE);
device.pif.ram[dcrc] = data_crc(device, data, PAK_CHUNK_SIZE)
} else {
device.pif.ram[dcrc] = !data_crc(device, data, PAK_CHUNK_SIZE)
@@ -130,8 +130,8 @@ fn pak_write_block(
((device.pif.ram[addr_acrc] as u16) << 8) | (device.pif.ram[addr_acrc + 1] & 0xe0) as u16;
let handler = device.pif.channels[channel].pak_handler;
if handler.is_some() {
(handler.unwrap().write)(device, channel, address, data, PAK_CHUNK_SIZE);
if let Some(handler) = handler {
(handler.write)(device, channel, address, data, PAK_CHUNK_SIZE);
device.pif.ram[dcrc] = data_crc(device, data, PAK_CHUNK_SIZE)
} else {
device.pif.ram[dcrc] = !data_crc(device, data, PAK_CHUNK_SIZE)
+6 -6
View File
@@ -155,7 +155,7 @@ fn write_mbc3(
}
}
} else {
panic!("Unsupported write address {:x}", address);
panic!("Unsupported write address {address:x}");
}
}
@@ -208,7 +208,7 @@ fn read_mbc3(
}
}
} else {
panic!("Unsupported read address {:x}", address);
panic!("Unsupported read address {address:x}");
}
}
@@ -231,7 +231,7 @@ fn write_mbc5(
} else if address < 0x6000 {
cart.ram_bank = (value & 0xf) as u16;
} else if address < 0xa000 {
println!("Unknown MBC5 write address {:x}", address);
println!("Unknown MBC5 write address {address:x}");
} else if (0xa000..0xc000).contains(&address) {
if !cart.ram_enabled {
return;
@@ -241,7 +241,7 @@ fn write_mbc5(
cart.ram[banked_address as usize..banked_address as usize + size]
.copy_from_slice(&pif_ram[data..data + size]);
} else {
panic!("Unsupported write address {:x}", address);
panic!("Unsupported write address {address:x}");
}
}
@@ -272,7 +272,7 @@ fn read_mbc5(
pif_ram[data..data + size]
.copy_from_slice(&cart.ram[banked_address as usize..banked_address as usize + size]);
} else {
panic!("Unsupported read address {:x}", address);
panic!("Unsupported read address {address:x}");
}
}
@@ -323,6 +323,6 @@ pub fn get_cart_type(data: u8) -> CartType {
0x10 => CartType::MBC3RamBattRtc,
0x13 => CartType::MBC3RamBatt,
0x1b => CartType::MBC5RamBatt,
_ => panic!("Unsupported cart type {:x}", data),
_ => panic!("Unsupported cart type {data:x}"),
}
}
+3 -3
View File
@@ -61,7 +61,7 @@ pub fn read(device: &mut device::Device, channel: usize, address: u16, data: usi
);
}
_ => {
panic!("unknown transfer pak read {:x}", address >> 12);
println!("unknown transfer pak read {:x}", address >> 12);
}
}
}
@@ -84,7 +84,7 @@ pub fn write(device: &mut device::Device, channel: usize, address: u16, data: us
}
pak.enabled = true;
}
_ => panic!("unknown transfer pak value"),
_ => println!("unknown transfer pak value"),
}
return;
}
@@ -127,7 +127,7 @@ pub fn write(device: &mut device::Device, channel: usize, address: u16, data: us
);
}
_ => {
panic!("unknown transfer pak write {:x}", address >> 12);
println!("unknown transfer pak write {:x}", address >> 12);
}
}
}
+16 -8
View File
@@ -1,5 +1,4 @@
use crate::{device, ui};
use eframe::egui;
use std::collections::HashMap;
const JCMD_VRU_READ: u8 = 0x09;
@@ -33,11 +32,9 @@ pub struct Vru {
#[derive(serde::Serialize, serde::Deserialize)]
pub struct VruWindow {
#[serde(skip)]
pub window_notifier: Option<tokio::sync::mpsc::Sender<Vec<String>>>,
pub window_notifier: Option<tokio::sync::mpsc::Sender<Option<Vec<String>>>>,
#[serde(skip)]
pub word_receiver: Option<tokio::sync::mpsc::Receiver<String>>,
#[serde(skip)]
pub gui_ctx: Option<egui::Context>,
}
pub fn init(device: &mut device::Device) {
@@ -129,7 +126,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
let (res, _enc, errors) = encoding_rs::SHIFT_JIS.decode(&data);
if errors {
panic!("Failed to decode Japanese word {:X?}", data);
panic!("Failed to decode Japanese word {data:X?}");
} else {
device.vru.words.push(res.to_string());
}
@@ -147,7 +144,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
if let Some(result) = word {
device.vru.words.push(result.clone());
} else {
panic!("Unknown VRU word {}", data);
panic!("Unknown VRU word {data}");
}
}
} else {
@@ -195,7 +192,6 @@ pub fn process(device: &mut device::Device, channel: usize) {
&device.vru.words,
device.vru_window.window_notifier.as_ref().unwrap(),
device.vru_window.word_receiver.as_mut().unwrap(),
device.vru_window.gui_ctx.as_ref().unwrap(),
)
} else {
0x7FFF
@@ -253,7 +249,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
}
device.vru.status = 0; /* status is always set to 0 after a write */
}
_ => panic!("unknown VRU command {}", cmd),
_ => panic!("unknown VRU command {cmd}"),
}
}
@@ -2730,5 +2726,17 @@ fn create_word_mappings(device: &mut device::Device) {
String::from("82B582E382C182CF82C282AF82A282A982A2"),
String::from("restricted-depart"),
),
(
String::from("009903720366006903C903FF0045033C"),
String::from("objection"),
),
(
String::from("041100CF02EE0381004503B7"),
String::from("hold-it"),
),
(
String::from("03BA019203C9036C0087000303C6"),
String::from("take-that"),
),
]);
}
+1
View File
@@ -227,6 +227,7 @@ pub fn init(device: &mut device::Device) {
device.cpu.clock_rate = if !device.cpu.overclock {
93750000
} else {
println!("Overclocking enabled, setting clock rate to 125 MHz");
125000000
};
+5 -1
View File
@@ -221,7 +221,11 @@ fn unknown_dma_write(
dram_addr &= device::rdram::RDRAM_MASK as u32;
for i in 0..length {
device.rdram.mem[(dram_addr + i) as usize ^ device.byte_swap] = 0;
*device
.rdram
.mem
.get_mut((dram_addr + i) as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = 0;
}
device::pi::calculate_cycles(device, 1, length)
}
+2 -2
View File
@@ -226,7 +226,7 @@ pub fn connect_pif_channels(device: &mut device::Device) {
device.pif.channels[i].process = Some(device::controller::process);
}
}
if device.ui.config.input.emulate_vru {
if device.ui.config.input.emulate_vru && device.netplay.is_none() {
device.pif.channels[3].process = Some(device::controller::vru::process);
}
device.pif.channels[4].process = Some(device::cart::process)
@@ -280,7 +280,7 @@ pub fn init(device: &mut device::Device) {
device.pif.channels[i].pak_handler = Some(default_handler);
}
}
if device.ui.config.input.emulate_vru {
if device.ui.config.input.emulate_vru && device.netplay.is_none() {
device.pif.channels[3].pak_handler = None;
}
}
+44 -23
View File
@@ -29,9 +29,13 @@ pub fn read_mem_fast(
) -> u32 {
let masked_address = address as usize & RDRAM_MASK;
u32::from_ne_bytes(
device.rdram.mem[masked_address..masked_address + 4]
device
.rdram
.mem
.get(masked_address..masked_address + 4)
.unwrap_or_default()
.try_into()
.unwrap(),
.unwrap_or_default(),
)
}
@@ -45,28 +49,35 @@ pub fn read_mem(
rdram_calculate_cycles(access_size as u64) / (access_size as u64 / 4),
);
let masked_address = address as usize & RDRAM_MASK;
if address < device.rdram.size as u64 {
u32::from_ne_bytes(
device.rdram.mem[masked_address..masked_address + 4]
.try_into()
.unwrap(),
)
} else {
0
}
u32::from_ne_bytes(
device
.rdram
.mem
.get(masked_address..masked_address + 4)
.unwrap_or_default()
.try_into()
.unwrap_or_default(),
)
}
pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u32) {
if address < device.rdram.size as u64 {
let mut data = u32::from_ne_bytes(
device.rdram.mem[address as usize..(address + 4) as usize]
.try_into()
.unwrap(),
);
device::memory::masked_write_32(&mut data, value, mask);
device.rdram.mem[address as usize..(address + 4) as usize]
.copy_from_slice(&data.to_ne_bytes());
}
let mut data = u32::from_ne_bytes(
device
.rdram
.mem
.get(address as usize..(address + 4) as usize)
.unwrap_or_default()
.try_into()
.unwrap_or_default(),
);
device::memory::masked_write_32(&mut data, value, mask);
device
.rdram
.mem
.get_mut(address as usize..(address + 4) as usize)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&data.to_ne_bytes());
}
pub fn read_regs(
@@ -103,9 +114,19 @@ pub fn init(device: &mut device::Device) {
unsafe { Vec::from_raw_parts(ptr, device.rdram.size as usize, device.rdram.size as usize) };
// hack, skip RDRAM initialization
device.rdram.mem[0x318..0x318 + 4].copy_from_slice(&device.rdram.size.to_ne_bytes());
device
.rdram
.mem
.get_mut(0x318..0x318 + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&device.rdram.size.to_ne_bytes());
// hack, skip RDRAM initialization
device.rdram.mem[0x3f0..0x3f0 + 4].copy_from_slice(&device.rdram.size.to_ne_bytes());
device
.rdram
.mem
.get_mut(0x3f0..0x3f0 + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&device.rdram.size.to_ne_bytes());
device.ri.regs[device::ri::RI_MODE_REG as usize] = 0x0e;
device.ri.regs[device::ri::RI_CONFIG_REG as usize] = 0x40;
+15 -9
View File
@@ -157,7 +157,11 @@ fn do_dma(device: &mut device::Device, dma: RspDma) {
.try_into()
.unwrap(),
);
device.rdram.mem[dram_addr as usize..dram_addr as usize + 4]
device
.rdram
.mem
.get_mut(dram_addr as usize..dram_addr as usize + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&data.to_ne_bytes());
mem_addr += 4;
dram_addr += 4;
@@ -171,14 +175,16 @@ fn do_dma(device: &mut device::Device, dma: RspDma) {
while j < count {
let mut i = 0;
while i < length {
let mut data = 0;
if dram_addr < device.rdram.size {
data = u32::from_ne_bytes(
device.rdram.mem[dram_addr as usize..dram_addr as usize + 4]
.try_into()
.unwrap(),
);
}
let data = u32::from_ne_bytes(
device
.rdram
.mem
.get(dram_addr as usize..dram_addr as usize + 4)
.unwrap_or_default()
.try_into()
.unwrap_or_default(),
);
if offset != 0 {
// imem being updated
device.rsp.cpu.instructions[((mem_addr & 0xFFF) / 4) as usize].func =
+8 -8
View File
@@ -709,7 +709,7 @@ pub fn lsv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 2 == 0 {
if element.is_multiple_of(2) {
modify_vpr16(
&mut device.rsp.cpu.vpr[rt(opcode) as usize],
element / 2,
@@ -736,7 +736,7 @@ pub fn llv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 4 == 0 {
if element.is_multiple_of(4) {
let mut value = 0;
for i in 0..4 {
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u32) << (8 * (3 - i));
@@ -766,7 +766,7 @@ pub fn ldv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 8 == 0 {
if element.is_multiple_of(8) {
let mut value = 0;
for i in 0..8 {
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u64) << (8 * (7 - i));
@@ -796,7 +796,7 @@ pub fn lqv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element == 0 && address % 16 == 0 {
if element == 0 && address.is_multiple_of(16) {
let mut value = 0;
for i in 0..16 {
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u128) << (8 * (15 - i));
@@ -975,7 +975,7 @@ pub fn ssv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 2 == 0 {
if element.is_multiple_of(2) {
let start = (address & 0xFFF) as usize;
device.rsp.mem[start..start + 2].copy_from_slice(
&get_vpr16(device.rsp.cpu.vpr[rt(opcode) as usize], element / 2).to_be_bytes(),
@@ -997,7 +997,7 @@ pub fn slv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 4 == 0 {
if element.is_multiple_of(4) {
let start = (address & 0xFFF) as usize;
device.rsp.mem[start..start + 4].copy_from_slice(
&get_vpr32(device.rsp.cpu.vpr[rt(opcode) as usize], element / 4).to_be_bytes(),
@@ -1019,7 +1019,7 @@ pub fn sdv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 8 == 0 {
if element.is_multiple_of(8) {
let start = (address & 0xFFF) as usize;
device.rsp.mem[start..start + 8].copy_from_slice(
&get_vpr64(device.rsp.cpu.vpr[rt(opcode) as usize], element / 8).to_be_bytes(),
@@ -1041,7 +1041,7 @@ pub fn sqv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element == 0 && address % 16 == 0 {
if element == 0 && address.is_multiple_of(16) {
let start = (address & 0xFFF) as usize;
device.rsp.mem[start..start + 16]
.copy_from_slice(&get_vpr128(device.rsp.cpu.vpr[rt(opcode) as usize]).to_be_bytes());
+12 -3
View File
@@ -86,9 +86,13 @@ fn copy_pif_rdram(device: &mut device::Device) {
let mut i = 0;
while i < device::pif::PIF_RAM_SIZE {
let data = u32::from_ne_bytes(
device.rdram.mem[dram_addr + i..dram_addr + i + 4]
device
.rdram
.mem
.get(dram_addr + i..dram_addr + i + 4)
.unwrap_or_default()
.try_into()
.unwrap(),
.unwrap_or_default(),
);
device.pif.ram[i..i + 4].copy_from_slice(&data.to_be_bytes());
i += 4;
@@ -97,7 +101,12 @@ fn copy_pif_rdram(device: &mut device::Device) {
let mut i = 0;
while i < device::pif::PIF_RAM_SIZE {
let data = u32::from_be_bytes(device.pif.ram[i..i + 4].try_into().unwrap());
device.rdram.mem[dram_addr + i..dram_addr + i + 4].copy_from_slice(&data.to_ne_bytes());
device
.rdram
.mem
.get_mut(dram_addr + i..dram_addr + i + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&data.to_ne_bytes());
i += 4;
}
} else {
+26 -19
View File
@@ -1,4 +1,4 @@
use crate::{device, netplay, ui};
use crate::{cheats, device, netplay, ui};
use governor::clock::Clock;
const VI_STATUS_REG: u32 = 0;
@@ -72,10 +72,9 @@ fn set_vertical_interrupt(device: &mut device::Device) {
fn set_current_line(device: &mut device::Device) {
let delay = device.vi.delay;
let next_vi = device::events::get_event(device, device::events::EVENT_TYPE_VI);
if next_vi.is_some() {
if let Some(next_vi) = next_vi {
device.vi.regs[VI_CURRENT_REG as usize] = ((delay
- (next_vi.unwrap().count
- device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize]))
- (next_vi.count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize]))
/ device.vi.count_per_scanline)
as u32;
@@ -135,16 +134,22 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
}
pub fn vertical_interrupt_event(device: &mut device::Device) {
ui::video::check_callback(device);
if device.cheats.enabled {
cheats::execute_cheats(device, device.cheats.cheats.clone());
}
let mut speed_limiter_toggled = ui::video::check_callback(device);
if device.netplay.is_some() {
netplay::send_sync_check(device);
device.vi.enable_speed_limiter = !device.netplay.as_ref().unwrap().fast_forward;
if device.vi.enable_speed_limiter == device.netplay.as_ref().unwrap().fast_forward {
speed_limiter_toggled = true;
device.vi.enable_speed_limiter = !device.netplay.as_ref().unwrap().fast_forward;
}
}
device.vi.vi_counter += 1;
if device.vi.vi_counter % device.vi.limit_freq == 0 && device.vi.enable_speed_limiter {
speed_limiter(device);
if device.vi.vi_counter.is_multiple_of(device.vi.limit_freq) && device.vi.enable_speed_limiter {
speed_limiter(device, speed_limiter_toggled);
}
ui::video::update_screen();
@@ -176,10 +181,9 @@ pub fn init(device: &mut device::Device) {
}
}
fn speed_limiter(device: &mut device::Device) {
fn speed_limiter(device: &mut device::Device, speed_limiter_toggled: bool) {
let result = device.vi.limiter.as_ref().unwrap().check();
if result.is_err() {
let outcome = result.unwrap_err();
if let Err(outcome) = result {
let dur = outcome.wait_time_from(governor::clock::DefaultClock::default().now());
spin_sleep::sleep(dur);
if dur < device.vi.min_wait_time {
@@ -196,14 +200,17 @@ fn speed_limiter(device: &mut device::Device) {
.as_secs_f64()
> 1.0
{
if device.vi.min_wait_time.as_secs_f64() == 0.0 && device.vi.limit_freq < MAX_LIMIT_FREQ {
device.vi.limit_freq += 1;
create_limiter(device);
} else if device.vi.min_wait_time.as_secs_f64() > device.vi.frame_time
&& device.vi.limit_freq > 1
{
device.vi.limit_freq -= 1;
create_limiter(device);
if !speed_limiter_toggled {
if device.vi.min_wait_time.as_secs_f64() == 0.0 && device.vi.limit_freq < MAX_LIMIT_FREQ
{
device.vi.limit_freq += 1;
create_limiter(device);
} else if device.vi.min_wait_time.as_secs_f64() > device.vi.frame_time
&& device.vi.limit_freq > 1
{
device.vi.limit_freq -= 1;
create_limiter(device);
}
}
//println!("limit freq: {}", device.vi.limit_freq);
+21 -43
View File
@@ -3,6 +3,7 @@
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
mod cheats;
mod device;
mod netplay;
mod savestates;
@@ -73,10 +74,6 @@ async fn main() {
if result.is_err() {
panic!("could not create config dir: {}", result.err().unwrap())
}
result = std::fs::create_dir_all(dirs.cache_dir.clone());
if result.is_err() {
panic!("could not create cache dir: {}", result.err().unwrap())
}
result = std::fs::create_dir_all(dirs.data_dir.join("saves"));
if result.is_err() {
panic!("could not create save dir: {}", result.err().unwrap())
@@ -85,13 +82,6 @@ async fn main() {
if result.is_err() {
panic!("could not create state dir: {}", result.err().unwrap())
}
let running_file = dirs.cache_dir.join("game_running");
if running_file.exists() {
result = std::fs::remove_file(running_file);
if result.is_err() {
panic!("could not remove running file: {}", result.err().unwrap())
}
}
let args = Args::parse();
let args_as_strings: Vec<String> = std::env::args().collect();
@@ -111,9 +101,9 @@ async fn main() {
}
}
if args.list_controllers {
let controllers = gui::get_controller_names(&ui);
let controllers = ui::input::get_controller_names(&ui);
for (i, controller) in controllers.iter().enumerate() {
println!("Controller {}: {}", i, controller);
println!("Controller {i}: {controller}");
}
return;
}
@@ -158,7 +148,23 @@ async fn main() {
.spawn(move || {
let mut device = device::Device::new();
let overclock = device.ui.config.emulation.overclock;
device::run_game(&mut device, rom_contents, args.fullscreen, overclock);
let disable_expansion_pak = device.ui.config.emulation.disable_expansion_pak;
let game_cheats = {
let game_crc = ui::storage::get_game_crc(&rom_contents);
let cheats = ui::config::Cheats::new();
cheats.cheats.get(&game_crc).cloned().unwrap_or_default()
};
device::run_game(
&mut device,
rom_contents,
ui::gui::GameSettings {
fullscreen: args.fullscreen,
overclock,
disable_expansion_pak,
cheats: game_cheats,
},
);
})
.unwrap();
@@ -168,34 +174,6 @@ async fn main() {
return;
}
} else {
let options = eframe::NativeOptions {
viewport: eframe::egui::ViewportBuilder::default()
.with_inner_size([854.0, 480.0])
.with_icon(
eframe::icon_data::from_png_bytes(include_bytes!("../data/gopher64.png"))
.unwrap(),
),
..Default::default()
};
let controllers_paths;
let controller_names;
{
let game_ui = ui::Ui::new();
controllers_paths = gui::get_controller_paths(&game_ui);
controller_names = gui::get_controller_names(&game_ui);
}
eframe::run_native(
"gopher64",
options,
Box::new(|cc| {
Ok(Box::new(ui::gui::GopherEguiApp::new(
cc,
controllers_paths,
controller_names,
)))
}),
)
.unwrap();
gui::app_window();
}
}
+3 -8
View File
@@ -113,7 +113,7 @@ pub fn receive_save(netplay: &mut Netplay, save_type: &str, save_data: &mut Vec<
pub fn send_sync_check(device: &mut device::Device) {
let netplay = device.netplay.as_mut().unwrap();
if netplay.vi_counter % 600 == 0 {
if netplay.vi_counter.is_multiple_of(600) {
let mut request: Vec<u8> = [UDP_SYNC_DATA].to_vec();
request.extend_from_slice(&(netplay.vi_counter).to_be_bytes());
@@ -246,18 +246,13 @@ fn process_incoming(netplay: &mut Netplay, ui: &ui::Ui) {
}
}
pub fn init(
mut peer_addr: std::net::SocketAddr,
session: ui::gui::gui_netplay::NetplayRoom,
player_number: u8,
) -> Netplay {
peer_addr.set_port(session.port.unwrap() as u16);
pub fn init(peer_addr: std::net::SocketAddr, player_number: u8) -> Netplay {
let udp_socket;
if peer_addr.is_ipv4() {
udp_socket = std::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0))
.expect("couldn't bind to address");
socket2::SockRef::from(&udp_socket)
.set_tos(CS4 << 2)
.set_tos_v4(CS4 << 2)
.unwrap();
} else {
udp_socket = std::net::UdpSocket::bind((std::net::Ipv6Addr::UNSPECIFIED, 0))
+14 -5
View File
@@ -12,7 +12,7 @@ impl<'de, const N: usize> Visitor<'de> for M128iArrayVisitor<N> {
type Value = [__m128i; N];
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str(&format!("an array of {} 128-bit integers", N))
formatter.write_str(&format!("an array of {N} 128-bit integers"))
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
@@ -87,14 +87,18 @@ pub fn create_savestate(device: &device::Device) {
compressed_file,
)
.unwrap();
println!(
"Savestate created at {}",
device.ui.storage.paths.savestate_file_path.display()
);
}
pub fn load_savestate(device: &mut device::Device) {
let savestate = std::fs::read(&device.ui.storage.paths.savestate_file_path);
if savestate.is_ok() {
let device_bytes = ui::storage::decompress_file(savestate.as_ref().unwrap(), "device");
let save_bytes = ui::storage::decompress_file(savestate.as_ref().unwrap(), "saves");
let rdp_state = ui::storage::decompress_file(savestate.as_ref().unwrap(), "rdp_state");
if let Ok(savestate) = &savestate {
let device_bytes = ui::storage::decompress_file(savestate, "device");
let save_bytes = ui::storage::decompress_file(savestate, "saves");
let rdp_state = ui::storage::decompress_file(savestate, "rdp_state");
if let Ok(state) = postcard::from_bytes::<device::Device>(&device_bytes) {
device.ui.storage.saves = postcard::from_bytes(&save_bytes).unwrap();
@@ -116,6 +120,7 @@ pub fn load_savestate(device: &mut device::Device) {
device.si = state.si;
device.ri = state.ri;
device.vru = state.vru;
device.cheats = state.cheats;
let mut tpak_rom = [vec![], vec![], vec![], vec![]];
for (i, item) in tpak_rom.iter_mut().enumerate() {
@@ -190,6 +195,10 @@ pub fn load_savestate(device: &mut device::Device) {
ui::audio::close(&mut device.ui);
ui::audio::init(&mut device.ui, device.ai.freq);
ui::video::load_state(device, rdp_state.as_ptr());
println!(
"Savestate loaded from {}",
device.ui.storage.paths.savestate_file_path.display()
);
} else {
println!("Failed to load savestate");
}
+6 -6
View File
@@ -1,18 +1,20 @@
pub mod audio;
pub mod cheats;
pub mod config;
pub mod gui;
pub mod input;
pub mod netplay;
pub mod storage;
pub mod video;
pub mod vru;
pub struct Dirs {
pub config_dir: std::path::PathBuf,
pub cache_dir: std::path::PathBuf,
pub data_dir: std::path::PathBuf,
}
pub struct Audio {
pub audio_device_spec: sdl3_sys::audio::SDL_AudioSpec,
pub audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
pub event_audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
pub audio_device: u32,
@@ -66,7 +68,7 @@ pub fn sdl_init(flag: sdl3_sys::init::SDL_InitFlags) {
let err = std::ffi::CStr::from_ptr(sdl3_sys::error::SDL_GetError())
.to_str()
.unwrap();
panic!("Could not initialize SDL subsystem: {}, {}", flag, err);
panic!("Could not initialize SDL subsystem: {flag}, {err}");
}
}
}
@@ -76,21 +78,17 @@ pub fn get_dirs() -> Dirs {
let portable_dir = exe_path.parent();
let portable = portable_dir.unwrap().join("portable.txt").exists();
let config_dir;
let cache_dir;
let data_dir;
if portable {
config_dir = portable_dir.unwrap().join("portable_data").join("config");
cache_dir = portable_dir.unwrap().join("portable_data").join("cache");
data_dir = portable_dir.unwrap().join("portable_data").join("data");
} else {
config_dir = dirs::config_dir().unwrap().join("gopher64");
cache_dir = dirs::cache_dir().unwrap().join("gopher64");
data_dir = dirs::data_dir().unwrap().join("gopher64");
};
Dirs {
config_dir,
cache_dir,
data_dir,
}
}
@@ -182,7 +180,9 @@ impl Ui {
include_bytes!("../data/netplay_p3_disconnected.wav").to_vec(),
include_bytes!("../data/netplay_p4_disconnected.wav").to_vec(),
],
cheats_enabled: include_bytes!("../data/cheats_enabled.wav").to_vec(),
},
audio_device_spec: Default::default(),
audio_stream: std::ptr::null_mut(),
event_audio_stream: std::ptr::null_mut(),
audio_device: 0,
+57 -29
View File
@@ -9,16 +9,12 @@ pub struct EventAudio {
pub netplay_desync: Vec<u8>,
pub netplay_lost_connection: Vec<u8>,
pub netplay_disconnected: [Vec<u8>; 4],
pub cheats_enabled: Vec<u8>,
}
pub fn init(ui: &mut ui::Ui, frequency: u64) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_AUDIO);
let game_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: frequency as i32,
channels: 2,
};
let device_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: 48000,
@@ -34,37 +30,25 @@ pub fn init(ui: &mut ui::Ui, frequency: u64) {
panic!("Could not open audio device");
}
let mut dst = Default::default();
if !unsafe {
sdl3_sys::audio::SDL_GetAudioDeviceFormat(
ui.audio.audio_device,
&mut dst,
&mut ui.audio.audio_device_spec,
std::ptr::null_mut(),
)
} {
panic!("Could not get audio device format");
}
ui.audio.audio_stream =
unsafe { sdl3_sys::audio::SDL_CreateAudioStream(&game_audio_spec, &dst) };
if ui.audio.audio_stream.is_null() {
return;
}
if !unsafe {
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain)
&& sdl3_sys::audio::SDL_BindAudioStream(ui.audio.audio_device, ui.audio.audio_stream)
} {
panic!("Could not bind audio stream");
}
let wav_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: 24000,
channels: 1,
};
ui.audio.event_audio_stream =
unsafe { sdl3_sys::audio::SDL_CreateAudioStream(&wav_audio_spec, &dst) };
ui.audio.event_audio_stream = unsafe {
sdl3_sys::audio::SDL_CreateAudioStream(&wav_audio_spec, &ui.audio.audio_device_spec)
};
if !unsafe {
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.event_audio_stream, ui.audio.gain)
&& sdl3_sys::audio::SDL_BindAudioStream(
@@ -74,14 +58,33 @@ pub fn init(ui: &mut ui::Ui, frequency: u64) {
} {
panic!("Could not bind audio stream");
}
init_game_audio(ui, frequency);
}
pub fn init_game_audio(ui: &mut ui::Ui, frequency: u64) {
let game_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: frequency as i32,
channels: 2,
};
ui.audio.audio_stream = unsafe {
sdl3_sys::audio::SDL_CreateAudioStream(&game_audio_spec, &ui.audio.audio_device_spec)
};
if ui.audio.audio_stream.is_null() {
return;
}
if !unsafe {
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain)
&& sdl3_sys::audio::SDL_BindAudioStream(ui.audio.audio_device, ui.audio.audio_stream)
} {
panic!("Could not bind audio stream");
}
}
pub fn close(ui: &mut ui::Ui) {
close_game_audio(ui);
unsafe {
if !ui.audio.audio_stream.is_null() {
sdl3_sys::audio::SDL_DestroyAudioStream(ui.audio.audio_stream);
ui.audio.audio_stream = std::ptr::null_mut();
}
if !ui.audio.event_audio_stream.is_null() {
sdl3_sys::audio::SDL_DestroyAudioStream(ui.audio.event_audio_stream);
ui.audio.event_audio_stream = std::ptr::null_mut();
@@ -91,6 +94,15 @@ pub fn close(ui: &mut ui::Ui) {
}
}
pub fn close_game_audio(ui: &mut ui::Ui) {
unsafe {
if !ui.audio.audio_stream.is_null() {
sdl3_sys::audio::SDL_DestroyAudioStream(ui.audio.audio_stream);
ui.audio.audio_stream = std::ptr::null_mut();
}
}
}
pub fn lower_audio_volume(ui: &mut ui::Ui) {
unsafe {
ui.audio.gain = sdl3_sys::audio::SDL_GetAudioStreamGain(ui.audio.audio_stream) - 0.05;
@@ -113,6 +125,22 @@ pub fn raise_audio_volume(ui: &mut ui::Ui) {
}
}
pub fn play_cheat_event(ui: &ui::Ui) {
if ui.audio.event_audio_stream.is_null() {
return;
}
let sound = &ui.audio.event_audio.cheats_enabled;
if !unsafe {
sdl3_sys::audio::SDL_PutAudioStreamData(
ui.audio.event_audio_stream,
sound.as_ptr() as *const std::ffi::c_void,
sound.len() as i32,
)
} {
panic!("Could not play audio");
}
}
pub fn play_netplay_audio(ui: &ui::Ui, error: u32) {
if ui.audio.event_audio_stream.is_null() {
return;
@@ -176,12 +204,12 @@ pub fn play_audio(device: &device::Device, dram_addr: usize, length: u64) {
let mut i = 0;
while i < length as usize / 2 {
// Left channel
primary_buffer[i] = device.rdram.mem[dram_addr + (i * 2) + 2] as i16
| ((device.rdram.mem[dram_addr + (i * 2) + 3] as i16) << 8);
primary_buffer[i] = *device.rdram.mem.get(dram_addr + (i * 2) + 2).unwrap_or(&0) as i16
| ((*device.rdram.mem.get(dram_addr + (i * 2) + 3).unwrap_or(&0) as i16) << 8);
// Right channel
primary_buffer[i + 1] = device.rdram.mem[dram_addr + (i * 2)] as i16
| ((device.rdram.mem[dram_addr + (i * 2) + 1] as i16) << 8);
primary_buffer[i + 1] = *device.rdram.mem.get(dram_addr + (i * 2)).unwrap_or(&0) as i16
| ((*device.rdram.mem.get(dram_addr + (i * 2) + 1).unwrap_or(&0) as i16) << 8);
i += 2;
}
+148
View File
@@ -0,0 +1,148 @@
use crate::device;
use crate::ui;
use crate::ui::gui::AppWindow;
use crate::ui::gui::ErrorDialog;
use slint::ComponentHandle;
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct CheatData {
note: String,
pub data: Vec<String>,
pub options: Option<std::collections::BTreeMap<String, String>>,
}
pub type Cheats = std::collections::BTreeMap<String, std::collections::BTreeMap<String, CheatData>>;
pub fn cheats_window(app: &AppWindow) {
let weak = app.as_weak();
app.on_cheats_select_rom_clicked(move || {
let select_rom = rfd::AsyncFileDialog::new()
.set_title("Select ROM")
.pick_file();
let weak = weak.clone();
tokio::spawn(async move {
if let Some(file) = select_rom.await
&& let Some(rom_contents) = device::get_rom_contents(file.path())
{
let game_crc = ui::storage::get_game_crc(&rom_contents);
let game_name = ui::storage::get_game_name(&rom_contents);
weak.upgrade_in_event_loop(move |handle| {
handle.set_cheat_game_name(game_name.into());
})
.unwrap();
let cheats: Cheats =
serde_json::from_slice(include_bytes!("../../data/cheats.json")).unwrap();
if let Some(cheat) = cheats.get(&game_crc) {
let cheat = cheat.clone();
weak.upgrade_in_event_loop(move |handle| {
let cheat_settings = ui::config::Cheats::new();
let game_cheats = cheat_settings.cheats.get(&game_crc).cloned();
handle.set_cheat_game_crc(game_crc.into());
let cheats_vec = slint::VecModel::default();
for item in cheat.iter() {
let mut cheat_enabled = false;
if let Some(game_cheat) = game_cheats.as_ref()
&& game_cheat.contains_key(item.0)
{
cheat_enabled = true;
}
let options_vec = slint::VecModel::default();
if let Some(options) = item.1.options.as_ref() {
for option in options.iter() {
let mut option_enabled = false;
if let Some(game_cheat) = game_cheats.as_ref()
&& game_cheat.contains_key(item.0)
&& let Some(opt) = game_cheat.get(item.0)
&& let Some(opt) = opt
&& opt == option.0
{
option_enabled = true;
}
options_vec.push((option_enabled, option.0.into()));
}
}
let options = slint::ModelRc::from(std::rc::Rc::new(options_vec));
cheats_vec.push((
item.0.clone().into(),
cheat_enabled,
item.1.clone().note.into(),
options,
));
}
#[allow(clippy::type_complexity)]
let cheats_model: std::rc::Rc<
slint::VecModel<(
slint::SharedString,
bool,
slint::SharedString,
slint::ModelRc<(bool, slint::SharedString)>,
)>,
> = std::rc::Rc::new(cheats_vec);
handle.set_cheats(slint::ModelRc::from(cheats_model));
})
.unwrap();
} else {
clear_cheats(&weak, false);
}
} else {
clear_cheats(&weak, true);
weak.upgrade_in_event_loop(move |_handle| {
let message_dialog = ErrorDialog::new().unwrap();
let weak_dialog = message_dialog.as_weak();
message_dialog.on_close_clicked(move || {
weak_dialog.unwrap().window().hide().unwrap();
});
message_dialog.set_text("Could not read ROM".into());
message_dialog.show().unwrap();
})
.unwrap();
}
});
});
let weak = app.as_weak();
app.on_cheats_clear_clicked(move || {
let mut cheats = ui::config::Cheats::new();
cheats.cheats.clear();
clear_cheats(&weak, true);
});
app.on_cheat_toggled(move |game_crc, cheat_name, option, enabled| {
let mut cheats = ui::config::Cheats::new();
let mut game_cheats = cheats
.cheats
.remove(&game_crc.to_string())
.unwrap_or_default();
if enabled {
let cheat_option = if option.is_empty() {
None
} else {
Some(option.into())
};
game_cheats.insert(cheat_name.into(), cheat_option);
} else {
game_cheats.remove(&cheat_name.to_string());
}
cheats.cheats.insert(game_crc.into(), game_cheats);
});
}
fn clear_cheats(weak: &slint::Weak<AppWindow>, clear_name: bool) {
weak.upgrade_in_event_loop(move |handle| {
let cheats_vec = slint::VecModel::default();
#[allow(clippy::type_complexity)]
let cheats_model: std::rc::Rc<
slint::VecModel<(
slint::SharedString,
bool,
slint::SharedString,
slint::ModelRc<(bool, slint::SharedString)>,
)>,
> = std::rc::Rc::new(cheats_vec);
handle.set_cheats(slint::ModelRc::from(cheats_model));
if clear_name {
handle.set_cheat_game_name("".into());
}
})
.unwrap();
}
+45 -5
View File
@@ -33,7 +33,7 @@ pub struct InputProfile {
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Input {
pub input_profiles: std::collections::HashMap<String, InputProfile>,
pub input_profiles: std::collections::BTreeMap<String, InputProfile>,
pub input_profile_binding: [String; 4],
pub controller_assignment: [Option<String>; 4],
pub controller_enabled: [bool; 4],
@@ -53,6 +53,13 @@ pub struct Video {
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Emulation {
pub overclock: bool,
pub disable_expansion_pak: bool,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Cheats {
pub cheats:
std::collections::HashMap<String, std::collections::HashMap<String, Option<String>>>,
}
#[derive(serde::Serialize, serde::Deserialize)]
@@ -62,6 +69,36 @@ pub struct Config {
pub emulation: Emulation,
}
impl Drop for Cheats {
fn drop(&mut self) {
write_cheats(self);
}
}
fn write_cheats(cheats: &Cheats) {
let dirs = ui::get_dirs();
let file_path = dirs.config_dir.join("cheats.json");
let f = std::fs::File::create(file_path).unwrap();
serde_json::to_writer_pretty(f, &cheats).unwrap();
}
impl Cheats {
pub fn new() -> Cheats {
let dirs = ui::get_dirs();
let file_path = dirs.config_dir.join("cheats.json");
let cheats_file = std::fs::read(file_path);
if let Ok(cheats_file) = cheats_file {
let result = serde_json::from_slice(cheats_file.as_ref());
if let Ok(result) = result {
return result;
}
}
Cheats {
cheats: std::collections::HashMap::new(),
}
}
}
impl Drop for Config {
fn drop(&mut self) {
write_config(self);
@@ -80,13 +117,13 @@ impl Config {
let dirs = ui::get_dirs();
let file_path = dirs.config_dir.join("config.json");
let config_file = std::fs::read(file_path);
if config_file.is_ok() {
let result = serde_json::from_slice(config_file.unwrap().as_ref());
if let Ok(config_file) = config_file {
let result = serde_json::from_slice(config_file.as_ref());
if let Ok(result) = result {
return result;
}
}
let mut input_profiles = std::collections::HashMap::new();
let mut input_profiles = std::collections::BTreeMap::new();
input_profiles.insert("default".to_string(), ui::input::get_default_profile());
Config {
input: Input {
@@ -109,7 +146,10 @@ impl Config {
widescreen: false,
crt: false,
},
emulation: Emulation { overclock: false },
emulation: Emulation {
overclock: false,
disable_expansion_pak: false,
},
}
}
}
+433 -634
View File
File diff suppressed because it is too large Load Diff
+98
View File
@@ -0,0 +1,98 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Button, HorizontalBox, VerticalBox } from "std-widgets.slint";
import { Page } from "page.slint";
export global AboutData {
in property <string> version;
in property <bool> has_update;
}
export component NewVersionButton {
callback newversion_button_clicked;
HorizontalBox {
alignment: center;
Button {
text: @tr("New version available! Click here to download");
primary: true;
clicked => {
newversion_button_clicked();
}
}
}
}
export component About inherits Page {
callback wiki_button_clicked;
callback discord_button_clicked;
callback newversion_button_clicked;
callback patreon_button_clicked;
callback github_sponsors_button_clicked;
callback source_code_button_clicked;
title: @tr("About");
VerticalBox {
alignment: start;
HorizontalBox {
alignment: center;
Button {
text: @tr("Wiki");
clicked => {
wiki_button_clicked();
}
}
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Discord Server");
clicked => {
discord_button_clicked();
}
}
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Source Code");
clicked => {
source_code_button_clicked();
}
}
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Donate (via Patreon)");
clicked => {
patreon_button_clicked();
}
}
Button {
text: @tr("Donate (via GitHub)");
clicked => {
github_sponsors_button_clicked();
}
}
}
HorizontalBox {
alignment: center;
Text {
text: AboutData.version;
}
}
if AboutData.has_update:NewVersionButton {
newversion_button_clicked => {
newversion_button_clicked();
}
}
Rectangle { }
}
}
+149
View File
@@ -0,0 +1,149 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { StandardButton } from "std-widgets.slint";
import { LocalGame, ControllerConfig, Netplay, Cheats, Settings, About } from "pages.slint";
import { SideBar } from "side_bar.slint";
import { SettingsData } from "settings_page.slint";
import { AboutData } from "about_page.slint";
import { ControllerData } from "controller_page.slint";
import { State } from "localgame_page.slint";
import { CheatsData } from "cheat_page.slint";
export { InputProfileDialog } from "input_profile.slint";
export { VruDialog } from "vru_dialog.slint";
export { NetplayCreate } from "netplay_create.slint";
export { NetplayJoin } from "netplay_join.slint";
export { NetplayWait } from "netplay_wait.slint";
export component AppWindow inherits Window {
callback wiki_button_clicked;
callback discord_button_clicked;
callback newversion_button_clicked;
callback patreon_button_clicked;
callback github_sponsors_button_clicked;
callback source_code_button_clicked;
callback open_rom_button_clicked;
callback saves_folder_button_clicked;
callback input_profile_button_clicked;
callback create_session_button_clicked;
callback join_session_button_clicked;
callback netplay_discord_button_clicked;
callback netplay_feedback_button_clicked;
callback cheats_select_rom_clicked;
callback cheats_clear_clicked;
callback cheat_toggled(string, string, string, bool);
in property version <=> AboutData.version;
in property has_update <=> AboutData.has_update;
in-out property integer_scaling <=> SettingsData.integer_scaling;
in-out property fullscreen <=> SettingsData.fullscreen;
in-out property widescreen <=> SettingsData.widescreen;
in-out property apply_crt_shader <=> SettingsData.apply_crt_shader;
in-out property overclock_n64_cpu <=> SettingsData.overclock_n64_cpu;
in-out property disable_expansion_pak <=> SettingsData.disable_expansion_pak;
in-out property resolution <=> SettingsData.resolution;
in-out property emulate_vru <=> ControllerData.emulate_vru;
in-out property controller_enabled <=> ControllerData.controller_enabled;
in-out property transferpak <=> ControllerData.transferpak;
in-out property selected_profile_binding <=> ControllerData.selected_profile_binding;
in-out property input_profiles <=> ControllerData.input_profiles;
in-out property controller_names <=> ControllerData.controller_names;
in-out property selected_controller <=> ControllerData.selected_controller;
in-out property game_running <=> State.game_running;
in-out property blank_profiles <=> ControllerData.blank_profiles;
in-out property cheat_game_name <=> CheatsData.cheat_game_name;
in-out property cheat_game_crc <=> CheatsData.cheat_game_crc;
in-out property cheats <=> CheatsData.cheats;
preferred-width: 700px;
preferred-height: 500px;
title: @tr("Gopher64");
icon: @image-url("../../../data/gopher64.png");
HorizontalLayout {
side-bar := SideBar {
title: @tr("Gopher64");
model: [
@tr("Menu" => "Local Game"),
@tr("Menu" => "Controller Config"),
@tr("Menu" => "Netplay"),
@tr("Menu" => "Cheats"),
@tr("Menu" => "Settings"),
@tr("Menu" => "About")
];
}
if(side-bar.current-item == 0): LocalGame {
open_rom_button_clicked => {
open_rom_button_clicked();
}
saves_folder_button_clicked => {
saves_folder_button_clicked();
}
newversion_button_clicked => {
newversion_button_clicked();
}
}
if(side-bar.current-item == 1): ControllerConfig {
input_profile_button_clicked => {
input_profile_button_clicked();
}
}
if(side-bar.current-item == 2): Netplay {
create_session_button_clicked => {
create_session_button_clicked();
}
join_session_button_clicked => {
join_session_button_clicked();
}
netplay_discord_button_clicked => {
netplay_discord_button_clicked();
}
netplay_feedback_button_clicked => {
netplay_feedback_button_clicked();
}
}
if(side-bar.current-item == 3): Cheats {
cheats_select_rom_clicked => {
cheats_select_rom_clicked();
}
cheats_clear_clicked => {
cheats_clear_clicked();
}
cheat_toggled(game_crc, cheat_name, option, enabled) => {
cheat_toggled(game_crc,cheat_name,option,enabled);
}
}
if(side-bar.current-item == 4): Settings { }
if(side-bar.current-item == 5): About {
wiki_button_clicked => {
wiki_button_clicked();
}
discord_button_clicked => {
discord_button_clicked();
}
newversion_button_clicked => {
newversion_button_clicked();
}
patreon_button_clicked => {
patreon_button_clicked();
}
github_sponsors_button_clicked => {
github_sponsors_button_clicked();
}
source_code_button_clicked => {
source_code_button_clicked();
}
}
}
}
export component ErrorDialog inherits Dialog {
in-out property <string> text;
title: @tr("Error");
Text {
text: text;
}
StandardButton {
kind: close;
}
}
+89
View File
@@ -0,0 +1,89 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { ListView, HorizontalBox, VerticalBox, Button, GroupBox, CheckBox } from "std-widgets.slint";
import { Page } from "page.slint";
export global CheatsData {
in-out property <string> cheat_game_name;
in-out property <string> cheat_game_crc;
in-out property <[{cheat_name:string, notes:string, enabled:bool, options:[{option:string, enabled:bool}]}]> cheats;
}
export component Cheats inherits Page {
title: @tr("Cheats");
callback cheats_select_rom_clicked;
callback cheats_clear_clicked;
callback cheat_toggled(string, string, string, bool);
VerticalBox {
alignment: start;
HorizontalBox {
alignment: start;
if CheatsData.cheat_game_name == "": Button {
text: @tr("Select ROM");
clicked => {
cheats_select_rom_clicked();
}
}
if CheatsData.cheat_game_name != "": Button {
text: CheatsData.cheat_game_name;
clicked => {
cheats_select_rom_clicked();
}
}
Rectangle {
width: 20px;
}
Button {
text: @tr("Clear Cheats For All ROMs");
clicked => {
cheats_clear_clicked();
}
}
}
Rectangle { }
if CheatsData.cheats.length > 0:ListView {
preferred-height: 500px;
preferred-width: 520px;
for data in CheatsData.cheats: VerticalBox {
Text {
text: data.cheat_name;
font-size: 16px;
wrap: word-wrap;
}
if data.notes != "":Text {
text: data.notes;
wrap: word-wrap;
}
if data.options.length == 0: CheckBox {
text: @tr("Enabled");
checked: data.enabled;
toggled => {
data.enabled = self.checked;
cheat_toggled(CheatsData.cheat_game_crc,data.cheat_name,"",self.checked);
}
}
for item[index] in data.options: CheckBox {
text: item.option;
enabled: !data.enabled || (data.enabled && item.enabled);
checked: item.enabled;
toggled => {
item.enabled = self.checked;
data.enabled = self.checked;
cheat_toggled(CheatsData.cheat_game_crc,data.cheat_name,item.option,self.checked);
}
}
Rectangle { }
}
}
if CheatsData.cheats.length == 0 && CheatsData.cheat_game_name != "": Text {
text: @tr("No Cheats Available");
}
}
}
+120
View File
@@ -0,0 +1,120 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { CheckBox, ComboBox, Button, HorizontalBox, VerticalBox, GroupBox } from "std-widgets.slint";
import { Page } from "page.slint";
import { State } from "localgame_page.slint";
export global ControllerData {
in-out property <bool> emulate_vru;
in-out property <[bool]> controller_enabled;
in-out property <[bool]> transferpak;
in-out property <[int]> selected_profile_binding: [-1, -1, -1, -1];
in-out property <[string]> input_profiles;
in-out property <[string]> controller_names;
in-out property <[int]> selected_controller;
in-out property <bool> blank_profiles;
}
export component ControllerConfig inherits Page {
callback input_profile_button_clicked;
title: @tr("Controller Configuration");
VerticalBox {
alignment: start;
HorizontalBox {
alignment: start;
Button {
text: @tr("Configure Input Profile");
enabled: !State.game_running;
clicked => {
input_profile_button_clicked();
}
}
}
Rectangle { }
HorizontalBox {
GroupBox {
title: @tr("Port");
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: Text {
text: player + 1;
vertical-alignment: center;
}
}
}
GroupBox {
title: @tr("Enabled");
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: CheckBox {
checked: ControllerData.controller_enabled[player];
toggled => {
ControllerData.controller_enabled[player] = self.checked;
}
}
}
}
GroupBox {
title: @tr("Emulate VRU");
VerticalLayout {
alignment: space-between;
for valid in [false, false, false, true]: CheckBox {
enabled: valid;
checked: valid && ControllerData.emulate_vru;
toggled => {
ControllerData.emulate_vru = self.checked;
}
}
}
}
GroupBox {
title: @tr("Transfer Pak");
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: CheckBox {
checked: ControllerData.transferpak[player];
toggled => {
ControllerData.transferpak[player] = self.checked;
}
}
}
}
if !ControllerData.blank_profiles: GroupBox {
title: @tr("Profile");
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: ComboBox {
model: ControllerData.input_profiles;
current-index: ControllerData.selected_profile_binding[player];
selected => {
ControllerData.selected_profile_binding[player] = self.current-index;
}
}
}
}
GroupBox {
title: @tr("Controller");
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: ComboBox {
model: ControllerData.controller_names;
width: self.preferred-width;
current-index: ControllerData.selected_controller[player];
selected => {
ControllerData.selected_controller[player] = self.current-index;
}
}
}
}
}
}
}
-947
View File
@@ -1,947 +0,0 @@
use crate::device;
use crate::ui::gui::GopherEguiApp;
use crate::ui::{self, gui};
use eframe::egui;
use sha2::{Digest, Sha256};
const NETPLAY_VERSION: i32 = 17;
const EMU_NAME: &str = "gopher64";
#[derive(Default)]
pub struct GameInfo {
pub md5: String,
pub game_name: String,
pub rom_label: String,
pub rom_contents: Vec<u8>,
}
#[derive(PartialEq, Default)]
pub struct NetplayServer {
pub name: String,
pub ip: String,
}
#[derive(Default)]
pub struct GuiNetplay {
pub create: bool,
pub join: bool,
pub wait: bool,
pub session_name: String,
pub password: String,
pub player_name: String,
pub error: String,
pub create_rom_label: String,
pub join_rom_label: String,
pub send_chat: bool,
pub have_sessions: Option<NetplayServer>,
pub begin_game: bool,
pub chat_log: String,
pub chat_message: String,
pub selected_session: Option<NetplayRoom>,
pub pending_begin: bool,
pub peer_addr: Option<std::net::SocketAddr>,
pub motd: String,
pub sessions: Vec<NetplayRoom>,
pub rom_contents: Vec<u8>,
pub player_number: u8,
pub player_names: [String; 4],
pub server: NetplayServer,
pub socket_waiting: bool,
pub game_info: GameInfo,
pub servers: Vec<NetplayServer>,
pub waiting_session: Option<NetplayRoom>,
pub socket:
Option<tungstenite::WebSocket<tungstenite::stream::MaybeTlsStream<std::net::TcpStream>>>,
pub server_receiver:
Option<tokio::sync::mpsc::Receiver<std::collections::HashMap<String, String>>>,
pub game_info_receiver: Option<tokio::sync::mpsc::Receiver<GameInfo>>,
pub broadcast_socket: Option<std::net::UdpSocket>,
pub broadcast_timer: Option<std::time::Instant>,
}
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Default, Clone)]
pub struct NetplayRoom {
room_name: Option<String>,
password: Option<String>,
protected: Option<bool>,
#[serde(rename = "MD5")]
md5: Option<String>,
game_name: Option<String>,
pub port: Option<i32>,
features: Option<std::collections::HashMap<String, String>>,
buffer_target: Option<i32>,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct NetplayMessage {
#[serde(rename = "type")]
message_type: String,
player_name: Option<String>,
client_sha: Option<String>,
netplay_version: Option<i32>,
emulator: Option<String>,
room: Option<NetplayRoom>,
accept: Option<i32>,
message: Option<String>,
auth: Option<String>,
player_names: Option<[String; 4]>,
#[serde(rename = "authTime")]
auth_time: Option<String>,
rooms: Option<Vec<NetplayRoom>>,
}
fn get_servers(app: &mut GopherEguiApp, ctx: &egui::Context) {
if app.netplay.servers.is_empty() {
if app.netplay.broadcast_socket.is_none() {
app.netplay.broadcast_socket = Some(
std::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0))
.expect("couldn't bind to address"),
);
let socket = app.netplay.broadcast_socket.as_ref().unwrap();
socket
.set_broadcast(true)
.expect("set_broadcast call failed");
socket
.set_nonblocking(true)
.expect("could not set up socket");
let data: [u8; 1] = [1];
socket
.send_to(&data, (std::net::Ipv4Addr::BROADCAST, 45000))
.expect("couldn't send data");
app.netplay.broadcast_timer =
Some(std::time::Instant::now() + std::time::Duration::from_secs(5));
ctx.request_repaint();
}
if app.netplay.server_receiver.is_none() {
let (tx, rx) = tokio::sync::mpsc::channel(1);
app.netplay.server_receiver = Some(rx);
let gui_ctx = ctx.clone();
let task = reqwest::get("https://m64p.s3.amazonaws.com/servers.json");
tokio::spawn(async move {
let response = task.await;
if let Ok(response) = response {
tx.send(response.json().await.unwrap()).await.unwrap();
gui_ctx.request_repaint();
}
});
}
}
if app.netplay.broadcast_timer.is_some()
&& std::time::Instant::now() > app.netplay.broadcast_timer.unwrap()
{
app.netplay.broadcast_timer = None;
}
if app.netplay.broadcast_socket.is_some() && app.netplay.broadcast_timer.is_some() {
let mut buffer = [0; 1024];
let result = app
.netplay
.broadcast_socket
.as_ref()
.unwrap()
.recv_from(&mut buffer);
if result.is_ok() {
let (amt, _src) = result.unwrap();
let data: std::collections::HashMap<String, String> =
serde_json::from_slice(&buffer[..amt]).unwrap();
for server in data.iter() {
let (server_name, server_ip) = server;
app.netplay.servers.push(NetplayServer {
name: server_name.to_string(),
ip: server_ip.to_string(),
});
app.netplay.server = NetplayServer {
name: server.0.clone(),
ip: server.1.clone(),
};
}
app.netplay.broadcast_socket = None;
}
ctx.request_repaint();
}
if app.netplay.server_receiver.is_some() {
let result = app.netplay.server_receiver.as_mut().unwrap().try_recv();
if result.is_ok() {
for server in result.unwrap().iter() {
app.netplay.servers.push(NetplayServer {
name: server.0.clone(),
ip: server.1.clone(),
});
}
app.netplay.server_receiver = None;
if app.netplay.server.name.is_empty() {
let first_server = app.netplay.servers.first().unwrap();
app.netplay.server = NetplayServer {
name: first_server.name.clone(),
ip: first_server.ip.clone(),
};
}
}
ctx.request_repaint();
}
}
pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) {
egui::Window::new("Create Netplay Session").show(ctx, |ui| {
egui::Grid::new("netplay_create_grid").show(ui, |ui| {
let profile_name_label = ui.label("Session Name:");
let mut size = ui.spacing().interact_size;
size.x = 200.0;
ui.add_sized(size, |ui: &mut egui::Ui| {
ui.text_edit_singleline(&mut app.netplay.session_name)
.labelled_by(profile_name_label.id)
});
ui.end_row();
let password_label = ui.label("Password (Optional):");
ui.text_edit_singleline(&mut app.netplay.password)
.labelled_by(password_label.id);
ui.end_row();
ui.label("ROM");
if app.netplay.create_rom_label.is_empty() {
app.netplay.create_rom_label = "Open ROM".to_string();
}
if ui.button(&app.netplay.create_rom_label).clicked() {
let (tx, rx) = tokio::sync::mpsc::channel(1);
app.netplay.game_info_receiver = Some(rx);
let gui_ctx = ctx.clone();
app.netplay.create_rom_label = "Inspecting ROM".to_string();
let task = rfd::AsyncFileDialog::new()
.set_title("Select ROM")
.pick_file();
tokio::spawn(async move {
let file = task.await;
if let Some(file) = file {
parse_rom_file(file, tx).await;
} else {
tx.send(GameInfo {
md5: "".to_string(),
game_name: "".to_string(),
rom_label: "Open ROM".to_string(),
rom_contents: vec![],
})
.await
.unwrap();
}
gui_ctx.request_repaint();
});
}
ui.end_row();
let player_name_label = ui.label("Player Name:");
ui.text_edit_singleline(&mut app.netplay.player_name)
.labelled_by(player_name_label.id);
ui.end_row();
ui.label("Server:");
get_servers(app, ctx);
if app.netplay.game_info_receiver.is_some() {
let result = app.netplay.game_info_receiver.as_mut().unwrap().try_recv();
if result.is_ok() {
app.netplay.game_info_receiver = None;
let data = result.unwrap();
if !data.md5.is_empty() {
app.netplay.game_info = data;
app.netplay.rom_contents = app.netplay.game_info.rom_contents.clone();
app.netplay.create_rom_label = app.netplay.game_info.rom_label.clone();
} else {
app.netplay.create_rom_label = data.rom_label;
}
}
ctx.request_repaint();
}
egui::ComboBox::from_id_salt("server-combobox")
.selected_text(app.netplay.server.name.to_string())
.show_ui(ui, |ui| {
for server in app.netplay.servers.iter() {
ui.selectable_value(
&mut app.netplay.server,
NetplayServer {
name: server.name.clone(),
ip: server.ip.clone(),
},
server.name.clone(),
);
}
});
ui.end_row();
if app.netplay.socket_waiting {
let data = app.netplay.socket.as_mut().unwrap().read();
if data.is_ok() {
let message: NetplayMessage =
serde_json::from_slice(&data.unwrap().into_data()).unwrap();
if message.accept.unwrap() == 0 {
if message.message_type == "reply_create_room" {
app.netplay.create = false;
app.netplay.wait = true;
app.netplay.waiting_session = Some(message.room.unwrap());
}
} else {
app.netplay.error = message.message.unwrap();
}
app.netplay.socket_waiting = false;
}
ctx.request_repaint();
}
});
ui.horizontal(|ui| {
if ui.button("Create Session").clicked() {
if app.netplay.player_name.is_empty() {
app.netplay.error = "Player Name cannot be empty".to_string();
} else if app.netplay.session_name.is_empty() {
app.netplay.error = "Session Name cannot be empty".to_string();
} else if app.netplay.game_info.md5.is_empty() {
app.netplay.error = "ROM not loaded".to_string();
} else {
let now_utc = chrono::Utc::now().timestamp_millis().to_string();
let hasher = Sha256::new().chain_update(&now_utc).chain_update(EMU_NAME);
let mut game_name = app.netplay.game_info.game_name.to_string();
if game_name.is_empty() {
// If the ROM doesn't report a name, use the filename
game_name = app.netplay.create_rom_label.clone();
}
let mut features = std::collections::HashMap::new();
features.insert("overclock".to_string(), app.overclock.to_string());
let netplay_message = NetplayMessage {
message_type: "request_create_room".to_string(),
player_name: Some(app.netplay.player_name.clone()),
client_sha: Some(env!("GIT_HASH").to_string()),
netplay_version: Some(NETPLAY_VERSION),
emulator: Some(EMU_NAME.to_string()),
accept: None,
message: None,
rooms: None,
auth_time: Some(now_utc),
player_names: None,
auth: Some(format!("{:x}", hasher.finalize())),
room: Some(NetplayRoom {
room_name: Some(app.netplay.session_name.clone()),
password: Some(app.netplay.password.clone()),
game_name: Some(game_name),
md5: Some(app.netplay.game_info.md5.clone()),
protected: None,
port: None,
features: Some(features),
buffer_target: None,
}),
};
if let Ok((mut socket, _response)) =
tungstenite::connect(&app.netplay.server.ip)
{
socket
.send(tungstenite::Message::Binary(tungstenite::Bytes::from(
serde_json::to_vec(&netplay_message).unwrap(),
)))
.unwrap();
match socket.get_mut() {
tungstenite::stream::MaybeTlsStream::Plain(stream) => {
app.netplay.peer_addr = Some(stream.peer_addr().unwrap());
stream.set_nonblocking(true)
}
_ => unimplemented!(),
}
.expect("could not set socket to non-blocking");
app.netplay.socket_waiting = true;
app.netplay.socket = Some(socket);
}
}
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Close").clicked() {
if let Some(socket) = app.netplay.socket.as_mut() {
socket.close(None).unwrap();
loop {
match socket.read() {
Err(tungstenite::Error::ConnectionClosed) => break,
_ => continue,
};
}
}
app.netplay = Default::default();
};
})
});
});
}
fn get_sessions(app: &mut GopherEguiApp, ctx: &egui::Context) {
if app.netplay.have_sessions.is_some()
&& app.netplay.server != *app.netplay.have_sessions.as_ref().unwrap()
{
// User has changed the server
app.netplay.have_sessions = None;
app.netplay.socket = None;
}
if app.netplay.socket.is_none() {
if let Ok((mut sock, _response)) = tungstenite::connect(&app.netplay.server.ip) {
match sock.get_mut() {
tungstenite::stream::MaybeTlsStream::Plain(stream) => {
app.netplay.peer_addr = Some(stream.peer_addr().unwrap());
stream.set_nonblocking(true)
}
_ => unimplemented!(),
}
.expect("could not set socket to non-blocking");
app.netplay.socket = Some(sock);
}
}
if app.netplay.socket.is_some() {
let socket = app.netplay.socket.as_mut().unwrap();
if app.netplay.have_sessions.is_none() {
let now_utc = chrono::Utc::now().timestamp_millis().to_string();
let hasher = Sha256::new().chain_update(&now_utc).chain_update(EMU_NAME);
let request_rooms = NetplayMessage {
message_type: "request_get_rooms".to_string(),
player_name: None,
client_sha: None,
netplay_version: Some(NETPLAY_VERSION),
player_names: None,
emulator: Some(EMU_NAME.to_string()),
accept: None,
rooms: None,
message: None,
auth_time: Some(now_utc),
auth: Some(format!("{:x}", hasher.finalize())),
room: None,
};
socket
.send(tungstenite::Message::Binary(tungstenite::Bytes::from(
serde_json::to_vec(&request_rooms).unwrap(),
)))
.unwrap();
app.netplay.have_sessions = Some(NetplayServer {
name: app.netplay.server.name.clone(),
ip: app.netplay.server.ip.clone(),
});
app.netplay.socket_waiting = true;
ctx.request_repaint();
}
}
}
async fn parse_rom_file(file: rfd::FileHandle, tx: tokio::sync::mpsc::Sender<GameInfo>) {
if let Some(rom_contents) = device::get_rom_contents(file.path()) {
let hash = device::cart::rom::calculate_hash(&rom_contents);
let game_name = ui::storage::get_game_name(&rom_contents);
tx.send(GameInfo {
md5: hash,
game_name,
rom_label: file.file_name(),
rom_contents,
})
.await
.unwrap();
} else {
tx.send(GameInfo {
md5: "".to_string(),
game_name: "".to_string(),
rom_label: "Invalid ROM".to_string(),
rom_contents: vec![],
})
.await
.unwrap();
}
}
pub fn netplay_join(app: &mut GopherEguiApp, ctx: &egui::Context) {
if app.netplay.socket_waiting {
let socket = app.netplay.socket.as_mut().unwrap();
let data = socket.read();
if data.is_ok() {
let message: NetplayMessage =
serde_json::from_slice(&data.unwrap().into_data()).unwrap();
if message.accept.unwrap() == 0 {
if message.message_type == "reply_get_rooms" {
if message.rooms.is_some() {
app.netplay.sessions = message.rooms.unwrap();
} else {
app.netplay.sessions = vec![];
}
} else if message.message_type == "reply_join_room" {
app.netplay.join = false;
app.netplay.wait = true;
app.netplay.waiting_session = Some(message.room.unwrap());
}
app.netplay.socket_waiting = false;
} else {
app.netplay.error = message.message.unwrap();
app.netplay.join_rom_label = "Join Session (Open ROM)".to_string();
}
}
ctx.request_repaint();
}
if app.netplay.game_info_receiver.is_some() {
let result = app.netplay.game_info_receiver.as_mut().unwrap().try_recv();
if result.is_ok() {
app.netplay.game_info_receiver = None;
let data = result.unwrap();
if !data.md5.is_empty() {
app.netplay.game_info = data;
app.netplay.rom_contents = app.netplay.game_info.rom_contents.clone();
let netplay_message = NetplayMessage {
message_type: "request_join_room".to_string(),
player_name: Some(app.netplay.player_name.clone()),
client_sha: Some(env!("GIT_HASH").to_string()),
netplay_version: None,
emulator: None,
accept: None,
message: None,
rooms: None,
auth_time: None,
player_names: None,
auth: None,
room: Some(NetplayRoom {
room_name: None,
password: Some(app.netplay.password.clone()),
game_name: None,
md5: Some(app.netplay.game_info.md5.clone()),
protected: None,
port: app.netplay.selected_session.as_ref().unwrap().port,
features: None,
buffer_target: None,
}),
};
let socket = app.netplay.socket.as_mut().unwrap();
socket
.send(tungstenite::Message::Binary(tungstenite::Bytes::from(
serde_json::to_vec(&netplay_message).unwrap(),
)))
.unwrap();
app.netplay.socket_waiting = true;
} else {
app.netplay.error = data.rom_label;
app.netplay.join_rom_label = "Join Session (Open ROM)".to_string();
}
}
ctx.request_repaint();
}
egui::Window::new("Join Netplay Session").show(ctx, |ui| {
ui.horizontal(|ui| {
let mut size = ui.spacing().interact_size;
size.x = 100.0;
ui.add_sized(
size,
egui::TextEdit::singleline(&mut app.netplay.player_name).hint_text("Player name"),
);
get_servers(app, ctx);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Refresh").clicked() {
app.netplay.socket = None;
app.netplay.have_sessions = None;
app.netplay.selected_session = None;
ctx.request_repaint();
}
egui::ComboBox::from_id_salt("server-combobox")
.selected_text(app.netplay.server.name.to_string())
.show_ui(ui, |ui| {
for server in app.netplay.servers.iter() {
ui.selectable_value(
&mut app.netplay.server,
NetplayServer {
name: server.name.clone(),
ip: server.ip.clone(),
},
server.name.clone(),
);
}
});
});
});
if !app.netplay.server.name.is_empty() {
get_sessions(app, ctx);
}
ui.add_space(16.0);
if app.netplay.sessions.is_empty() {
ui.label("No sessions available");
} else {
egui::Grid::new("netplay_join_grid").show(ui, |ui| {
ui.label(egui::RichText::new("Session Name (click to select)").underline());
ui.label(egui::RichText::new("Game Name").underline());
ui.label(egui::RichText::new("Password Protected").underline());
ui.end_row();
for room in app.netplay.sessions.iter() {
ui.selectable_value(
&mut app.netplay.selected_session,
Some(room.clone()),
room.room_name.as_ref().unwrap(),
);
ui.label(room.game_name.as_ref().unwrap());
ui.label(room.protected.unwrap_or(false).to_string());
ui.end_row();
}
});
}
ui.add_space(16.0);
ui.horizontal(|ui| {
let mut size = ui.spacing().interact_size;
size.x = 130.0;
ui.add_sized(
size,
egui::TextEdit::singleline(&mut app.netplay.password)
.hint_text("Password (if required)"),
);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Close").clicked() {
if let Some(socket) = app.netplay.socket.as_mut() {
socket.close(None).unwrap();
loop {
match socket.read() {
Err(tungstenite::Error::ConnectionClosed) => break,
_ => continue,
};
}
}
app.netplay = Default::default();
};
if app.netplay.join_rom_label.is_empty() {
app.netplay.join_rom_label = "Join Session (Open ROM)".to_string();
}
if ui.button(&app.netplay.join_rom_label).clicked() {
if app.netplay.player_name.is_empty() {
app.netplay.error = "Player Name cannot be empty".to_string();
} else if app.netplay.selected_session.is_none() {
app.netplay.error = "No session selected".to_string();
} else if app
.netplay
.selected_session
.as_ref()
.unwrap()
.protected
.unwrap()
&& app.netplay.password.is_empty()
{
app.netplay.error = "Session requires a password".to_string();
} else {
let (tx, rx) = tokio::sync::mpsc::channel(1);
app.netplay.game_info_receiver = Some(rx);
let gui_ctx = ctx.clone();
app.netplay.join_rom_label = "Inspecting ROM".to_string();
let task = rfd::AsyncFileDialog::new()
.set_title("Select ROM")
.pick_file();
tokio::spawn(async move {
let file = task.await;
if let Some(file) = file {
parse_rom_file(file, tx).await;
} else {
tx.send(GameInfo {
md5: "".to_string(),
game_name: "".to_string(),
rom_label: "No ROM selected".to_string(),
rom_contents: vec![],
})
.await
.unwrap();
}
gui_ctx.request_repaint();
});
}
};
});
});
});
}
pub fn netplay_wait(app: &mut GopherEguiApp, ctx: &egui::Context) {
let overclock = app
.netplay
.waiting_session
.as_ref()
.unwrap()
.features
.as_ref()
.unwrap()
.get("overclock")
.unwrap();
let motd_message = NetplayMessage {
message_type: "request_motd".to_string(),
player_name: None,
client_sha: None,
netplay_version: None,
emulator: None,
accept: None,
rooms: None,
player_names: None,
message: None,
auth_time: None,
auth: None,
room: None,
};
let request_players = NetplayMessage {
message_type: "request_players".to_string(),
player_name: None,
client_sha: None,
netplay_version: None,
player_names: None,
emulator: None,
rooms: None,
accept: None,
message: None,
auth_time: None,
auth: None,
room: Some(NetplayRoom {
room_name: None,
password: None,
game_name: None,
md5: None,
protected: None,
port: app.netplay.waiting_session.as_ref().unwrap().port,
features: None,
buffer_target: None,
}),
};
if !app.netplay.socket_waiting {
let socket = app.netplay.socket.as_mut().unwrap();
socket
.send(tungstenite::Message::Binary(tungstenite::Bytes::from(
serde_json::to_vec(&motd_message).unwrap(),
)))
.unwrap();
socket
.send(tungstenite::Message::Binary(tungstenite::Bytes::from(
serde_json::to_vec(&request_players).unwrap(),
)))
.unwrap();
app.netplay.socket_waiting = true;
}
if app.netplay.begin_game {
let begin_game = NetplayMessage {
message_type: "request_begin_game".to_string(),
player_name: None,
client_sha: None,
netplay_version: None,
player_names: None,
rooms: None,
emulator: None,
accept: None,
message: None,
auth_time: None,
auth: None,
room: Some(NetplayRoom {
room_name: None,
password: None,
game_name: None,
md5: None,
protected: None,
port: app.netplay.waiting_session.as_ref().unwrap().port,
features: None,
buffer_target: None,
}),
};
let socket = app.netplay.socket.as_mut().unwrap();
socket
.send(tungstenite::Message::Binary(tungstenite::Bytes::from(
serde_json::to_vec(&begin_game).unwrap(),
)))
.unwrap();
app.netplay.begin_game = false;
}
if app.netplay.send_chat {
let send_chat = NetplayMessage {
message_type: "request_chat_message".to_string(),
player_name: Some(app.netplay.player_name.clone()),
client_sha: None,
netplay_version: None,
player_names: None,
rooms: None,
emulator: None,
accept: None,
message: Some(app.netplay.chat_message.clone()),
auth_time: None,
auth: None,
room: Some(NetplayRoom {
room_name: None,
password: None,
game_name: None,
md5: None,
protected: None,
port: app.netplay.waiting_session.as_ref().unwrap().port,
features: None,
buffer_target: None,
}),
};
app.netplay.chat_message.clear();
let socket = app.netplay.socket.as_mut().unwrap();
socket
.send(tungstenite::Message::Binary(tungstenite::Bytes::from(
serde_json::to_vec(&send_chat).unwrap(),
)))
.unwrap();
app.netplay.send_chat = false;
}
if app.netplay.socket_waiting {
let socket = app.netplay.socket.as_mut().unwrap();
let data = socket.read();
if data.is_ok() {
let message: NetplayMessage =
serde_json::from_slice(&data.unwrap().into_data()).unwrap();
if message.accept.unwrap() == 0 {
if message.message_type == "reply_motd" {
let re = regex::Regex::new(r"<[^>]*>").unwrap();
app.netplay.motd = re
.replace_all(message.message.unwrap().as_str(), "")
.into_owned();
} else if message.message_type == "reply_players" {
app.netplay.player_names = message.player_names.unwrap();
} else if message.message_type == "reply_chat_message" {
app.netplay.chat_log.push_str(&message.message.unwrap());
app.netplay.chat_log.push('\n');
} else if message.message_type == "reply_begin_game" {
let mut player = 0;
for (i, name) in app.netplay.player_names.iter().enumerate() {
if *name == app.netplay.player_name {
player = i;
break;
}
}
app.netplay.player_number = player as u8;
if message.accept.unwrap() == 0 {
socket.close(None).unwrap();
loop {
match socket.read() {
Err(tungstenite::Error::ConnectionClosed) => break,
_ => continue,
};
}
gui::open_rom(app, ctx, *overclock == "true");
app.netplay = Default::default();
return;
} else {
app.netplay.error = message.message.unwrap();
}
}
} else {
app.netplay.error = message.message.unwrap();
}
}
ctx.request_repaint();
}
egui::Window::new("Pending Netplay Session").show(ctx, |ui| {
egui::Grid::new("netplay_wait_grid_1").show(ui, |ui| {
ui.label("Session Name:");
let room_name = app
.netplay
.waiting_session
.as_ref()
.unwrap()
.room_name
.as_ref()
.unwrap();
let game_name = app
.netplay
.waiting_session
.as_ref()
.unwrap()
.game_name
.as_ref()
.unwrap();
ui.label(room_name);
ui.end_row();
ui.label("Game Name:");
ui.label(game_name);
ui.end_row();
for i in 0..4 {
ui.label(format!("Player {}:", i + 1));
ui.label(app.netplay.player_names[i].clone());
ui.end_row();
}
});
egui::ScrollArea::vertical()
.max_height(100.0)
.stick_to_bottom(true)
.show(ui, |ui| {
ui.add(
egui::TextEdit::multiline(&mut app.netplay.chat_log)
.interactive(false)
.desired_width(ui.available_width()),
);
});
ui.horizontal(|ui| {
let mut size = ui.spacing().interact_size;
size.x = 200.0;
ui.add_sized(
size,
egui::TextEdit::singleline(&mut app.netplay.chat_message)
.hint_text("Enter chat message here"),
);
if ui
.add_enabled(!app.netplay.send_chat, egui::Button::new("Send Message"))
.clicked()
&& !app.netplay.chat_message.is_empty()
{
app.netplay.send_chat = true;
}
});
ui.add_space(16.0);
ui.label(app.netplay.motd.clone());
ui.add_space(16.0);
ui.horizontal(|ui| {
let button_enabled = app.netplay.player_name == app.netplay.player_names[0]
&& !app.netplay.pending_begin;
if ui
.add_enabled(button_enabled, egui::Button::new("Start Session"))
.clicked()
{
app.netplay.begin_game = true;
app.netplay.pending_begin = true;
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Close").clicked() {
if let Some(socket) = app.netplay.socket.as_mut() {
socket.close(None).unwrap();
loop {
match socket.read() {
Err(tungstenite::Error::ConnectionClosed) => break,
_ => continue,
};
}
}
app.netplay = Default::default();
};
});
});
});
}
pub fn netplay_error(app: &mut GopherEguiApp, ctx: &egui::Context, error: String) {
egui::Window::new("Netplay Error").show(ctx, |ui| {
ui.label(error);
if ui.button("Close").clicked() {
app.netplay.error = "".to_string();
};
});
}
+45
View File
@@ -0,0 +1,45 @@
import { StandardButton, HorizontalBox, VerticalBox, Button, LineEdit, CheckBox } from "std-widgets.slint";
export global InputProfileData {
in-out property <bool> dinput;
in-out property <string> profile_name;
}
export component InputProfileDialog inherits Window {
title: @tr("Create Input Profile");
callback profile_creation_button_clicked;
in-out property dinput <=> InputProfileData.dinput;
in-out property profile_name <=> InputProfileData.profile_name;
VerticalBox {
HorizontalBox {
LineEdit {
placeholder-text: @tr("Enter profile name");
preferred-width: 300px;
edited(text) => {
InputProfileData.profile_name = text;
}
}
}
HorizontalBox {
alignment: center;
CheckBox {
text: @tr("Use DirectInput");
checked: InputProfileData.dinput;
toggled => {
InputProfileData.dinput = self.checked;
}
}
}
HorizontalBox {
Button {
text: @tr("Start Profile Creation Wizard");
enabled: InputProfileData.profile_name != "" && InputProfileData.profile_name != "default";
clicked => {
profile_creation_button_clicked();
}
}
}
}
}
+48
View File
@@ -0,0 +1,48 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Button, HorizontalBox, VerticalBox } from "std-widgets.slint";
import { Page } from "page.slint";
import { AboutData, NewVersionButton } from "about_page.slint";
export global State {
in-out property <bool> game_running;
}
export component LocalGame inherits Page {
callback open_rom_button_clicked;
callback saves_folder_button_clicked;
callback newversion_button_clicked;
title: @tr("Local Game");
VerticalBox {
alignment: start;
HorizontalBox {
alignment: center;
Button {
text: @tr("Open ROM");
enabled: !State.game_running;
clicked => {
open_rom_button_clicked();
}
}
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Open Saves Folder");
clicked => {
saves_folder_button_clicked();
}
}
}
if AboutData.has_update:NewVersionButton {
newversion_button_clicked => {
newversion_button_clicked();
}
}
Rectangle { }
}
}
+119
View File
@@ -0,0 +1,119 @@
import { LineEdit, HorizontalBox, VerticalBox, Button, ComboBox } from "std-widgets.slint";
import { NetplayData } from "netplay_page.slint";
export component NetplayCreate inherits Window {
title: @tr("Netplay: Create Session");
in-out property server_names <=> NetplayData.server_names;
in-out property server_urls <=> NetplayData.server_urls;
in-out property ping <=> NetplayData.ping;
in-out property game_name <=> NetplayData.game_name;
in-out property game_hash <=> NetplayData.game_hash;
in-out property game_cheats <=> NetplayData.game_cheats;
in-out property pending_session <=> NetplayData.pending_session;
in-out property rom_path <=> NetplayData.rom_path;
in-out property peer_addr <=> NetplayData.peer_addr;
callback get_ping(string);
callback select_rom;
callback create_session(string, string, string, string, string, string, string);
VerticalBox {
HorizontalBox {
VerticalBox {
Text {
text: @tr("Session Name:");
}
LineEdit {
placeholder_text: @tr("Enter session name");
edited(text) => {
NetplayData.session_name = text;
}
}
}
VerticalBox {
Text {
text: @tr("Password (Optional):");
}
LineEdit {
placeholder_text: @tr("Enter session password");
edited(text) => {
NetplayData.password = text;
}
}
}
}
VerticalBox {
Text {
text: @tr("Player Name:");
}
LineEdit {
placeholder_text: @tr("Enter player name");
edited(text) => {
NetplayData.player_name = text;
}
}
}
VerticalBox {
Text {
text: @tr("Server:");
}
HorizontalBox {
server := ComboBox {
model: NetplayData.server_names;
selected => {
if (self.current-index >= 0) {
get_ping(NetplayData.server_urls[self.current-index]);
}
}
}
Text {
vertical-alignment: center;
text: NetplayData.ping;
}
}
}
if NetplayData.game_name == "": Button {
text: @tr("Select ROM");
clicked => {
select_rom();
}
}
if NetplayData.game_name != "": Button {
text: NetplayData.game_name;
clicked => {
select_rom();
}
}
Rectangle { }
if NetplayData.game_cheats != "":Text {
text: @tr("This game has cheats enabled. Cheats will be synchronized with other players.");
}
Rectangle { }
Button {
text: @tr("Create Session");
enabled: NetplayData.session_name != "" && NetplayData.player_name != "" && NetplayData.game_name != "" && !NetplayData.pending_session;
clicked => {
NetplayData.pending_session = true;
create_session(
NetplayData.server_urls[server.current-index],
NetplayData.session_name,
NetplayData.player_name,
NetplayData.game_name,
NetplayData.game_hash,
NetplayData.game_cheats,
NetplayData.password);
}
}
}
}
+131
View File
@@ -0,0 +1,131 @@
import { VerticalBox, LineEdit, StandardTableView, HorizontalBox, ComboBox, Button } from "std-widgets.slint";
import { NetplayData } from "netplay_page.slint";
export component NetplayJoin inherits Window {
title: @tr("Netplay: Join Session");
in-out property server_names <=> NetplayData.server_names;
in-out property server_urls <=> NetplayData.server_urls;
in-out property ping <=> NetplayData.ping;
in-out property sessions <=> NetplayData.sessions;
in-out property current_session <=> NetplayData.current_session;
in-out property ports <=> NetplayData.ports;
in-out property game_name <=> NetplayData.game_name;
in-out property game_hash <=> NetplayData.game_hash;
in-out property game_cheats <=> NetplayData.game_cheats;
in-out property peer_addr <=> NetplayData.peer_addr;
in-out property pending_session <=> NetplayData.pending_session;
in-out property rom_path <=> NetplayData.rom_path;
callback get_ping(string);
callback refresh_session(string);
callback select_rom;
callback join_session(string, string, string, int);
VerticalBox {
HorizontalBox {
VerticalBox {
Text {
text: @tr("Player Name:");
}
LineEdit {
placeholder_text: @tr("Enter player name");
edited(text) => {
NetplayData.player_name = text;
}
}
}
VerticalBox {
Text {
text: @tr("Server:");
}
HorizontalBox {
server := ComboBox {
model: NetplayData.server_names;
selected => {
if (self.current-index >= 0) {
get_ping(NetplayData.server_urls[self.current-index]);
}
}
}
Text {
vertical-alignment: center;
text: NetplayData.ping;
}
}
}
}
if NetplayData.sessions.length > 0: Text {
text: @tr("Click on a session below to select it");
horizontal-alignment: center;
vertical-alignment: center;
}
if NetplayData.sessions.length == 0: Text {
text: @tr("No sessions available. Please refresh or try a different server.");
horizontal-alignment: center;
vertical-alignment: center;
}
StandardTableView {
columns: [
{ title: "Session Name" },
{ title: "Game Name" },
{ title: "Password" },
{ title: "Cheats" }
];
rows: NetplayData.sessions;
current-row: NetplayData.current_session;
current-row-changed(current-row) => {
NetplayData.current_session = current-row;
}
}
HorizontalBox {
Rectangle { }
Button {
text: @tr("Refresh Session List");
clicked => {
refresh_session(NetplayData.server_urls[server.current-index]);
}
}
}
HorizontalBox {
LineEdit {
placeholder-text: @tr("Password (if required)");
edited(text) => {
NetplayData.password = text;
}
}
if NetplayData.game_name == "": Button {
text: @tr("Select ROM");
clicked => {
select_rom();
}
}
if NetplayData.game_name != "": Button {
text: NetplayData.game_name;
clicked => {
select_rom();
}
}
Button {
text: @tr("Join Session");
enabled: NetplayData.player_name != "" && NetplayData.current_session >= 0 && NetplayData.game_name != "" && !NetplayData.pending_session;
clicked => {
NetplayData.pending_session = true;
join_session(
NetplayData.player_name,
NetplayData.game_hash,
NetplayData.password,
NetplayData.ports[NetplayData.current_session]);
}
}
}
}
}
+75
View File
@@ -0,0 +1,75 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { VerticalBox, HorizontalBox, Button } from "std-widgets.slint";
import { Page } from "page.slint";
export global NetplayData {
in-out property <[string]> server_names;
in-out property <[string]> server_urls;
in-out property <string> ping: "Ping: Unknown";
in-out property <string> player_name;
in-out property <string> game_name;
in-out property <string> game_hash;
in-out property <string> game_cheats;
in-out property <string> rom_path;
in-out property <string> password;
in-out property <string> peer_addr;
in-out property <bool> pending_session;
in-out property <string> session_name;
in-out property <[[StandardListViewItem]]> sessions;
in-out property <[int]> ports;
in-out property <int> current_session: -1;
}
export component Netplay inherits Page {
callback create_session_button_clicked;
callback join_session_button_clicked;
callback netplay_discord_button_clicked;
callback netplay_feedback_button_clicked;
title: @tr("Netplay");
VerticalBox {
alignment: start;
HorizontalBox {
alignment: center;
Button {
text: @tr("Create Session");
clicked => {
create_session_button_clicked();
}
}
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Join Session");
clicked => {
join_session_button_clicked();
}
}
}
Rectangle {
height: 20px;
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Netplay Discord Channel");
clicked => {
netplay_discord_button_clicked();
}
}
Button {
text: @tr("Provide Feedback");
clicked => {
netplay_feedback_button_clicked();
}
}
}
}
}
+103
View File
@@ -0,0 +1,103 @@
import { VerticalBox, HorizontalBox, TextEdit, ScrollView, LineEdit, Button, GridBox } from "std-widgets.slint";
import { NetplayData } from "netplay_page.slint";
export global NetplayWaitData {
in-out property <string> motd;
in-out property <[string]> players;
in-out property <int> port;
in-out property <bool> can_start;
in-out property <string> chat_text;
}
export component NetplayWait inherits Window {
title: @tr("Netplay: Waiting Room");
preferred-width: 400px;
preferred-height: 400px;
in-out property players <=> NetplayWaitData.players;
in-out property session_name <=> NetplayData.session_name;
in-out property game_name <=> NetplayData.game_name;
in-out property rom_path <=> NetplayData.rom_path;
in-out property motd <=> NetplayWaitData.motd;
in-out property port <=> NetplayWaitData.port;
in-out property can_start <=> NetplayWaitData.can_start;
in-out property chat_text <=> NetplayWaitData.chat_text;
callback send_chat_message(string);
callback begin_game;
VerticalBox {
Text {
text: @tr("Session Name: ") + NetplayData.session_name;
font-size: 15px;
horizontal-alignment: center;
}
Text {
text: @tr("Game Name: ") + NetplayData.game_name;
font-size: 15px;
horizontal-alignment: center;
}
GridBox {
Row {
Text {
text: "Player 1: " + players[0];
}
Text {
text: "Player 2: " + players[1];
}
}
Row {
Text {
text: "Player 3: " + players[2];
}
Text {
text: "Player 4: " + players[3];
}
}
}
TextEdit {
read-only: true;
text: NetplayWaitData.chat_text;
}
HorizontalBox {
chat_message := LineEdit {
placeholder_text: @tr("Enter chat message");
key-released(event) => {
if (event.text == Key.Return && chat_message.text != "") {
send_chat_message(chat_message.text);
chat_message.text = "";
}
accept
}
}
Button {
text: @tr("Send Message");
enabled: chat_message.text != "";
clicked => {
send_chat_message(chat_message.text);
chat_message.text = "";
}
}
}
Text {
text: NetplayWaitData.motd;
wrap: word-wrap;
}
Button {
text: @tr("Start Game");
enabled: NetplayWaitData.can_start;
clicked => {
NetplayWaitData.can_start = false;
begin_game();
}
}
}
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Switch, GridBox, ListView, ScrollView, HorizontalBox, VerticalBox, Palette } from "std-widgets.slint";
export component Page inherits VerticalBox {
in property <string> title: "title";
accessible-role: tab-panel;
accessible-label: root.title;
HorizontalBox {
Text {
font-size: 20px;
text <=> root.title;
}
// Spacer
Rectangle { }
}
@children
}
+6
View File
@@ -0,0 +1,6 @@
export { LocalGame } from "localgame_page.slint";
export { ControllerConfig } from "controller_page.slint";
export { Netplay } from "netplay_page.slint";
export { Cheats } from "cheat_page.slint";
export { Settings } from "settings_page.slint";
export { About } from "about_page.slint";
+106
View File
@@ -0,0 +1,106 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { ComboBox, HorizontalBox, VerticalBox, CheckBox } from "std-widgets.slint";
import { Page } from "page.slint";
export global SettingsData {
in-out property <bool> integer_scaling;
in-out property <bool> fullscreen;
in-out property <bool> widescreen;
in-out property <bool> apply_crt_shader;
in-out property <bool> overclock_n64_cpu;
in-out property <bool> disable_expansion_pak;
in-out property <int> resolution;
}
export component Settings inherits Page {
title: @tr("Settings");
VerticalBox {
alignment: start;
HorizontalBox {
alignment: start;
Text {
text: @tr("Resolution:");
vertical-alignment: center;
}
ComboBox {
model: ["1x", "2x", "4x"];
current-index: SettingsData.resolution;
selected => {
SettingsData.resolution = self.current-index;
}
}
}
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Integer Scaling");
checked: SettingsData.integer_scaling;
toggled => {
SettingsData.integer_scaling = self.checked;
}
}
}
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Fullscreen (Esc closes game)");
checked: SettingsData.fullscreen;
toggled => {
SettingsData.fullscreen = self.checked;
}
}
}
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Widescreen (strech)");
checked: SettingsData.widescreen;
toggled => {
SettingsData.widescreen = self.checked;
}
}
}
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Apply CRT shader");
checked: SettingsData.apply_crt_shader;
toggled => {
SettingsData.apply_crt_shader = self.checked;
}
}
}
Rectangle { }
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Overclock N64 CPU (may cause bugs)");
checked: SettingsData.overclock_n64_cpu;
toggled => {
SettingsData.overclock_n64_cpu = self.checked;
}
}
}
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Disable Expansion Pak");
checked: SettingsData.disable_expansion_pak;
toggled => {
SettingsData.disable_expansion_pak = self.checked;
}
}
}
}
}
+145
View File
@@ -0,0 +1,145 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { HorizontalBox, VerticalBox, Palette } from "std-widgets.slint";
component SideBarItem inherits Rectangle {
in property <int> tab-index;
in property <bool> selected;
in property <bool> has-focus;
in-out property <string> text <=> label.text;
callback clicked <=> touch.clicked;
min-height: l.preferred-height;
accessible-role: tab;
accessible-label: root.text;
accessible-item-index: root.tab-index;
accessible-item-selectable: true;
accessible-item-selected: root.selected;
accessible-action-default => {
self.clicked();
}
states [
pressed when touch.pressed: {
state.opacity: 0.8;
}
hover when touch.has-hover: {
state.opacity: 0.6;
}
selected when root.selected: {
state.opacity: 1;
}
focused when root.has-focus: {
state.opacity: 0.8;
}
]
state := Rectangle {
opacity: 0;
background: Palette.background;
animate opacity { duration: 150ms; }
}
l := HorizontalBox {
y: (parent.height - self.height) / 2;
spacing: 0px;
label := Text {
vertical-alignment: center;
accessible-role: none;
}
}
touch := TouchArea {
width: 100%;
height: 100%;
}
}
export component SideBar inherits Rectangle {
in property <[string]> model: [];
in property <string> title <=> label.text;
out property <int> current-item: 0;
out property <int> current-focused: fs.has-focus ? fs.focused-tab : -1; // The currently focused tab
width: 180px;
forward-focus: fs;
Rectangle {
background: Palette.background.darker(0.2);
}
VerticalBox {
padding-left: 0px;
padding-right: 0px;
alignment: start;
label := Text {
font-size: 16px;
horizontal-alignment: center;
}
navigation := VerticalLayout {
alignment: start;
vertical-stretch: 0;
accessible-role: tab-list;
accessible-delegate-focus: root.current-focused >= 0 ? root.current-focused : root.current-item;
accessible-label: root.title;
accessible-item-count: root.model.length;
fs := FocusScope {
key-pressed(event) => {
if (event.text == "\n") {
root.current-item = root.current-focused;
return accept;
}
if (event.text == Key.UpArrow) {
self.focused-tab = Math.max(self.focused-tab - 1, 0);
return accept;
}
if (event.text == Key.DownArrow) {
self.focused-tab = Math.min(self.focused-tab + 1, root.model.length - 1);
return accept;
}
return reject;
}
key-released(event) => {
if (event.text == " ") {
root.current-item = root.current-focused;
return accept;
}
return reject;
}
property <int> focused-tab: 0;
x: 0;
width: 0; // Do not react on clicks
}
for item[index] in root.model: SideBarItem {
clicked => {
root.current-item = index;
}
tab-index: index;
has-focus: index == root.current-focused;
text: item;
selected: index == root.current-item;
}
}
VerticalLayout {
bottom := VerticalBox {
padding-top: 0px;
padding-bottom: 0px;
@children
}
}
}
}
+24
View File
@@ -0,0 +1,24 @@
import { Button, VerticalBox, ScrollView } from "std-widgets.slint";
export global VruData {
in-out property <[string]> words;
}
export component VruDialog inherits Window {
title: @tr("What Would You Like To Say?");
preferred-width: 400px;
preferred-height: 400px;
callback vru_button_clicked(string);
in-out property words <=> VruData.words;
ScrollView {
VerticalBox {
padding-bottom: 40px;
for word[i] in words: Button {
text: word;
clicked => {
vru_button_clicked(self.text);
}
}
}
}
}
+30 -2
View File
@@ -334,6 +334,34 @@ fn change_paks(
pressed
}
pub fn get_controller_names(game_ui: &ui::Ui) -> Vec<String> {
let mut controllers: Vec<String> = vec![];
for joystick in game_ui.input.joysticks.iter() {
let name = unsafe {
std::ffi::CStr::from_ptr(sdl3_sys::joystick::SDL_GetJoystickNameForID(*joystick))
};
controllers.push(name.to_string_lossy().to_string());
}
controllers
}
pub fn get_controller_paths(game_ui: &ui::Ui) -> Vec<Option<String>> {
let mut controller_paths: Vec<Option<String>> = vec![];
for joystick in game_ui.input.joysticks.iter() {
let path = unsafe {
std::ffi::CStr::from_ptr(sdl3_sys::joystick::SDL_GetJoystickPathForID(*joystick))
.to_string_lossy()
.to_string()
};
controller_paths.push(Some(path));
}
controller_paths
}
pub fn get(ui: &ui::Ui, channel: usize) -> InputData {
let profile_name = ui.config.input.input_profile_binding[channel].clone();
let profile = ui.config.input.input_profiles.get(&profile_name).unwrap();
@@ -889,7 +917,7 @@ pub fn init(ui: &mut ui::Ui) {
if !profile.dinput {
let gamepad = unsafe { sdl3_sys::gamepad::SDL_OpenGamepad(joystick_id) };
if gamepad.is_null() {
println!("could not connect gamepad: {}", joystick_id)
println!("could not connect gamepad: {joystick_id}")
} else {
ui.input.controllers[i].game_controller = gamepad;
let properties =
@@ -908,7 +936,7 @@ pub fn init(ui: &mut ui::Ui) {
} else {
let joystick = unsafe { sdl3_sys::joystick::SDL_OpenJoystick(joystick_id) };
if joystick.is_null() {
println!("could not connect joystick: {}", joystick_id)
println!("could not connect joystick: {joystick_id}")
} else {
ui.input.controllers[i].joystick = joystick;
let properties =
+1049
View File
File diff suppressed because it is too large Load Diff
+31 -21
View File
@@ -57,10 +57,10 @@ fn get_save_type(rom: &[u8], game_id: &str) -> Vec<SaveTypes> {
1 => return vec![SaveTypes::Eeprom4k],
2 => return vec![SaveTypes::Eeprom16k],
3 => return vec![SaveTypes::Sram],
4 => panic!("Unsupported save type: {}", save_type),
4 => panic!("Unsupported save type: {save_type}"),
5 => return vec![SaveTypes::Flash],
6 => panic!("Unsupported save type: {}", save_type),
_ => panic!("Unknown save type: {}", save_type),
6 => panic!("Unsupported save type: {save_type}"),
_ => panic!("Unknown save type: {save_type}"),
}
}
match game_id {
@@ -120,15 +120,26 @@ fn get_save_type(rom: &[u8], game_id: &str) -> Vec<SaveTypes> {
}
}
pub fn get_game_crc(rom: &[u8]) -> String {
let crc1 = format!(
"{:08X}",
u32::from_be_bytes(rom[0x10..0x14].try_into().unwrap())
);
let crc2 = format!(
"{:08X}",
u32::from_be_bytes(rom[0x14..0x18].try_into().unwrap())
);
let country_code = format!("{:02X}", &rom[0x3E]);
format!("{crc1}-{crc2}-C:{country_code}")
}
pub fn get_game_name(rom: &[u8]) -> String {
let mut game_name = "".to_owned();
let header_value = std::str::from_utf8(&rom[0x20..0x34]);
if header_value.is_ok() {
if let Ok(header_value) = header_value {
let re = regex::Regex::new(r"[^a-zA-Z0-9_ -]").unwrap();
game_name = re
.replace_all(header_value.unwrap(), "")
.trim()
.replace('\0', "");
game_name = re.replace_all(header_value, "").trim().replace('\0', "");
}
game_name
}
@@ -197,29 +208,28 @@ pub fn init(ui: &mut ui::Ui, rom: &[u8]) {
pub fn load_saves(ui: &mut ui::Ui, netplay: &mut Option<netplay::Netplay>) {
if netplay.is_none() || netplay.as_ref().unwrap().player_number == 0 {
let eep = std::fs::read(&ui.storage.paths.eep_file_path);
if eep.is_ok() {
ui.storage.saves.eeprom.data = eep.unwrap();
if let Ok(eep) = eep {
ui.storage.saves.eeprom.data = eep;
}
let sra = std::fs::read(&ui.storage.paths.sra_file_path);
if sra.is_ok() {
ui.storage.saves.sram.data = sra.unwrap();
if let Ok(sra) = sra {
ui.storage.saves.sram.data = sra;
}
let fla = std::fs::read(&ui.storage.paths.fla_file_path);
if fla.is_ok() {
ui.storage.saves.flash.data = fla.unwrap();
if let Ok(fla) = fla {
ui.storage.saves.flash.data = fla;
}
let mempak = std::fs::read(&ui.storage.paths.pak_file_path);
if mempak.is_ok() {
ui.storage.saves.mempak.data = mempak.unwrap();
if let Ok(mempak) = mempak {
ui.storage.saves.mempak.data = mempak;
}
let sdcard = std::fs::read(&ui.storage.paths.sdcard_file_path);
if sdcard.is_ok() {
ui.storage.saves.sdcard.data = sdcard.unwrap();
if let Ok(sdcard) = sdcard {
ui.storage.saves.sdcard.data = sdcard;
}
let romsave = std::fs::read(&ui.storage.paths.romsave_file_path);
if romsave.is_ok() {
ui.storage.saves.romsave.data =
postcard::from_bytes(romsave.unwrap().as_ref()).unwrap();
if let Ok(romsave) = romsave {
ui.storage.saves.romsave.data = postcard::from_bytes(romsave.as_ref()).unwrap();
}
}
+7 -2
View File
@@ -114,7 +114,8 @@ pub fn load_state(device: &mut device::Device, rdp_state: *const u8) {
}
}
pub fn check_callback(device: &mut device::Device) {
pub fn check_callback(device: &mut device::Device) -> bool {
let mut speed_limiter_toggled = false;
let callback = unsafe { rdp_check_callback() };
device.cpu.running = callback.emu_running;
if device.netplay.is_none() {
@@ -123,7 +124,10 @@ pub fn check_callback(device: &mut device::Device) {
} else if callback.load_state {
device.load_state = true;
}
device.vi.enable_speed_limiter = callback.enable_speedlimiter;
if device.vi.enable_speed_limiter != callback.enable_speedlimiter {
speed_limiter_toggled = true;
device.vi.enable_speed_limiter = callback.enable_speedlimiter;
}
}
if callback.lower_volume {
@@ -131,6 +135,7 @@ pub fn check_callback(device: &mut device::Device) {
} else if callback.raise_volume {
ui::audio::raise_audio_volume(&mut device.ui);
}
speed_limiter_toggled
}
pub fn set_register(reg: u32, value: u32) {
+2 -6
View File
@@ -1,18 +1,14 @@
use eframe::egui;
use crate::ui;
pub fn prompt_for_match(
words: &[String],
window_notifier: &tokio::sync::mpsc::Sender<Vec<String>>,
window_notifier: &tokio::sync::mpsc::Sender<Option<Vec<String>>>,
word_index_receiver: &mut tokio::sync::mpsc::Receiver<String>,
gui_ctx: &egui::Context,
) -> u16 {
let mut dedup_words = words.to_owned();
dedup_words.sort();
dedup_words.dedup();
window_notifier.try_send(dedup_words).unwrap();
gui_ctx.request_repaint(); // this is so the window pops up right away
window_notifier.try_send(Some(dedup_words)).unwrap();
let mut result = word_index_receiver.try_recv();
while result.is_err() {
result = word_index_receiver.try_recv();