Compare commits

...

44 Commits

Author SHA1 Message Date
Logan McNaughton cea405ea30 bump to 1.1.0 (#489) 2025-07-11 21:14:18 +02:00
Logan McNaughton c13d354201 savestate speed improvements (#486)
* savestate speed improvements

* more
2025-07-10 18:13:16 +02:00
Logan McNaughton ff43f59f74 fix compile warning (#485)
* fix compile warning

* more
2025-07-10 14:38:28 +02:00
dependabot[bot] eb97b96daf Bump zip from 4.2.0 to 4.3.0 (#483)
Bumps [zip](https://github.com/zip-rs/zip2) from 4.2.0 to 4.3.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.2.0...v4.3.0)

---
updated-dependencies:
- dependency-name: zip
  dependency-version: 4.3.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-07-10 12:48:44 +02:00
Logan McNaughton 9bf43a16b4 Move SDL_PumpEvents (#482)
* Move SDL_PumpEvents

* more
2025-07-09 22:07:38 +02:00
Logan McNaughton b67247f935 Sync sdl window to prevent fullscreen issues (#481) 2025-07-09 19:55:24 +02:00
Logan McNaughton f10c591047 init rdp device 2025-07-09 10:45:12 +02:00
Logan McNaughton 82f1965f26 avoid syncing RDP too much (#479) 2025-07-09 10:31:20 +02:00
Logan McNaughton a5ddea353a print sdl errors (#478) 2025-07-09 10:19:44 +02:00
Logan McNaughton 6c977dbe5e Update README.md 2025-07-07 14:38:36 +02:00
Logan McNaughton 5dcfd72b18 bump to 1.0.21 (#477) 2025-07-06 20:07:31 +02:00
Logan McNaughton e578d6083d Don't use wgpu 2025-07-06 15:57:15 +02:00
Logan McNaughton e99a236070 use thin lto (#475) 2025-07-06 14:36:52 +02:00
Logan McNaughton e37633ecbd LTO2 (#474)
* LTO2

* more
2025-07-06 13:18:56 +02:00
Logan McNaughton d42e145b1f rename rdp build 2025-07-06 11:04:19 +02:00
Logan McNaughton 8ab31b0c28 Use Clang (#473) 2025-07-06 10:44:05 +02:00
Logan McNaughton 6d43191c56 cleanup release args 2025-07-06 07:28:34 +02:00
Logan McNaughton 75457c5f7a codegen-units = 16 2025-07-05 22:06:47 +02:00
Logan McNaughton 6d85358f2b re-order destruction of UI elements (#471)
* re-order destruction of UI elements

* more
2025-07-05 21:25:41 +02:00
Logan McNaughton ef4f93d6f0 bump to 1.0.20 (#470) 2025-07-05 18:14:18 +02:00
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
45 changed files with 1546 additions and 867 deletions
+10
View File
@@ -1,5 +1,15 @@
[target.'cfg(not(target_os = "windows"))']
rustflags = ["-C", "linker=clang", "-C", "link-arg=-fuse-ld=lld"]
[target.'cfg(target_os = "windows")']
rustflags = ["-C", "linker=lld-link", "-C", "link-arg=-fuse-ld=lld-link"]
[target.'cfg(target_arch = "x86_64")']
rustflags = ["-C", "target-cpu=x86-64-v3"]
[target.'cfg(target_arch = "aarch64")']
rustflags = ["-C", "target-cpu=cortex-a76"]
[env]
CC = "clang"
CXX = "clang++"
+14
View File
@@ -10,6 +10,7 @@ on:
env:
CARGO_TERM_COLOR: always
LLVM_VERSION: "20.1"
jobs:
build-linux:
@@ -44,6 +45,10 @@ jobs:
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev fcitx-libs-dev \
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
version: ${{ matrix.target }}-1.0
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v2
with:
version: "${{ env.LLVM_VERSION }}"
- name: Build
run: |
cargo build --release -v --target=${{ matrix.target }}
@@ -75,11 +80,19 @@ jobs:
with:
target: ${{ matrix.target }}
rustflags: ""
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v2
with:
version: "${{ env.LLVM_VERSION }}"
- name: Build
run: |
cargo build --release -v --target=${{ matrix.target }}
mkdir output
cp target/${{ matrix.target }}/release/gopher64.exe output/gopher64-${{ matrix.arch }}.exe
env:
CC: clang-cl
CXX: clang-cl
AR: llvm-lib
- name: Upload file
uses: actions/upload-artifact@v4
with:
@@ -87,6 +100,7 @@ jobs:
path: output/gopher64-${{ matrix.arch }}.exe
build-macos:
if: false
runs-on: macos-15
strategy:
fail-fast: false
+5
View File
@@ -11,6 +11,7 @@ permissions:
env:
CARGO_TERM_COLOR: always
LLVM_VERSION: "20.1"
jobs:
lint-linux:
@@ -34,6 +35,10 @@ jobs:
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev fcitx-libs-dev \
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
version: lint-1.0
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v2
with:
version: "${{ env.LLVM_VERSION }}"
- name: Rustfmt Check
uses: actions-rust-lang/rustfmt@v1
- name: Lint
Generated
+301 -579
View File
File diff suppressed because it is too large Load Diff
+7 -8
View File
@@ -1,6 +1,6 @@
[package]
name = "gopher64"
version = "1.0.18"
version = "1.1.0"
edition = "2024"
rust-version = "1.88"
@@ -8,27 +8,27 @@ rust-version = "1.88"
[dependencies]
dirs = "6.0"
zip = "4.2"
zip = "4.3"
governor = "0.10"
sevenz-rust2 = { version = "0.15", 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"
slint = { version = "1.12", default-features = false, features = ["compat-1-2", "std", "backend-winit", "renderer-femtovg-wgpu"] }
slint = { version = "1.12", default-features = false, features = ["compat-1-2", "std", "backend-winit", "renderer-femtovg"] }
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"
tokio-tungstenite = "0.27"
futures = "0.3"
socket2 = "0.5"
socket2 = { version = "0.6", features = ["all"] }
rand_chacha = "0.9"
fatfs = "0.3"
semver = "1.0"
@@ -43,6 +43,5 @@ slint-build = "1.12"
cc = { version = "1.2", features = ["parallel"] }
[profile.release]
codegen-units = 1
strip = true
panic = "abort"
lto = "thin"
+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://flathub.org/apps/io.github.gopher64.gopher64
## wiki
+31 -34
View File
@@ -6,11 +6,15 @@ fn main() {
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
let mut volk_build = cc::Build::new();
volk_build
.include("parallel-rdp/parallel-rdp-standalone/vulkan-headers/include")
.file("parallel-rdp/parallel-rdp-standalone/volk/volk.c");
let mut rdp_build = cc::Build::new();
rdp_build
.cpp(true)
.warnings(false)
.std("c++17")
.flag("-Wno-unused-parameter")
.flag("-Wno-missing-field-initializers")
.file("parallel-rdp/parallel-rdp-standalone/parallel-rdp/command_ring.cpp")
.file("parallel-rdp/parallel-rdp-standalone/parallel-rdp/rdp_device.cpp")
.file("parallel-rdp/parallel-rdp-standalone/parallel-rdp/rdp_dump_write.cpp")
@@ -47,7 +51,6 @@ fn main() {
.file("parallel-rdp/parallel-rdp-standalone/util/timeline_trace_file.cpp")
.file("parallel-rdp/parallel-rdp-standalone/util/environment.cpp")
.file("parallel-rdp/parallel-rdp-standalone/util/thread_name.cpp")
.file("parallel-rdp/parallel-rdp-standalone/volk/volk.c")
.file("parallel-rdp/interface.cpp")
.file("parallel-rdp/wsi_platform.cpp")
.include("parallel-rdp/parallel-rdp-standalone/parallel-rdp")
@@ -62,43 +65,35 @@ fn main() {
let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap();
let profile = std::env::var("PROFILE").unwrap();
let opt_flag = if arch == "x86_64" {
"-march=x86-64-v3"
} else if arch == "aarch64" {
"-march=armv8.2-a"
} else {
panic!("unknown arch")
};
volk_build.flag(opt_flag);
rdp_build.flag(opt_flag);
simd_build.flag(opt_flag);
if os == "windows" {
if arch == "x86_64" {
if env == "msvc" {
build.flag("/arch:AVX2");
} else if env == "gnu" {
build.flag("-march=x86-64-v3");
} else {
panic!("unknown env")
}
} else if arch == "aarch64" {
panic!("unsupported platform")
} else {
panic!("unknown arch")
}
build.flag("-DVK_USE_PLATFORM_WIN32_KHR");
volk_build.flag("-DVK_USE_PLATFORM_WIN32_KHR");
rdp_build.flag("-DVK_USE_PLATFORM_WIN32_KHR");
winresource::WindowsResource::new()
.set_icon("data/icon.ico")
.compile()
.unwrap();
} else if os == "linux" || os == "macos" {
if arch == "x86_64" {
build.flag("-march=x86-64-v3");
} 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")
}
} else {
panic!("unknown OS")
}
build.compile("parallel-rdp");
if profile == "release" {
volk_build.flag("-flto=thin");
rdp_build.flag("-flto=thin");
simd_build.flag("-flto=thin");
}
volk_build.compile("volk");
rdp_build.compile("parallel-rdp");
let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
@@ -169,6 +164,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");
+1
View File
File diff suppressed because one or more lines are too long
+11 -1
View File
@@ -58,6 +58,8 @@ typedef struct
int cmd_cur;
int cmd_ptr;
uint32_t region;
uint64_t vi_counter;
uint64_t last_sync;
} RDP_DEVICE;
static SDL_Window *window;
@@ -225,7 +227,10 @@ void rdp_new_processor(GFX_INFO _gfx_info)
void rdp_init(void *_window, GFX_INFO _gfx_info)
{
memset(&rdp_device, 0, sizeof(RDP_DEVICE));
window = (SDL_Window *)_window;
SDL_SyncWindow(window);
bool result = SDL_AddEventWatch(sdl_event_filter, nullptr);
if (!result)
{
@@ -435,6 +440,7 @@ void rdp_update_screen()
render_frame(device);
wsi->end_frame();
wsi->begin_frame();
rdp_device.vi_counter += 1;
}
CALL_BACK rdp_check_callback()
@@ -542,7 +548,11 @@ uint64_t rdp_process_commands()
break;
}
case RDP::Op::SyncFull:
processor->wait_for_timeline(processor->signal_timeline());
if (rdp_device.last_sync != rdp_device.vi_counter)
{
processor->wait_for_timeline(processor->signal_timeline());
rdp_device.last_sync = rdp_device.vi_counter;
}
interrupt_timer = rdp_device.region;
if (interrupt_timer == 0)
interrupt_timer = 5000;
-1
View File
@@ -56,7 +56,6 @@ bool SDL_WSIPlatform::alive(Vulkan::WSI &wsi)
void SDL_WSIPlatform::poll_input()
{
SDL_PumpEvents();
}
void SDL_WSIPlatform::poll_input_async(Granite::InputTrackerHandler *handler)
+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;
}
+22 -11
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);
@@ -55,8 +58,8 @@ pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, fullscreen: bool, ov
// rdram pointer is shared with parallel-rdp
rdram::init(device);
ui::audio::init(&mut device.ui, device.ai.freq);
ui::video::init(device);
ui::audio::init(&mut device.ui, device.ai.freq);
ui::input::init(&mut device.ui);
mi::init(device);
@@ -74,12 +77,14 @@ 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);
ui::video::close(&device.ui);
ui::audio::close(&mut device.ui);
ui::storage::write_saves(device);
ui::input::close(&mut device.ui);
ui::audio::close(&mut device.ui);
ui::video::close(&device.ui);
}
fn set_rng() -> rand_chacha::ChaCha8Rng {
@@ -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 {
@@ -497,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;
+12 -4
View File
@@ -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;
}
+32 -12
View File
@@ -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 */
+12
View File
@@ -2726,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"),
),
]);
}
+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)
}
+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 {
+23 -14
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;
@@ -134,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();
@@ -175,7 +181,7 @@ 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 let Err(outcome) = result {
let dur = outcome.wait_time_from(governor::clock::DefaultClock::default().now());
@@ -194,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);
+26 -11
View File
@@ -3,6 +3,7 @@
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
mod cheats;
mod device;
mod netplay;
mod savestates;
@@ -106,6 +107,14 @@ async fn main() {
}
return;
}
if args.configure_input_profile.is_some() {
ui::input::configure_input_profile(
&mut ui,
args.configure_input_profile.unwrap(),
args.use_dinput,
);
return;
}
if args.assign_controller.is_some() {
if args.port.is_none() {
println!("Must specify port number");
@@ -116,7 +125,6 @@ async fn main() {
args.assign_controller.unwrap(),
args.port.unwrap(),
);
return;
}
if args.bind_input_profile.is_some() {
if args.port.is_none() {
@@ -128,15 +136,6 @@ async fn main() {
args.bind_input_profile.unwrap(),
args.port.unwrap(),
);
return;
}
if args.configure_input_profile.is_some() {
ui::input::configure_input_profile(
&mut ui,
args.configure_input_profile.unwrap(),
args.use_dinput,
);
return;
}
} else if args.game.is_some() {
let file_path = std::path::Path::new(args.game.as_ref().unwrap());
@@ -147,7 +146,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();
+2 -2
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());
@@ -252,7 +252,7 @@ pub fn init(peer_addr: std::net::SocketAddr, player_number: u8) -> Netplay {
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))
+3 -2
View File
@@ -120,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() {
@@ -191,8 +192,8 @@ pub fn load_savestate(device: &mut device::Device) {
}
}
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);
ui::video::load_state(device, rdp_state.as_ptr());
println!(
"Savestate loaded from {}",
+4
View File
@@ -1,4 +1,5 @@
pub mod audio;
pub mod cheats;
pub mod config;
pub mod gui;
pub mod input;
@@ -13,6 +14,7 @@ pub struct Dirs {
}
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,
@@ -178,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();
}
+43 -3
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);
@@ -86,7 +123,7 @@ impl Config {
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,
},
}
}
}
+37 -44
View File
@@ -25,6 +25,14 @@ pub struct VruChannel {
pub vru_word_receiver: Option<tokio::sync::mpsc::Receiver<String>>,
}
#[derive(Clone)]
pub struct GameSettings {
pub fullscreen: bool,
pub overclock: bool,
pub disable_expansion_pak: bool,
pub cheats: std::collections::HashMap<String, Option<String>>,
}
fn check_latest_version(weak: slint::Weak<AppWindow>) {
let client = reqwest::Client::builder()
.user_agent(env!("CARGO_PKG_NAME"))
@@ -52,43 +60,6 @@ fn check_latest_version(weak: slint::Weak<AppWindow>) {
});
}
fn netplay_window(app: &AppWindow, controller_paths: &[Option<String>]) {
let weak_create = app.as_weak();
let weak_app = app.as_weak();
let controller_paths_create = controller_paths.to_owned();
app.on_create_session_button_clicked(move || {
let controller_paths = controller_paths_create.clone();
let weak_app = weak_app.clone();
weak_create
.upgrade_in_event_loop(move |handle| {
let create_window = NetplayCreate::new().unwrap();
save_settings(&handle, &controller_paths);
ui::netplay::setup_create_window(
&create_window,
handle.get_overclock_n64_cpu(),
handle.get_fullscreen(),
weak_app,
);
})
.unwrap();
});
let weak_join = app.as_weak();
let weak_app = app.as_weak();
let controller_paths_join = controller_paths.to_owned();
app.on_join_session_button_clicked(move || {
let controller_paths = controller_paths_join.clone();
let weak_app = weak_app.clone();
weak_join
.upgrade_in_event_loop(move |handle| {
let join_window = NetplayJoin::new().unwrap();
save_settings(&handle, &controller_paths);
ui::netplay::setup_join_window(&join_window, handle.get_fullscreen(), weak_app);
})
.unwrap();
});
}
fn local_game_window(app: &AppWindow, controller_paths: &[Option<String>]) {
let dirs = ui::get_dirs();
let weak = app.as_weak();
@@ -122,6 +93,7 @@ fn settings_window(app: &AppWindow, config: &ui::config::Config) {
app.set_widescreen(config.video.widescreen);
app.set_apply_crt_shader(config.video.crt);
app.set_overclock_n64_cpu(config.emulation.overclock);
app.set_disable_expansion_pak(config.emulation.disable_expansion_pak);
let combobox_value = match config.video.upscale {
1 => 0,
2 => 1,
@@ -244,13 +216,14 @@ fn controller_window(
});
}
fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
let mut config = ui::config::Config::new();
config.video.integer_scaling = app.get_integer_scaling();
config.video.fullscreen = app.get_fullscreen();
config.video.widescreen = app.get_widescreen();
config.video.crt = app.get_apply_crt_shader();
config.emulation.overclock = app.get_overclock_n64_cpu();
config.emulation.disable_expansion_pak = app.get_disable_expansion_pak();
let upscale_values = [1, 2, 4];
config.video.upscale = upscale_values[app.get_resolution() as usize];
@@ -282,6 +255,15 @@ fn about_window(app: &AppWindow) {
app.on_discord_button_clicked(move || {
open::that_detached("https://discord.gg/9RGXq8W8JQ").unwrap();
});
app.on_patreon_button_clicked(move || {
open::that_detached("https://patreon.com/loganmc10").unwrap();
});
app.on_github_sponsors_button_clicked(move || {
open::that_detached("https://github.com/sponsors/loganmc10").unwrap();
});
app.on_source_code_button_clicked(move || {
open::that_detached("https://github.com/gopher64/gopher64").unwrap();
});
app.on_newversion_button_clicked(move || {
open::that_detached("https://github.com/gopher64/gopher64/releases/latest").unwrap();
});
@@ -303,7 +285,8 @@ pub fn app_window() {
controller_window(&app, &game_ui.config, &controller_names, &controller_paths);
}
local_game_window(&app, &controller_paths);
netplay_window(&app, &controller_paths);
ui::netplay::netplay_window(&app, &controller_paths);
ui::cheats::cheats_window(&app);
app.run().unwrap();
save_settings(&app, &controller_paths);
}
@@ -355,8 +338,7 @@ fn setup_vru_word_watcher(
pub fn run_rom(
gb_paths: GbPaths,
file_path: std::path::PathBuf,
fullscreen: bool,
overclock: bool,
mut game_settings: GameSettings,
vru_channel: VruChannel,
netplay: Option<NetplayDevice>,
weak: slint::Weak<AppWindow>,
@@ -389,8 +371,14 @@ pub fn run_rom(
netplay_device.peer_addr,
netplay_device.player_number,
));
} else {
game_settings.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, fullscreen, overclock);
device::run_game(&mut device, rom_contents, game_settings);
if device.netplay.is_some() {
netplay::close(&mut device);
}
@@ -444,6 +432,7 @@ fn open_rom(app: &AppWindow) {
let fullscreen = app.get_fullscreen();
let overclock = app.get_overclock_n64_cpu();
let emulate_vru = app.get_emulate_vru();
let disable_expansion_pak = app.get_disable_expansion_pak();
if emulate_vru {
setup_vru_word_watcher(app.as_weak(), vru_word_notifier, vru_window_receiver);
@@ -470,8 +459,12 @@ fn open_rom(app: &AppWindow) {
ram: gb_ram_path,
},
file.path().to_path_buf(),
fullscreen,
overclock,
GameSettings {
fullscreen,
overclock,
disable_expansion_pak,
cheats: std::collections::HashMap::new(), // will be filled in later
},
if emulate_vru {
VruChannel {
vru_window_notifier: Some(vru_window_notifier),
+48 -8
View File
@@ -9,10 +9,27 @@ export global AboutData {
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 {
@@ -37,6 +54,33 @@ export component About inherits Page {
}
}
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 {
@@ -44,15 +88,11 @@ export component About inherits Page {
}
}
if AboutData.has_update:HorizontalBox {
alignment: center;
Button {
text: @tr("New version available! Click to download");
primary: true;
clicked => {
newversion_button_clicked();
}
if AboutData.has_update:NewVersionButton {
newversion_button_clicked => {
newversion_button_clicked();
}
}
Rectangle { }
}
}
+59 -4
View File
@@ -1,28 +1,37 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { LocalGame, ControllerConfig, Netplay, Settings, About } from "pages.slint";
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 { NetplayDialog } from "netplay_page.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;
@@ -30,6 +39,7 @@ export component AppWindow inherits Window {
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;
@@ -40,6 +50,9 @@ export component AppWindow inherits Window {
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");
@@ -52,6 +65,7 @@ export component AppWindow inherits Window {
@tr("Menu" => "Local Game"),
@tr("Menu" => "Controller Config"),
@tr("Menu" => "Netplay"),
@tr("Menu" => "Cheats"),
@tr("Menu" => "Settings"),
@tr("Menu" => "About")
];
@@ -64,6 +78,9 @@ export component AppWindow inherits Window {
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 => {
@@ -77,9 +94,26 @@ export component AppWindow inherits Window {
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): Settings { }
if(side-bar.current-item == 4): About {
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();
}
@@ -89,6 +123,27 @@ export component AppWindow inherits Window {
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");
}
}
}
+9
View File
@@ -3,6 +3,7 @@
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;
@@ -11,6 +12,7 @@ export global State {
export component LocalGame inherits Page {
callback open_rom_button_clicked;
callback saves_folder_button_clicked;
callback newversion_button_clicked;
title: @tr("Local Game");
VerticalBox {
@@ -35,5 +37,12 @@ export component LocalGame inherits Page {
}
}
}
if AboutData.has_update:NewVersionButton {
newversion_button_clicked => {
newversion_button_clicked();
}
}
Rectangle { }
}
}
+6 -1
View File
@@ -8,12 +8,13 @@ export component NetplayCreate inherits Window {
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);
callback create_session(string, string, string, string, string, string, string);
VerticalBox {
HorizontalBox {
@@ -94,6 +95,9 @@ export component NetplayCreate inherits Window {
Rectangle { }
if NetplayData.game_cheats != "":Text {
text: @tr("This game has cheats enabled. Cheats will be synchronized with other players.");
}
Rectangle { }
Button {
@@ -107,6 +111,7 @@ export component NetplayCreate inherits Window {
NetplayData.player_name,
NetplayData.game_name,
NetplayData.game_hash,
NetplayData.game_cheats,
NetplayData.password);
}
}
+3 -1
View File
@@ -11,6 +11,7 @@ export component NetplayJoin inherits Window {
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;
@@ -71,7 +72,8 @@ export component NetplayJoin inherits Window {
columns: [
{ title: "Session Name" },
{ title: "Game Name" },
{ title: "Password Protected" },
{ title: "Password" },
{ title: "Cheats" }
];
rows: NetplayData.sessions;
current-row: NetplayData.current_session;
+25 -13
View File
@@ -1,7 +1,7 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { VerticalBox, HorizontalBox, Button, StandardButton } from "std-widgets.slint";
import { VerticalBox, HorizontalBox, Button } from "std-widgets.slint";
import { Page } from "page.slint";
export global NetplayData {
@@ -11,6 +11,7 @@ export global NetplayData {
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;
@@ -21,21 +22,11 @@ export global NetplayData {
in-out property <int> current_session: -1;
}
export component NetplayDialog inherits Dialog {
in-out property <string> text;
title: @tr("Netplay Error");
Text {
text: text;
}
StandardButton {
kind: close;
}
}
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 {
@@ -59,5 +50,26 @@ export component Netplay inherits Page {
}
}
}
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();
}
}
}
}
}
+1
View File
@@ -1,5 +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";
+12
View File
@@ -10,6 +10,7 @@ export global SettingsData {
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;
}
@@ -90,5 +91,16 @@ export component Settings inherits Page {
}
}
}
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Disable Expansion Pak");
checked: SettingsData.disable_expansion_pak;
toggled => {
SettingsData.disable_expansion_pak = self.checked;
}
}
}
}
}
+2
View File
@@ -363,6 +363,8 @@ pub fn get_controller_paths(game_ui: &ui::Ui) -> Vec<Option<String>> {
}
pub fn get(ui: &ui::Ui, channel: usize) -> InputData {
unsafe { sdl3_sys::events::SDL_PumpEvents() };
let profile_name = ui.config.input.input_profile_binding[channel].clone();
let profile = ui.config.input.input_profiles.get(&profile_name).unwrap();
let mut keys = 0;
+164 -26
View File
@@ -1,11 +1,8 @@
#![allow(clippy::too_many_arguments)]
use crate::device;
use crate::ui;
use crate::ui::gui::AppWindow;
use crate::ui::gui::{
GbPaths, NetplayCreate, NetplayDevice, NetplayDialog, NetplayJoin, NetplayWait, VruChannel,
run_rom,
AppWindow, ErrorDialog, GameSettings, GbPaths, NetplayCreate, NetplayDevice, NetplayJoin,
NetplayWait, VruChannel, run_rom, save_settings,
};
use futures::{SinkExt, StreamExt};
use sha2::{Digest, Sha256};
@@ -53,6 +50,7 @@ trait NetplayPages {
fn set_ping(&self, ping: slint::SharedString);
fn set_game_name(&self, game_name: slint::SharedString);
fn set_game_hash(&self, game_hash: slint::SharedString);
fn set_game_cheats(&self, game_cheats: slint::SharedString);
fn set_rom_path(&self, rom_path: slint::SharedString);
fn set_peer_addr(&self, peer_addr: slint::SharedString);
fn refresh_sessions(&self, _server: slint::SharedString) {
@@ -76,6 +74,9 @@ impl NetplayPages for NetplayCreate {
fn set_game_hash(&self, game_hash: slint::SharedString) {
self.set_game_hash(game_hash);
}
fn set_game_cheats(&self, game_cheats: slint::SharedString) {
self.set_game_cheats(game_cheats);
}
fn set_rom_path(&self, rom_path: slint::SharedString) {
self.set_rom_path(rom_path);
}
@@ -103,6 +104,9 @@ impl NetplayPages for NetplayJoin {
fn set_game_hash(&self, game_hash: slint::SharedString) {
self.set_game_hash(game_hash);
}
fn set_game_cheats(&self, game_cheats: slint::SharedString) {
self.set_game_cheats(game_cheats);
}
fn set_rom_path(&self, rom_path: slint::SharedString) {
self.set_rom_path(rom_path);
}
@@ -112,7 +116,7 @@ impl NetplayPages for NetplayJoin {
}
fn populate_server_names<T: ComponentHandle + NetplayPages + 'static>(weak: slint::Weak<T>) {
let task = reqwest::get("https://m64p.s3.amazonaws.com/serverstest.json");
let task = reqwest::get("https://m64p.s3.amazonaws.com/servers.json");
tokio::spawn(async move {
let mut local_servers: Vec<(String, String)> = vec![];
@@ -178,15 +182,25 @@ fn select_rom<T: ComponentHandle + NetplayPages + 'static>(weak: slint::Weak<T>)
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);
let game_crc = ui::storage::get_game_crc(&rom_contents);
let cheats = ui::config::Cheats::new();
let mut parsed_cheats = "".to_string();
if let Some(game_cheats) = cheats.cheats.get(&game_crc)
&& !game_cheats.is_empty()
{
parsed_cheats = serde_json::to_string(game_cheats).unwrap();
}
weak.upgrade_in_event_loop(move |handle| {
handle.set_game_name(game_name.into());
handle.set_game_hash(hash.into());
handle.set_game_cheats(parsed_cheats.into());
handle.set_rom_path(file.path().to_str().unwrap().into());
})
.unwrap();
} else {
weak.upgrade_in_event_loop(move |handle| {
let message_dialog = NetplayDialog::new().unwrap();
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();
@@ -196,6 +210,7 @@ fn select_rom<T: ComponentHandle + NetplayPages + 'static>(weak: slint::Weak<T>)
handle.set_game_name("".into());
handle.set_game_hash("".into());
handle.set_game_cheats("".into());
handle.set_rom_path("".into());
})
.unwrap();
@@ -236,8 +251,7 @@ fn update_ping<T: ComponentHandle + NetplayPages + 'static>(
pub fn setup_create_window(
create_window: &NetplayCreate,
overclock_setting: bool,
fullscreen: bool,
game_settings: GameSettings,
weak_app: slint::Weak<AppWindow>,
) {
let (netplay_read_sender, netplay_read_receiver): (
@@ -262,7 +276,13 @@ pub fn setup_create_window(
let weak = create_window.as_weak();
create_window.on_create_session(
move |server_url, session_name, player_name, game_name, game_hash, password| {
move |server_url,
session_name,
player_name,
game_name,
game_hash,
game_cheats,
password| {
let _ = netplay_write_sender.send(None); // close current websocket if any
manage_websocket(
server_url.to_string(),
@@ -278,9 +298,9 @@ pub fn setup_create_window(
player_name.to_string(),
game_name.to_string(),
game_hash.to_string(),
game_cheats.to_string(),
password.to_string(),
overclock_setting,
fullscreen,
game_settings.clone(),
weak_app.clone(),
weak.clone(),
);
@@ -338,7 +358,7 @@ fn manage_websocket<T: ComponentHandle + NetplayPages + 'static>(
}
fn show_netplay_error(message: String) {
let message_dialog = NetplayDialog::new().unwrap();
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();
@@ -407,6 +427,15 @@ fn update_sessions(
"False"
}),
));
session_vec.push(slint::StandardListViewItem::from(
slint::SharedString::from(
if room.features.unwrap_or_default().contains_key("cheats") {
"True"
} else {
"False"
},
),
));
let session_model: std::rc::Rc<
slint::VecModel<slint::StandardListViewItem>,
> = std::rc::Rc::new(session_vec);
@@ -444,6 +473,7 @@ fn update_sessions(
});
}
#[allow(clippy::too_many_arguments)]
fn create_session(
netplay_write_sender: tokio::sync::broadcast::Sender<Option<NetplayMessage>>,
mut netplay_read_receiver: tokio::sync::broadcast::Receiver<NetplayMessage>,
@@ -451,9 +481,9 @@ fn create_session(
player_name: String,
game_name: String,
game_hash: String,
game_cheats: String,
password: String,
overclock: bool,
fullscreen: bool,
game_settings: GameSettings,
weak_app: slint::Weak<AppWindow>,
weak: slint::Weak<NetplayCreate>,
) {
@@ -461,7 +491,16 @@ fn create_session(
let now_utc = chrono::Utc::now().timestamp_millis().to_string();
let hasher = Sha256::new().chain_update(&now_utc).chain_update(EMU_NAME);
let mut features = std::collections::HashMap::new();
features.insert("overclock".to_string(), overclock.to_string());
if !game_cheats.is_empty() {
features.insert("cheats".to_string(), game_cheats);
}
features.insert("overclock".to_string(), game_settings.overclock.to_string());
features.insert(
"disable_expansion_pak".to_string(),
game_settings.disable_expansion_pak.to_string(),
);
let create_room = NetplayMessage {
message_type: "request_create_room".to_string(),
@@ -498,7 +537,26 @@ fn create_session(
if message.accept.unwrap() == 0 {
weak.upgrade_in_event_loop(move |handle| {
let session = message.room.as_ref().unwrap();
let overclock = session.features.as_ref().unwrap().get("overclock").unwrap();
let features_default = "false".to_string();
let cheats_default = "{}".to_string();
let overclock = session
.features
.as_ref()
.unwrap()
.get("overclock")
.unwrap_or(&features_default);
let disable_expansion_pak = session
.features
.as_ref()
.unwrap()
.get("disable_expansion_pak")
.unwrap_or(&features_default);
let cheats = session
.features
.as_ref()
.unwrap()
.get("cheats")
.unwrap_or(&cheats_default);
setup_wait_window(
netplay_write_sender,
netplay_read_receiver,
@@ -508,8 +566,12 @@ fn create_session(
message.player_name.as_ref().unwrap().into(),
session.port.unwrap(),
true,
fullscreen,
overclock == "true",
GameSettings {
fullscreen: game_settings.fullscreen,
overclock: overclock.parse().unwrap(),
disable_expansion_pak: disable_expansion_pak.parse().unwrap(),
cheats: serde_json::from_str(cheats).unwrap(),
},
handle.get_peer_addr(),
weak_app,
);
@@ -535,6 +597,7 @@ fn create_session(
});
}
#[allow(clippy::too_many_arguments)]
fn join_session(
netplay_write_sender: tokio::sync::broadcast::Sender<Option<NetplayMessage>>,
mut netplay_read_receiver: tokio::sync::broadcast::Receiver<NetplayMessage>,
@@ -582,7 +645,26 @@ fn join_session(
if message.accept.unwrap() == 0 {
weak.upgrade_in_event_loop(move |handle| {
let session = message.room.as_ref().unwrap();
let overclock = session.features.as_ref().unwrap().get("overclock").unwrap();
let features_default = "false".to_string();
let cheats_default = "{}".to_string();
let overclock = session
.features
.as_ref()
.unwrap()
.get("overclock")
.unwrap_or(&features_default);
let disable_expansion_pak = session
.features
.as_ref()
.unwrap()
.get("disable_expansion_pak")
.unwrap_or(&features_default);
let cheats = session
.features
.as_ref()
.unwrap()
.get("cheats")
.unwrap_or(&cheats_default);
setup_wait_window(
netplay_write_sender,
netplay_read_receiver,
@@ -592,8 +674,12 @@ fn join_session(
message.player_name.as_ref().unwrap().into(),
session.port.unwrap(),
false,
fullscreen,
overclock == "true",
GameSettings {
fullscreen,
overclock: overclock.parse().unwrap(),
disable_expansion_pak: disable_expansion_pak.parse().unwrap(),
cheats: serde_json::from_str(cheats).unwrap(),
},
handle.get_peer_addr(),
weak_app,
);
@@ -619,6 +705,7 @@ fn join_session(
});
}
#[allow(clippy::too_many_arguments)]
fn setup_wait_window(
netplay_write_sender: tokio::sync::broadcast::Sender<Option<NetplayMessage>>,
mut netplay_read_receiver: tokio::sync::broadcast::Receiver<NetplayMessage>,
@@ -628,8 +715,7 @@ fn setup_wait_window(
player_name: slint::SharedString,
port: i32,
can_start: bool,
fullscreen: bool,
overclock: bool,
game_settings: GameSettings,
peer_addr: slint::SharedString,
weak_app: slint::Weak<AppWindow>,
) {
@@ -809,8 +895,12 @@ fn setup_wait_window(
ram: [None, None, None, None],
},
handle.get_rom_path().as_str().into(),
fullscreen,
overclock,
GameSettings {
fullscreen: game_settings.fullscreen,
overclock: game_settings.overclock,
disable_expansion_pak: game_settings.disable_expansion_pak,
cheats: game_settings.cheats,
},
VruChannel {
vru_window_notifier: None,
vru_word_receiver: None,
@@ -909,3 +999,51 @@ pub fn setup_join_window(
join_window.show().unwrap();
}
pub fn netplay_window(app: &AppWindow, controller_paths: &[Option<String>]) {
let weak_create = app.as_weak();
let weak_app = app.as_weak();
let controller_paths_create = controller_paths.to_owned();
app.on_create_session_button_clicked(move || {
let controller_paths = controller_paths_create.clone();
let weak_app = weak_app.clone();
weak_create
.upgrade_in_event_loop(move |handle| {
let create_window = NetplayCreate::new().unwrap();
save_settings(&handle, &controller_paths);
setup_create_window(
&create_window,
GameSettings {
fullscreen: handle.get_fullscreen(),
overclock: handle.get_overclock_n64_cpu(),
disable_expansion_pak: handle.get_disable_expansion_pak(),
cheats: std::collections::HashMap::new(), // not used here
},
weak_app,
);
})
.unwrap();
});
let weak_join = app.as_weak();
let weak_app = app.as_weak();
let controller_paths_join = controller_paths.to_owned();
app.on_join_session_button_clicked(move || {
let controller_paths = controller_paths_join.clone();
let weak_app = weak_app.clone();
weak_join
.upgrade_in_event_loop(move |handle| {
let join_window = NetplayJoin::new().unwrap();
save_settings(&handle, &controller_paths);
setup_join_window(&join_window, handle.get_fullscreen(), weak_app);
})
.unwrap();
});
app.on_netplay_discord_button_clicked(move || {
open::that_detached("https://discord.gg/JyW6ZgBUyS").unwrap();
});
app.on_netplay_feedback_button_clicked(move || {
open::that_detached("https://github.com/gopher64/gopher64/discussions/453").unwrap();
});
}
+16 -1
View File
@@ -120,6 +120,20 @@ 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]);
@@ -327,7 +341,8 @@ pub fn compress_file(data: &[(&[u8], &str)]) -> Vec<u8> {
.start_file(
item.1,
zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Zstd),
.compression_method(zip::CompressionMethod::Zstd)
.compression_level(Some(1)),
)
.unwrap();
writer.write_all(item.0).unwrap();
+17 -4
View File
@@ -37,10 +37,18 @@ pub fn init(device: &mut device::Device) {
sdl3_sys::video::SDL_CreateWindow(title.as_ptr(), window_width, window_height, flags)
};
if device.ui.video.window.is_null() {
panic!("Could not create window");
panic!("Could not create window: {}", unsafe {
std::ffi::CStr::from_ptr(sdl3_sys::error::SDL_GetError())
.to_str()
.unwrap()
});
}
if !unsafe { sdl3_sys::video::SDL_ShowWindow(device.ui.video.window) } {
panic!("Could not show window");
panic!("Could not show window: {}", unsafe {
std::ffi::CStr::from_ptr(sdl3_sys::error::SDL_GetError())
.to_str()
.unwrap()
});
}
unsafe {
sdl3_sys::everything::SDL_HideCursor();
@@ -114,7 +122,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 +132,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 +143,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) {
+1 -3
View File
@@ -1,5 +1,3 @@
use crate::ui;
pub fn prompt_for_match(
words: &[String],
window_notifier: &tokio::sync::mpsc::Sender<Option<Vec<String>>>,
@@ -13,7 +11,7 @@ pub fn prompt_for_match(
while result.is_err() {
result = word_index_receiver.try_recv();
std::thread::sleep(std::time::Duration::from_secs_f64(1.0 / 60.0));
ui::video::update_screen(); // so the OS doesn't complain about the game window being frozen
unsafe { sdl3_sys::events::SDL_PumpEvents() }; // so the OS doesn't complain about the game window being frozen
}
for (i, v) in words.iter().enumerate() {
if *v == *result.as_ref().unwrap() {