mirror of
https://github.com/gopher64/gopher64.git
synced 2026-07-11 01:25:20 +02:00
Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3da98c58c5 | |||
| 6e9b8ae126 | |||
| ada6936b9a | |||
| 0bb3d2cd5d | |||
| f213b0b9a9 | |||
| 69029f3a02 | |||
| 0c2b4b2aac | |||
| c7680f10a0 | |||
| a28ed6766b | |||
| 6e6b1d463d | |||
| 662c0060a1 | |||
| 79e4cb1807 | |||
| f3e819158a | |||
| e83bcbb4ce | |||
| eb5d255fd7 | |||
| 776d127a84 | |||
| af5f292459 | |||
| c25a56df50 | |||
| 9d9f965b41 | |||
| 68197d235b | |||
| 1c91abbf2d | |||
| 9348594eb0 | |||
| e97f4e7f16 | |||
| cea55914d9 | |||
| 77a22f08b3 | |||
| cf0f325ced | |||
| 21f1a274b4 | |||
| def8932922 | |||
| 5b6f9b76a6 | |||
| 5a838464a4 | |||
| 383b568012 | |||
| 27bb66df4c | |||
| 33f74e5062 | |||
| 68f90e1cd7 | |||
| cc5f636912 | |||
| e3a9849ad7 | |||
| 7ed6867691 | |||
| 11a8e96564 | |||
| 7111cc9761 | |||
| b4832b941f | |||
| 82b1888f82 | |||
| f35869190b | |||
| 64eda1a1e3 | |||
| 8026bf13d6 | |||
| 83103130e7 | |||
| 87717c4c7e | |||
| 6355885a56 | |||
| 5d180dfb65 | |||
| fd6965ff3b | |||
| 2fcf202a5c | |||
| 93630c4156 | |||
| a66e8184e4 | |||
| e4f05af38d | |||
| 8a09cb711f | |||
| a58eed7dc9 | |||
| 09ba7e4d13 | |||
| 7316e41c7e | |||
| 1aa00de1b2 | |||
| f71ec9d54a | |||
| bf2734e59c | |||
| 7ee26961f1 | |||
| 5d692318fe | |||
| f2966230f9 | |||
| 48575f9c52 | |||
| dbab58f4a7 | |||
| f6b0b59bbc | |||
| ca61695dd1 | |||
| a182fbb1f9 | |||
| da9b2247f8 | |||
| 1bcadb3a8b | |||
| 433f08c7e0 | |||
| b07dde8768 | |||
| cba7bc7477 | |||
| ea6f203e33 | |||
| e48ad9bdff | |||
| 11b1137412 | |||
| 5528f233c0 | |||
| 9d18673257 | |||
| 2a3fa03b35 | |||
| ec2f47e5d3 | |||
| 139d66eb82 | |||
| 6e10bee216 | |||
| dff9fc5381 | |||
| 84dcc6c154 | |||
| 8aa23e1665 |
@@ -1,4 +1,6 @@
|
||||
name: Rust
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -31,13 +33,13 @@ jobs:
|
||||
target: ${{ matrix.target }}
|
||||
rustflags: ""
|
||||
- name: Install SDL Dependencies
|
||||
# https://wiki.libsdl.org/SDL3/README/linux#build-dependencies
|
||||
# https://wiki.libsdl.org/SDL3/README-linux#build-dependencies
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1
|
||||
with:
|
||||
packages: build-essential git make \
|
||||
pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev \
|
||||
libaudio-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
|
||||
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev \
|
||||
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
|
||||
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
|
||||
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
|
||||
@@ -54,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:
|
||||
@@ -81,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:
|
||||
|
||||
@@ -6,6 +6,9 @@ on:
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
@@ -20,13 +23,13 @@ jobs:
|
||||
with:
|
||||
components: clippy, rustfmt
|
||||
- name: Install SDL Dependencies
|
||||
# https://wiki.libsdl.org/SDL3/README/linux#build-dependencies
|
||||
# https://wiki.libsdl.org/SDL3/README-linux#build-dependencies
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1
|
||||
with:
|
||||
packages: build-essential git make \
|
||||
pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev \
|
||||
libaudio-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
|
||||
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev \
|
||||
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
|
||||
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
|
||||
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
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
name: Security audit
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
jobs:
|
||||
audit:
|
||||
permissions:
|
||||
issues: write
|
||||
checks: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: rustsec/audit-check@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
+1
-1
@@ -9,7 +9,7 @@ target/
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
|
||||
.vscode/settings.json
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
|
||||
Generated
+2878
-902
File diff suppressed because it is too large
Load Diff
+15
-12
@@ -1,31 +1,33 @@
|
||||
[package]
|
||||
name = "gopher64"
|
||||
version = "1.0.11"
|
||||
version = "1.0.18"
|
||||
edition = "2024"
|
||||
rust-version = "1.85"
|
||||
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 = "2.2"
|
||||
governor = "0.8"
|
||||
sevenz-rust = "0.6"
|
||||
zip = "4.2"
|
||||
governor = "0.10"
|
||||
sevenz-rust2 = { version = "0.15", 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", "wgpu"] }
|
||||
slint = { version = "1.12", default-features = false, features = ["compat-1-2", "std", "backend-winit", "renderer-femtovg-wgpu"] }
|
||||
open = "5.3"
|
||||
sha2 = "0.10"
|
||||
rusttype = "0.9"
|
||||
sdl3-sys = { version = "0.4", features = ["build-from-source-static"] }
|
||||
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.43", features = ["rt-multi-thread", "macros"] }
|
||||
tokio = {version = "1.45", features = ["rt-multi-thread", "macros"] }
|
||||
spin_sleep = "1.3"
|
||||
encoding_rs = "0.8"
|
||||
mimalloc = "0.1"
|
||||
regex = "1.11"
|
||||
tungstenite = "0.26"
|
||||
tokio-tungstenite = "0.27"
|
||||
futures = "0.3"
|
||||
socket2 = "0.5"
|
||||
rand_chacha = "0.9"
|
||||
fatfs = "0.3"
|
||||
@@ -35,8 +37,9 @@ clap = { version = "4.5", features = ["derive"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["http2","rustls-tls","charset","json"] }
|
||||
|
||||
[build-dependencies]
|
||||
winres = "0.1"
|
||||
bindgen = "0.71"
|
||||
winresource = "0.1"
|
||||
bindgen = "0.72"
|
||||
slint-build = "1.12"
|
||||
cc = { version = "1.2", features = ["parallel"] }
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -15,17 +15,17 @@ https://discord.gg/9RGXq8W8JQ
|
||||
|
||||
Keys are mapped according to [these defaults](https://github.com/gopher64/gopher64/wiki/Default-Keyboard-Setup). Xbox-style controllers also have a [default mapping applied](https://github.com/gopher64/gopher64/wiki/Default-Gamepad-Setup).
|
||||
|
||||
You can create you own mappings by running `./gopher64 --configure-input-profile my_profile`. You then bind that profile to a port: `./gopher64 --bind-input-profile my_profile --port 1`
|
||||
You can create your mappings by running `./gopher64 --configure-input-profile my_profile`. You then bind that profile to a port: `./gopher64 --bind-input-profile my_profile --port 1`
|
||||
|
||||
In order to use a controller (for example, an Xbox controller), run `./gopher64 --list-controllers` to get a list of attached controllers, and then assign it by doing `./gopher64 --assign-controller <controller_number> --port 1`
|
||||
To use a controller (for example, an Xbox controller), run `./gopher64 --list-controllers` to get a list of attached controllers, and then assign it by doing `./gopher64 --assign-controller <controller_number> --port 1`
|
||||
|
||||
## netplay
|
||||
|
||||
Gopher64 supports netplay (online play with others). It has a few public netplay servers. If you are interested in running a public netplay server, please let me know (open an issue or discussion, or contact me on Discord). You can also run the server (https://github.com/simple64/simple64-netplay-server) yourself on a LAN.
|
||||
Gopher64 supports netplay (online play with others). It has a few public netplay servers. If you are interested in running a public netplay server, please let me know (open an issue or discussion or contact me on Discord). You can also run the server (https://github.com/gopher64/gopher64-netplay-server) yourself on a LAN.
|
||||
|
||||
## portable mode
|
||||
|
||||
If you would like to keep all the game data in the same folder as executable, you just need to create a file called "portable.txt" in the same directory as the executable.
|
||||
If you would like to keep all the game data in the same folder as the executable, you just need to create a file called "portable.txt" in the same directory as the executable.
|
||||
|
||||
## flatpak
|
||||
|
||||
@@ -39,11 +39,11 @@ flatpak run --filesystem=host:ro io.github.gopher64.gopher64 /path/to/rom.z64
|
||||
|
||||
1. Performance. I want to be able to use this emulator on my laptop.
|
||||
2. Easy to use.
|
||||
3. Easy to work on. Dynamic recompilers perform well, but they are very hard to read and understand. This emulator will only have interpreters for the CPU and RSP. Additionally, it is completely written in Rust (besides Parallel-RDP), a modern programming language with a growing user base. I've tried to avoid the use of macros, which can reduce some repetitiveness in the code, but also reduce readability.
|
||||
3. Easy to work on. Dynamic recompilers perform well, but they are very hard to read and understand. This emulator will only have interpreters for the CPU and RSP. Additionally, it is completely written in Rust (besides Parallel-RDP), a modern programming language with a growing user base. I've tried to avoid the use of macros, which can reduce some repetitiveness in the code but also reduce readability.
|
||||
|
||||
## building and usage
|
||||
|
||||
1. Linux only: install the SDL3 dependencies: https://wiki.libsdl.org/SDL3/README/linux
|
||||
1. Linux only: install the SDL3 dependencies: https://wiki.libsdl.org/SDL3/README-linux#build-dependencies
|
||||
2. Install rust: https://www.rust-lang.org/tools/install
|
||||
3. `git clone --recursive https://github.com/gopher64/gopher64.git`
|
||||
4. `cd gopher64`
|
||||
@@ -52,7 +52,7 @@ flatpak run --filesystem=host:ro io.github.gopher64.gopher64 /path/to/rom.z64
|
||||
|
||||
## contributing
|
||||
|
||||
I am very open to contributions! Please reach out to me via a GitHub issue, or via discord (loganmc10) before doing substantial work on a PR.
|
||||
I am very open to contributions! Please contact me via a GitHub issue or Discord (loganmc10) before doing substantial work on a PR.
|
||||
|
||||
## license
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -59,9 +62,16 @@ 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();
|
||||
if os == "windows" {
|
||||
if arch == "x86_64" {
|
||||
build.flag("/arch:AVX2");
|
||||
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 {
|
||||
@@ -69,7 +79,7 @@ fn main() {
|
||||
}
|
||||
build.flag("-DVK_USE_PLATFORM_WIN32_KHR");
|
||||
|
||||
winres::WindowsResource::new()
|
||||
winresource::WindowsResource::new()
|
||||
.set_icon("data/icon.ico")
|
||||
.compile()
|
||||
.unwrap();
|
||||
@@ -101,8 +111,9 @@ fn main() {
|
||||
.allowlist_function("rdp_process_commands")
|
||||
.allowlist_function("rdp_check_callback")
|
||||
.allowlist_function("rdp_new_processor")
|
||||
.allowlist_function("rdp_check_framebuffers")
|
||||
.allowlist_function("rdp_full_sync")
|
||||
.allowlist_function("rdp_state_size")
|
||||
.allowlist_function("rdp_save_state")
|
||||
.allowlist_function("rdp_load_state")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
.generate()
|
||||
.expect("Unable to generate bindings");
|
||||
@@ -169,7 +180,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);
|
||||
}
|
||||
|
||||
+59
-117
@@ -52,42 +52,32 @@ enum vi_registers
|
||||
VI_REGS_COUNT
|
||||
};
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint32_t cmd_data[0x00040000 >> 2];
|
||||
int cmd_cur;
|
||||
int cmd_ptr;
|
||||
uint32_t region;
|
||||
} RDP_DEVICE;
|
||||
|
||||
static SDL_Window *window;
|
||||
static RDP::CommandProcessor *processor;
|
||||
static SDL_WSIPlatform *wsi_platform;
|
||||
static WSI *wsi;
|
||||
static uint32_t cmd_data[0x00040000 >> 2];
|
||||
static int cmd_cur;
|
||||
static int cmd_ptr;
|
||||
|
||||
static RDP_DEVICE rdp_device;
|
||||
static bool crop_letterbox;
|
||||
static CALL_BACK callback;
|
||||
static GFX_INFO gfx_info;
|
||||
static uint32_t region;
|
||||
static bool crop_letterbox;
|
||||
static const uint32_t *fragment_spirv;
|
||||
static size_t fragment_size;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint32_t depthbuffer_address;
|
||||
uint32_t framebuffer_address;
|
||||
uint32_t framebuffer_pixel_size;
|
||||
uint32_t framebuffer_width;
|
||||
uint32_t framebuffer_height;
|
||||
uint32_t framebuffer_size;
|
||||
uint32_t depthbuffer_size;
|
||||
uint8_t depthbuffer_enabled;
|
||||
} FrameBufferInfo;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
float SourceSize[4];
|
||||
float OutputSize[4];
|
||||
} Push;
|
||||
|
||||
static uint8_t *rdram_dirty;
|
||||
static uint64_t sync_signal;
|
||||
static FrameBufferInfo frame_buffer_info;
|
||||
|
||||
static const unsigned cmd_len_lut[64] = {
|
||||
1,
|
||||
1,
|
||||
@@ -195,6 +185,12 @@ bool sdl_event_filter(void *userdata, SDL_Event *event)
|
||||
case SDL_SCANCODE_F7:
|
||||
callback.load_state = true;
|
||||
break;
|
||||
case SDL_SCANCODE_LEFTBRACKET:
|
||||
callback.lower_volume = true;
|
||||
break;
|
||||
case SDL_SCANCODE_RIGHTBRACKET:
|
||||
callback.raise_volume = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -205,11 +201,8 @@ bool sdl_event_filter(void *userdata, SDL_Event *event)
|
||||
|
||||
void rdp_new_processor(GFX_INFO _gfx_info)
|
||||
{
|
||||
memset(&frame_buffer_info, 0, sizeof(FrameBufferInfo));
|
||||
sync_signal = 0;
|
||||
memset(rdram_dirty, 0, gfx_info.RDRAM_SIZE / 8);
|
||||
|
||||
gfx_info = _gfx_info;
|
||||
|
||||
if (processor)
|
||||
{
|
||||
delete processor;
|
||||
@@ -270,7 +263,6 @@ void rdp_init(void *_window, GFX_INFO _gfx_info)
|
||||
rdp_close();
|
||||
}
|
||||
|
||||
rdram_dirty = (uint8_t *)malloc(gfx_info.RDRAM_SIZE / 8);
|
||||
rdp_new_processor(gfx_info);
|
||||
|
||||
if (!processor->device_is_supported())
|
||||
@@ -289,12 +281,6 @@ void rdp_init(void *_window, GFX_INFO _gfx_info)
|
||||
|
||||
void rdp_close()
|
||||
{
|
||||
if (rdram_dirty)
|
||||
{
|
||||
free(rdram_dirty);
|
||||
rdram_dirty = nullptr;
|
||||
}
|
||||
|
||||
wsi->end_frame();
|
||||
|
||||
if (processor)
|
||||
@@ -414,7 +400,7 @@ static void render_frame(Vulkan::Device &device)
|
||||
if (image)
|
||||
{
|
||||
VkViewport vp = cmd->get_viewport();
|
||||
calculate_viewport(&vp.x, &vp.y, &vp.width, &vp.height, image->get_height());
|
||||
calculate_viewport(&vp.x, &vp.y, &vp.width, &vp.height, image->get_height() / gfx_info.upscale);
|
||||
|
||||
if (gfx_info.crt)
|
||||
{
|
||||
@@ -456,42 +442,24 @@ CALL_BACK rdp_check_callback()
|
||||
CALL_BACK return_value = callback;
|
||||
callback.save_state = false;
|
||||
callback.load_state = false;
|
||||
callback.lower_volume = false;
|
||||
callback.raise_volume = false;
|
||||
return return_value;
|
||||
}
|
||||
|
||||
void rdp_check_framebuffers(uint32_t address)
|
||||
size_t rdp_state_size()
|
||||
{
|
||||
if (sync_signal && rdram_dirty[address >> 3])
|
||||
{
|
||||
processor->wait_for_timeline(sync_signal);
|
||||
memset(rdram_dirty, 0, gfx_info.RDRAM_SIZE / 8);
|
||||
sync_signal = 0;
|
||||
}
|
||||
return sizeof(RDP_DEVICE);
|
||||
}
|
||||
|
||||
void rdp_full_sync()
|
||||
void rdp_save_state(uint8_t *state)
|
||||
{
|
||||
processor->wait_for_timeline(processor->signal_timeline());
|
||||
memcpy(state, &rdp_device, sizeof(RDP_DEVICE));
|
||||
}
|
||||
|
||||
void calculate_buffer_size()
|
||||
void rdp_load_state(const uint8_t *state)
|
||||
{
|
||||
switch (frame_buffer_info.framebuffer_pixel_size)
|
||||
{
|
||||
case 0:
|
||||
frame_buffer_info.framebuffer_size = (frame_buffer_info.framebuffer_width * frame_buffer_info.framebuffer_height / 2) >> 3;
|
||||
break;
|
||||
case 1:
|
||||
frame_buffer_info.framebuffer_size = (frame_buffer_info.framebuffer_width * frame_buffer_info.framebuffer_height) >> 3;
|
||||
break;
|
||||
case 2:
|
||||
frame_buffer_info.framebuffer_size = (frame_buffer_info.framebuffer_width * frame_buffer_info.framebuffer_height * 2) >> 3;
|
||||
break;
|
||||
case 3:
|
||||
frame_buffer_info.framebuffer_size = (frame_buffer_info.framebuffer_width * frame_buffer_info.framebuffer_height * 4) >> 3;
|
||||
break;
|
||||
}
|
||||
frame_buffer_info.depthbuffer_size = (frame_buffer_info.framebuffer_width * frame_buffer_info.framebuffer_height * 2) >> 3;
|
||||
memcpy(&rdp_device, state, sizeof(RDP_DEVICE));
|
||||
}
|
||||
|
||||
uint64_t rdp_process_commands()
|
||||
@@ -505,7 +473,7 @@ uint64_t rdp_process_commands()
|
||||
return interrupt_timer;
|
||||
|
||||
length = unsigned(length) >> 3;
|
||||
if ((cmd_ptr + length) & ~(0x0003FFFF >> 3))
|
||||
if ((rdp_device.cmd_ptr + length) & ~(0x0003FFFF >> 3))
|
||||
return interrupt_timer;
|
||||
|
||||
uint32_t offset = DP_CURRENT;
|
||||
@@ -514,10 +482,10 @@ uint64_t rdp_process_commands()
|
||||
do
|
||||
{
|
||||
offset &= 0xFF8;
|
||||
cmd_data[2 * cmd_ptr + 0] = SDL_Swap32BE(*reinterpret_cast<const uint32_t *>(gfx_info.DMEM + offset));
|
||||
cmd_data[2 * cmd_ptr + 1] = SDL_Swap32BE(*reinterpret_cast<const uint32_t *>(gfx_info.DMEM + offset + 4));
|
||||
rdp_device.cmd_data[2 * rdp_device.cmd_ptr + 0] = SDL_Swap32BE(*reinterpret_cast<const uint32_t *>(gfx_info.DMEM + offset));
|
||||
rdp_device.cmd_data[2 * rdp_device.cmd_ptr + 1] = SDL_Swap32BE(*reinterpret_cast<const uint32_t *>(gfx_info.DMEM + offset + 4));
|
||||
offset += sizeof(uint64_t);
|
||||
cmd_ptr++;
|
||||
rdp_device.cmd_ptr++;
|
||||
} while (--length > 0);
|
||||
}
|
||||
else
|
||||
@@ -531,89 +499,63 @@ uint64_t rdp_process_commands()
|
||||
do
|
||||
{
|
||||
offset &= 0xFFFFF8;
|
||||
cmd_data[2 * cmd_ptr + 0] = *reinterpret_cast<const uint32_t *>(gfx_info.RDRAM + offset);
|
||||
cmd_data[2 * cmd_ptr + 1] = *reinterpret_cast<const uint32_t *>(gfx_info.RDRAM + offset + 4);
|
||||
rdp_device.cmd_data[2 * rdp_device.cmd_ptr + 0] = *reinterpret_cast<const uint32_t *>(gfx_info.RDRAM + offset);
|
||||
rdp_device.cmd_data[2 * rdp_device.cmd_ptr + 1] = *reinterpret_cast<const uint32_t *>(gfx_info.RDRAM + offset + 4);
|
||||
offset += sizeof(uint64_t);
|
||||
cmd_ptr++;
|
||||
rdp_device.cmd_ptr++;
|
||||
} while (--length > 0);
|
||||
}
|
||||
}
|
||||
|
||||
while (cmd_cur - cmd_ptr < 0)
|
||||
while (rdp_device.cmd_cur - rdp_device.cmd_ptr < 0)
|
||||
{
|
||||
uint32_t w1 = cmd_data[2 * cmd_cur];
|
||||
uint32_t w2 = cmd_data[2 * cmd_cur + 1];
|
||||
uint32_t w1 = rdp_device.cmd_data[2 * rdp_device.cmd_cur];
|
||||
uint32_t w2 = rdp_device.cmd_data[2 * rdp_device.cmd_cur + 1];
|
||||
uint32_t command = (w1 >> 24) & 63;
|
||||
int cmd_length = cmd_len_lut[command];
|
||||
|
||||
if (cmd_ptr - cmd_cur - cmd_length < 0)
|
||||
if (rdp_device.cmd_ptr - rdp_device.cmd_cur - cmd_length < 0)
|
||||
{
|
||||
*gfx_info.DPC_START_REG = *gfx_info.DPC_CURRENT_REG = *gfx_info.DPC_END_REG;
|
||||
return interrupt_timer;
|
||||
}
|
||||
|
||||
if (command >= 8)
|
||||
processor->enqueue_command(cmd_length * 2, &cmd_data[2 * cmd_cur]);
|
||||
processor->enqueue_command(cmd_length * 2, &rdp_device.cmd_data[2 * rdp_device.cmd_cur]);
|
||||
|
||||
if ((RDP::Op(command) >= RDP::Op::FillTriangle && RDP::Op(command) <= RDP::Op::ShadeTextureZBufferTriangle) ||
|
||||
RDP::Op(command) == RDP::Op::TextureRectangle ||
|
||||
RDP::Op(command) == RDP::Op::TextureRectangleFlip ||
|
||||
RDP::Op(command) == RDP::Op::FillRectangle)
|
||||
switch (RDP::Op(command))
|
||||
{
|
||||
if (!rdram_dirty[frame_buffer_info.framebuffer_address])
|
||||
{
|
||||
for (uint32_t i = frame_buffer_info.framebuffer_address; i < frame_buffer_info.framebuffer_address + frame_buffer_info.framebuffer_size; ++i)
|
||||
{
|
||||
rdram_dirty[i] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (frame_buffer_info.depthbuffer_enabled && !rdram_dirty[frame_buffer_info.depthbuffer_address])
|
||||
{
|
||||
for (uint32_t i = frame_buffer_info.depthbuffer_address; i < frame_buffer_info.depthbuffer_address + frame_buffer_info.depthbuffer_size; ++i)
|
||||
{
|
||||
rdram_dirty[i] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (RDP::Op(command) == RDP::Op::SetOtherModes)
|
||||
{
|
||||
frame_buffer_info.depthbuffer_enabled = (w2 >> 5) & 1;
|
||||
}
|
||||
else if (RDP::Op(command) == RDP::Op::SetColorImage)
|
||||
{
|
||||
frame_buffer_info.framebuffer_address = (w2 & 0x00FFFFFF) >> 3;
|
||||
frame_buffer_info.framebuffer_pixel_size = (w1 >> 19) & 0x3;
|
||||
frame_buffer_info.framebuffer_width = (w1 & 0x3FF) + 1;
|
||||
calculate_buffer_size();
|
||||
}
|
||||
else if (RDP::Op(command) == RDP::Op::SetMaskImage)
|
||||
{
|
||||
frame_buffer_info.depthbuffer_address = (w2 & 0x00FFFFFF) >> 3;
|
||||
}
|
||||
else if (RDP::Op(command) == RDP::Op::SetScissor)
|
||||
case RDP::Op::SetScissor:
|
||||
{
|
||||
uint32_t upper_left_x = ((w1 >> 12) & 0xFFF) >> 2;
|
||||
uint32_t upper_left_y = (w1 & 0xFFF) >> 2;
|
||||
uint32_t lower_right_x = ((w2 >> 12) & 0xFFF) >> 2;
|
||||
uint32_t lower_right_y = (w2 & 0xFFF) >> 2;
|
||||
region = (lower_right_x - upper_left_x) * (lower_right_y - upper_left_y);
|
||||
frame_buffer_info.framebuffer_height = lower_right_y;
|
||||
calculate_buffer_size();
|
||||
if (lower_right_x > upper_left_x && lower_right_y > upper_left_y)
|
||||
{
|
||||
rdp_device.region = (lower_right_x - upper_left_x) * (lower_right_y - upper_left_y);
|
||||
}
|
||||
else
|
||||
{
|
||||
rdp_device.region = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
else if (RDP::Op(command) == RDP::Op::SyncFull)
|
||||
{
|
||||
sync_signal = processor->signal_timeline();
|
||||
interrupt_timer = region;
|
||||
case RDP::Op::SyncFull:
|
||||
processor->wait_for_timeline(processor->signal_timeline());
|
||||
interrupt_timer = rdp_device.region;
|
||||
if (interrupt_timer == 0)
|
||||
interrupt_timer = 5000;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
cmd_cur += cmd_length;
|
||||
rdp_device.cmd_cur += cmd_length;
|
||||
}
|
||||
|
||||
cmd_ptr = 0;
|
||||
cmd_cur = 0;
|
||||
rdp_device.cmd_ptr = 0;
|
||||
rdp_device.cmd_cur = 0;
|
||||
*gfx_info.DPC_CURRENT_REG = *gfx_info.DPC_END_REG;
|
||||
|
||||
return interrupt_timer;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
#include <stddef.h>
|
||||
|
||||
extern "C"
|
||||
{
|
||||
@@ -30,6 +31,8 @@ extern "C"
|
||||
bool save_state;
|
||||
bool load_state;
|
||||
bool enable_speedlimiter;
|
||||
bool lower_volume;
|
||||
bool raise_volume;
|
||||
} CALL_BACK;
|
||||
|
||||
void rdp_init(void *_window, GFX_INFO _gfx_info);
|
||||
@@ -39,8 +42,9 @@ extern "C"
|
||||
CALL_BACK rdp_check_callback();
|
||||
uint64_t rdp_process_commands();
|
||||
void rdp_new_processor(GFX_INFO _gfx_info);
|
||||
void rdp_check_framebuffers(uint32_t address);
|
||||
void rdp_full_sync();
|
||||
size_t rdp_state_size();
|
||||
void rdp_save_state(uint8_t *state);
|
||||
void rdp_load_state(const uint8_t *state);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ std::vector<const char *> SDL_WSIPlatform::get_instance_extensions()
|
||||
}
|
||||
|
||||
std::vector<const char *> extensionNames;
|
||||
for (int i = 0; i < extensionCount; ++i)
|
||||
for (unsigned int i = 0; i < extensionCount; ++i)
|
||||
{
|
||||
extensionNames.push_back(extensions[i]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "1.88.0"
|
||||
+15
-13
@@ -61,7 +61,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);
|
||||
@@ -99,11 +99,11 @@ fn init_rng(device: &mut Device) {
|
||||
device.rng = rand_chacha::ChaCha8Rng::seed_from_u64(rng_seed);
|
||||
}
|
||||
|
||||
fn swap_rom(contents: Vec<u8>) -> Vec<u8> {
|
||||
fn swap_rom(contents: Vec<u8>) -> Option<Vec<u8>> {
|
||||
let test = u32::from_be_bytes(contents[0..4].try_into().unwrap());
|
||||
if test == 0x80371240 {
|
||||
// z64
|
||||
contents
|
||||
Some(contents)
|
||||
} else if test == 0x37804012 {
|
||||
// v64
|
||||
let mut data: Vec<u8> = vec![0; contents.len()];
|
||||
@@ -111,7 +111,7 @@ fn swap_rom(contents: Vec<u8>) -> Vec<u8> {
|
||||
let temp = u16::from_ne_bytes(contents[i..i + 2].try_into().unwrap());
|
||||
data[i..i + 2].copy_from_slice(&temp.to_be_bytes());
|
||||
}
|
||||
return data;
|
||||
Some(data)
|
||||
} else if test == 0x40123780 {
|
||||
// n64
|
||||
let mut data: Vec<u8> = vec![0; contents.len()];
|
||||
@@ -119,14 +119,13 @@ fn swap_rom(contents: Vec<u8>) -> Vec<u8> {
|
||||
let temp = u32::from_ne_bytes(contents[i..i + 4].try_into().unwrap());
|
||||
data[i..i + 4].copy_from_slice(&temp.to_be_bytes());
|
||||
}
|
||||
return data;
|
||||
Some(data)
|
||||
} else {
|
||||
let data: Vec<u8> = vec![];
|
||||
data
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_rom_contents(file_path: &std::path::Path) -> Vec<u8> {
|
||||
pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
|
||||
let mut contents = vec![];
|
||||
if file_path.extension().unwrap().eq_ignore_ascii_case("zip") {
|
||||
let zip_file = fs::File::open(file_path).unwrap();
|
||||
@@ -147,12 +146,12 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Vec<u8> {
|
||||
}
|
||||
} else if file_path.extension().unwrap().eq_ignore_ascii_case("7z") {
|
||||
let mut archive =
|
||||
sevenz_rust::SevenZReader::open(file_path, sevenz_rust::Password::empty()).unwrap();
|
||||
sevenz_rust2::SevenZReader::open(file_path, sevenz_rust2::Password::empty()).unwrap();
|
||||
|
||||
let mut found = false;
|
||||
archive
|
||||
.for_each_entries(
|
||||
&mut |entry: &sevenz_rust::SevenZArchiveEntry, reader: &mut dyn std::io::Read| {
|
||||
&mut |entry: &sevenz_rust2::SevenZArchiveEntry, 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"))
|
||||
@@ -173,7 +172,11 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Vec<u8> {
|
||||
contents = fs::read(file_path).expect("Should have been able to read the file");
|
||||
}
|
||||
|
||||
swap_rom(contents)
|
||||
if contents.is_empty() {
|
||||
None
|
||||
} else {
|
||||
swap_rom(contents)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
@@ -348,7 +351,7 @@ impl Device {
|
||||
},
|
||||
},
|
||||
memory: memory::Memory {
|
||||
fast_read: [unmapped::read_mem; 0x2000],
|
||||
fast_read: [unmapped::read_mem_fast; 0x2000],
|
||||
memory_map_read: [unmapped::read_mem; 0x2000],
|
||||
memory_map_write: [unmapped::write_mem; 0x2000],
|
||||
icache: [cache::ICache {
|
||||
@@ -476,7 +479,6 @@ impl Device {
|
||||
vru_window: controller::vru::VruWindow {
|
||||
window_notifier: None,
|
||||
word_receiver: None,
|
||||
gui_ctx: None,
|
||||
},
|
||||
vru: controller::vru::Vru {
|
||||
status: 0,
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ pub struct AiDma {
|
||||
pub duration: u64,
|
||||
}
|
||||
|
||||
fn get_remaining_dma_length(device: &mut device::Device) -> u64 {
|
||||
fn get_remaining_dma_length(device: &device::Device) -> u64 {
|
||||
if device.ai.fifo[0].duration == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
+36
-160
@@ -30,54 +30,14 @@ pub fn icache_writeback(device: &mut device::Device, line_index: usize) {
|
||||
let cache_address = ((device.memory.icache[line_index].tag
|
||||
| (device.memory.icache[line_index].index) as u32)
|
||||
& 0x1ffffffc) as u64;
|
||||
device.memory.memory_map_write[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address,
|
||||
device.memory.icache[line_index].words[0],
|
||||
0xFFFFFFFF,
|
||||
);
|
||||
device.memory.memory_map_write[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0x4,
|
||||
device.memory.icache[line_index].words[1],
|
||||
0xFFFFFFFF,
|
||||
);
|
||||
device.memory.memory_map_write[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0x8,
|
||||
device.memory.icache[line_index].words[2],
|
||||
0xFFFFFFFF,
|
||||
);
|
||||
device.memory.memory_map_write[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0xC,
|
||||
device.memory.icache[line_index].words[3],
|
||||
0xFFFFFFFF,
|
||||
);
|
||||
device.memory.memory_map_write[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0x10,
|
||||
device.memory.icache[line_index].words[4],
|
||||
0xFFFFFFFF,
|
||||
);
|
||||
device.memory.memory_map_write[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0x14,
|
||||
device.memory.icache[line_index].words[5],
|
||||
0xFFFFFFFF,
|
||||
);
|
||||
device.memory.memory_map_write[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0x18,
|
||||
device.memory.icache[line_index].words[6],
|
||||
0xFFFFFFFF,
|
||||
);
|
||||
device.memory.memory_map_write[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0x1C,
|
||||
device.memory.icache[line_index].words[7],
|
||||
0xFFFFFFFF,
|
||||
);
|
||||
for i in 0..8 {
|
||||
device.memory.memory_map_write[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | (i * 4),
|
||||
device.memory.icache[line_index].words[i as usize],
|
||||
0xFFFFFFFF,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icache_fill(device: &mut device::Device, line_index: usize, phys_address: u64) {
|
||||
@@ -88,71 +48,17 @@ pub fn icache_fill(device: &mut device::Device, line_index: usize, phys_address:
|
||||
let cache_address = ((device.memory.icache[line_index].tag
|
||||
| (device.memory.icache[line_index].index) as u32)
|
||||
& 0x1ffffffc) as u64;
|
||||
device.memory.icache[line_index].words[0] = device.memory.memory_map_read
|
||||
[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address,
|
||||
device::memory::AccessSize::Icache,
|
||||
);
|
||||
device.memory.icache[line_index].words[1] = device.memory.memory_map_read
|
||||
[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0x4,
|
||||
device::memory::AccessSize::Icache,
|
||||
);
|
||||
device.memory.icache[line_index].words[2] = device.memory.memory_map_read
|
||||
[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0x8,
|
||||
device::memory::AccessSize::Icache,
|
||||
);
|
||||
device.memory.icache[line_index].words[3] = device.memory.memory_map_read
|
||||
[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0xC,
|
||||
device::memory::AccessSize::Icache,
|
||||
);
|
||||
device.memory.icache[line_index].words[4] = device.memory.memory_map_read
|
||||
[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0x10,
|
||||
device::memory::AccessSize::Icache,
|
||||
);
|
||||
device.memory.icache[line_index].words[5] = device.memory.memory_map_read
|
||||
[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0x14,
|
||||
device::memory::AccessSize::Icache,
|
||||
);
|
||||
device.memory.icache[line_index].words[6] = device.memory.memory_map_read
|
||||
[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0x18,
|
||||
device::memory::AccessSize::Icache,
|
||||
);
|
||||
device.memory.icache[line_index].words[7] = device.memory.memory_map_read
|
||||
[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0x1C,
|
||||
device::memory::AccessSize::Icache,
|
||||
);
|
||||
for i in 0..8 {
|
||||
device.memory.icache[line_index].words[i as usize] = device.memory.memory_map_read
|
||||
[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | (i * 4),
|
||||
device::memory::AccessSize::Icache,
|
||||
);
|
||||
|
||||
device.memory.icache[line_index].instruction[0] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[0]);
|
||||
device.memory.icache[line_index].instruction[1] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[1]);
|
||||
device.memory.icache[line_index].instruction[2] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[2]);
|
||||
device.memory.icache[line_index].instruction[3] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[3]);
|
||||
device.memory.icache[line_index].instruction[4] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[4]);
|
||||
device.memory.icache[line_index].instruction[5] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[5]);
|
||||
device.memory.icache[line_index].instruction[6] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[6]);
|
||||
device.memory.icache[line_index].instruction[7] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[7]);
|
||||
device.memory.icache[line_index].instruction[i as usize] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[i as usize]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icache_fetch(device: &mut device::Device, phys_address: u64) {
|
||||
@@ -180,30 +86,15 @@ pub fn dcache_writeback(device: &mut device::Device, line_index: usize) {
|
||||
let cache_address = ((device.memory.dcache[line_index].tag
|
||||
| (device.memory.dcache[line_index].index) as u32)
|
||||
& 0x1ffffffc) as u64;
|
||||
device.memory.memory_map_write[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address,
|
||||
device.memory.dcache[line_index].words[0],
|
||||
0xFFFFFFFF,
|
||||
);
|
||||
device.memory.memory_map_write[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0x4,
|
||||
device.memory.dcache[line_index].words[1],
|
||||
0xFFFFFFFF,
|
||||
);
|
||||
device.memory.memory_map_write[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0x8,
|
||||
device.memory.dcache[line_index].words[2],
|
||||
0xFFFFFFFF,
|
||||
);
|
||||
device.memory.memory_map_write[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0xC,
|
||||
device.memory.dcache[line_index].words[3],
|
||||
0xFFFFFFFF,
|
||||
);
|
||||
|
||||
for i in 0..4 {
|
||||
device.memory.memory_map_write[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | (i * 4),
|
||||
device.memory.dcache[line_index].words[i as usize],
|
||||
0xFFFFFFFF,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn dcache_fill(device: &mut device::Device, line_index: usize, phys_address: u64) {
|
||||
@@ -216,30 +107,15 @@ fn dcache_fill(device: &mut device::Device, line_index: usize, phys_address: u64
|
||||
let cache_address = ((device.memory.dcache[line_index].tag
|
||||
| (device.memory.dcache[line_index].index) as u32)
|
||||
& 0x1ffffffc) as u64;
|
||||
device.memory.dcache[line_index].words[0] = device.memory.memory_map_read
|
||||
[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address,
|
||||
device::memory::AccessSize::Dcache,
|
||||
);
|
||||
device.memory.dcache[line_index].words[1] = device.memory.memory_map_read
|
||||
[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0x4,
|
||||
device::memory::AccessSize::Dcache,
|
||||
);
|
||||
device.memory.dcache[line_index].words[2] = device.memory.memory_map_read
|
||||
[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0x8,
|
||||
device::memory::AccessSize::Dcache,
|
||||
);
|
||||
device.memory.dcache[line_index].words[3] = device.memory.memory_map_read
|
||||
[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | 0xC,
|
||||
device::memory::AccessSize::Dcache,
|
||||
);
|
||||
|
||||
for i in 0..4 {
|
||||
device.memory.dcache[line_index].words[i as usize] = device.memory.memory_map_read
|
||||
[(cache_address >> 16) as usize](
|
||||
device,
|
||||
cache_address | (i * 4),
|
||||
device::memory::AccessSize::Dcache,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dcache_read(device: &mut device::Device, phys_address: u64) -> u32 {
|
||||
|
||||
@@ -4,7 +4,7 @@ use sha2::{Digest, Sha256};
|
||||
|
||||
const CART_MASK: usize = 0xFFFFFFF;
|
||||
|
||||
fn read_cart_word(device: &mut device::Device, address: usize) -> u32 {
|
||||
fn read_cart_word(device: &device::Device, address: usize) -> u32 {
|
||||
let mut data: [u8; 4] = [0; 4];
|
||||
for i in 0..4 {
|
||||
if let Some(value) = device
|
||||
@@ -24,7 +24,7 @@ fn read_cart_word(device: &mut device::Device, address: usize) -> u32 {
|
||||
}
|
||||
|
||||
pub fn read_mem_fast(
|
||||
device: &mut device::Device,
|
||||
device: &device::Device,
|
||||
address: u64,
|
||||
_access_size: device::memory::AccessSize,
|
||||
) -> u32 {
|
||||
@@ -186,7 +186,7 @@ fn set_cic(device: &mut device::Device) {
|
||||
}
|
||||
_ => {
|
||||
device.cart.cic_seed = 0x3F; // CicNus6102
|
||||
println!("unknown IPL3 {}", hash)
|
||||
//println!("unknown IPL3 {}", hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
+10
-10
@@ -57,10 +57,10 @@ pub fn process(device: &mut device::Device, channel: usize) {
|
||||
JCMD_CONTROLLER_READ => {
|
||||
let offset = device.pif.channels[channel].rx_buf.unwrap();
|
||||
let input = if device.netplay.is_none() {
|
||||
ui::input::get(&mut device.ui, channel)
|
||||
ui::input::get(&device.ui, channel)
|
||||
} else {
|
||||
if device.netplay.as_ref().unwrap().player_number as usize == channel {
|
||||
let local_input = ui::input::get(&mut device.ui, 0);
|
||||
let local_input = ui::input::get(&device.ui, 0);
|
||||
netplay::send_input(device.netplay.as_ref().unwrap(), local_input);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -163,9 +163,9 @@ pub fn pak_switch_event(device: &mut device::Device) {
|
||||
if channel.change_pak != PakType::None {
|
||||
//stop rumble if it is on
|
||||
if device.netplay.is_none() {
|
||||
device::ui::input::set_rumble(&mut device.ui, i, 0);
|
||||
device::ui::input::set_rumble(&device.ui, i, 0);
|
||||
} else if device.netplay.as_ref().unwrap().player_number as usize == i {
|
||||
device::ui::input::set_rumble(&mut device.ui, 0, 0);
|
||||
device::ui::input::set_rumble(&device.ui, 0, 0);
|
||||
}
|
||||
|
||||
let new_pak_type = match channel.change_pak {
|
||||
@@ -202,7 +202,7 @@ pub fn pak_switch_event(device: &mut device::Device) {
|
||||
pak_type: new_pak_type,
|
||||
});
|
||||
}
|
||||
ui::audio::play_pak_switch(&mut device.ui, new_pak_type);
|
||||
ui::audio::play_pak_switch(&device.ui, new_pak_type);
|
||||
channel.change_pak = PakType::None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ pub enum CartType {
|
||||
}
|
||||
|
||||
fn write_mbc1(
|
||||
pif_ram: &mut [u8],
|
||||
pif_ram: &[u8],
|
||||
cart: &mut device::controller::gbcart::GbCart,
|
||||
address: u16,
|
||||
data: usize,
|
||||
@@ -66,7 +66,7 @@ fn write_mbc1(
|
||||
|
||||
fn read_mbc1(
|
||||
pif_ram: &mut [u8],
|
||||
cart: &mut device::controller::gbcart::GbCart,
|
||||
cart: &device::controller::gbcart::GbCart,
|
||||
address: u16,
|
||||
data: usize,
|
||||
size: usize,
|
||||
@@ -100,7 +100,7 @@ fn read_mbc1(
|
||||
}
|
||||
|
||||
fn write_mbc3(
|
||||
pif_ram: &mut [u8],
|
||||
pif_ram: &[u8],
|
||||
cart: &mut device::controller::gbcart::GbCart,
|
||||
address: u16,
|
||||
data: usize,
|
||||
@@ -155,13 +155,13 @@ fn write_mbc3(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panic!("Unsupported write address {:x}", address);
|
||||
panic!("Unsupported write address {address:x}");
|
||||
}
|
||||
}
|
||||
|
||||
fn read_mbc3(
|
||||
pif_ram: &mut [u8],
|
||||
cart: &mut device::controller::gbcart::GbCart,
|
||||
cart: &device::controller::gbcart::GbCart,
|
||||
address: u16,
|
||||
data: usize,
|
||||
size: usize,
|
||||
@@ -208,12 +208,12 @@ fn read_mbc3(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panic!("Unsupported read address {:x}", address);
|
||||
panic!("Unsupported read address {address:x}");
|
||||
}
|
||||
}
|
||||
|
||||
fn write_mbc5(
|
||||
pif_ram: &mut [u8],
|
||||
pif_ram: &[u8],
|
||||
cart: &mut device::controller::gbcart::GbCart,
|
||||
address: u16,
|
||||
data: usize,
|
||||
@@ -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,13 +241,13 @@ 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}");
|
||||
}
|
||||
}
|
||||
|
||||
fn read_mbc5(
|
||||
pif_ram: &mut [u8],
|
||||
cart: &mut device::controller::gbcart::GbCart,
|
||||
cart: &device::controller::gbcart::GbCart,
|
||||
address: u16,
|
||||
data: usize,
|
||||
size: usize,
|
||||
@@ -272,13 +272,13 @@ 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}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(
|
||||
pif_ram: &mut [u8],
|
||||
cart: &mut device::controller::gbcart::GbCart,
|
||||
cart: &device::controller::gbcart::GbCart,
|
||||
address: u16,
|
||||
data: usize,
|
||||
size: usize,
|
||||
@@ -299,7 +299,7 @@ pub fn read(
|
||||
}
|
||||
|
||||
pub fn write(
|
||||
pif_ram: &mut [u8],
|
||||
pif_ram: &[u8],
|
||||
cart: &mut device::controller::gbcart::GbCart,
|
||||
address: u16,
|
||||
data: usize,
|
||||
@@ -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}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ pub fn write(device: &mut device::Device, channel: usize, address: u16, data: us
|
||||
if address == 0xc000 {
|
||||
let rumble = device.pif.ram[data + size - 1];
|
||||
if device.netplay.is_none() {
|
||||
device::ui::input::set_rumble(&mut device.ui, channel, rumble);
|
||||
device::ui::input::set_rumble(&device.ui, channel, rumble);
|
||||
} else if device.netplay.as_ref().unwrap().player_number as usize == channel {
|
||||
device::ui::input::set_rumble(&mut device.ui, 0, rumble);
|
||||
device::ui::input::set_rumble(&device.ui, 0, rumble);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,14 +54,14 @@ pub fn read(device: &mut device::Device, channel: usize, address: u16, data: usi
|
||||
0xC..=0xF => {
|
||||
device::controller::gbcart::read(
|
||||
&mut device.pif.ram,
|
||||
&mut pak.cart,
|
||||
&pak.cart,
|
||||
0x4000 * pak.bank + (address & 0x7fff) - 0x4000,
|
||||
data,
|
||||
size,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
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;
|
||||
}
|
||||
@@ -119,7 +119,7 @@ pub fn write(device: &mut device::Device, channel: usize, address: u16, data: us
|
||||
}
|
||||
0xC..=0xF => {
|
||||
device::controller::gbcart::write(
|
||||
&mut device.pif.ram,
|
||||
&device.pif.ram,
|
||||
&mut pak.cart,
|
||||
0x4000 * pak.bank + (address & 0x7fff) - 0x4000,
|
||||
data,
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -36,9 +36,9 @@ pub fn create_event_at(device: &mut device::Device, name: usize, when: u64) {
|
||||
set_next_event(device);
|
||||
}
|
||||
|
||||
pub fn get_event(device: &mut device::Device, name: usize) -> Option<&mut Event> {
|
||||
pub fn get_event(device: &device::Device, name: usize) -> Option<&Event> {
|
||||
if device.cpu.events[name].enabled {
|
||||
return Some(&mut device.cpu.events[name]);
|
||||
return Some(&device.cpu.events[name]);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@ pub enum AccessSize {
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Memory {
|
||||
#[serde(skip, default = "savestates::default_memory_read")]
|
||||
pub fast_read: [fn(&mut device::Device, u64, AccessSize) -> u32; 0x2000], // fast_read is used for lookups that try to detect idle loops
|
||||
#[serde(skip, default = "savestates::default_memory_read_fast")]
|
||||
pub fast_read: [fn(&device::Device, u64, AccessSize) -> u32; 0x2000], // fast_read is used for lookups that try to detect idle loops
|
||||
#[serde(skip, default = "savestates::default_memory_read")]
|
||||
pub memory_map_read: [fn(&mut device::Device, u64, AccessSize) -> u32; 0x2000],
|
||||
#[serde(skip, default = "savestates::default_memory_write")]
|
||||
|
||||
+1
-2
@@ -1,4 +1,4 @@
|
||||
use crate::{device, ui};
|
||||
use crate::device;
|
||||
use rand_chacha::rand_core::RngCore;
|
||||
|
||||
const PI_DRAM_ADDR_REG: u32 = 0;
|
||||
@@ -83,7 +83,6 @@ fn dma_write(device: &mut device::Device) {
|
||||
let cart_addr = device.pi.regs[PI_CART_ADDR_REG as usize] & !1;
|
||||
let dram_addr = device.pi.regs[PI_DRAM_ADDR_REG as usize] & 0xFFFFFE;
|
||||
let mut length = (device.pi.regs[PI_WR_LEN_REG as usize] & 0xFFFFFF) + 1;
|
||||
ui::video::check_framebuffers(dram_addr);
|
||||
|
||||
/* PI seems to treat the first 128 bytes differently, see https://n64brew.dev/wiki/Peripheral_Interface#Unaligned_DMA_transfer */
|
||||
if length >= 0x7f && (length & 1) != 0 {
|
||||
|
||||
+22
-9
@@ -226,12 +226,29 @@ 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)
|
||||
}
|
||||
|
||||
fn get_default_handler(device: &device::Device) -> device::controller::PakHandler {
|
||||
if device.ui.game_id == "NCT" {
|
||||
// Chameleon Twist does not support the mempak
|
||||
device::controller::PakHandler {
|
||||
read: device::controller::rumble::read,
|
||||
write: device::controller::rumble::write,
|
||||
pak_type: device::controller::PakType::RumblePak,
|
||||
}
|
||||
} else {
|
||||
device::controller::PakHandler {
|
||||
read: device::controller::mempak::read,
|
||||
write: device::controller::mempak::write,
|
||||
pak_type: device::controller::PakType::MemPak,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
if device.cart.pal {
|
||||
device.pif.rom = rom::PAL_PIF_ROM;
|
||||
@@ -241,11 +258,7 @@ pub fn init(device: &mut device::Device) {
|
||||
device.pif.ram[0x26] = device.cart.cic_seed;
|
||||
device.pif.ram[0x27] = device.cart.cic_seed;
|
||||
|
||||
let mempak_handler = device::controller::PakHandler {
|
||||
read: device::controller::mempak::read,
|
||||
write: device::controller::mempak::write,
|
||||
pak_type: device::controller::PakType::MemPak,
|
||||
};
|
||||
let default_handler = get_default_handler(device);
|
||||
let tpak_handler = device::controller::PakHandler {
|
||||
read: device::controller::transferpak::read,
|
||||
write: device::controller::transferpak::write,
|
||||
@@ -258,16 +271,16 @@ pub fn init(device: &mut device::Device) {
|
||||
if device.netplay.is_none() {
|
||||
if device.ui.config.input.controller_enabled[i] {
|
||||
if device.transferpaks[i].cart.rom.is_empty() {
|
||||
device.pif.channels[i].pak_handler = Some(mempak_handler);
|
||||
device.pif.channels[i].pak_handler = Some(default_handler);
|
||||
} else {
|
||||
device.pif.channels[i].pak_handler = Some(tpak_handler);
|
||||
}
|
||||
}
|
||||
} else if device.netplay.as_ref().unwrap().player_data[i].reg_id != 0 {
|
||||
device.pif.channels[i].pak_handler = Some(mempak_handler);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-4
@@ -1,5 +1,4 @@
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
use std::alloc::{Layout, alloc_zeroed};
|
||||
|
||||
//const RDRAM_CONFIG_REG: u32 = 0;
|
||||
@@ -24,7 +23,7 @@ pub struct Rdram {
|
||||
}
|
||||
|
||||
pub fn read_mem_fast(
|
||||
device: &mut device::Device,
|
||||
device: &device::Device,
|
||||
address: u64,
|
||||
_access_size: device::memory::AccessSize,
|
||||
) -> u32 {
|
||||
@@ -47,7 +46,6 @@ pub fn read_mem(
|
||||
);
|
||||
let masked_address = address as usize & RDRAM_MASK;
|
||||
if address < device.rdram.size as u64 {
|
||||
ui::video::check_framebuffers(masked_address as u32);
|
||||
u32::from_ne_bytes(
|
||||
device.rdram.mem[masked_address..masked_address + 4]
|
||||
.try_into()
|
||||
@@ -60,7 +58,6 @@ pub fn read_mem(
|
||||
|
||||
pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
if address < device.rdram.size as u64 {
|
||||
ui::video::check_framebuffers(address as u32);
|
||||
let mut data = u32::from_ne_bytes(
|
||||
device.rdram.mem[address as usize..(address + 4) as usize]
|
||||
.try_into()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
|
||||
const SP_MEM_ADDR_REG: u32 = 0;
|
||||
const SP_DRAM_ADDR_REG: u32 = 1;
|
||||
@@ -89,7 +88,7 @@ pub struct Rsp {
|
||||
}
|
||||
|
||||
pub fn read_mem_fast(
|
||||
device: &mut device::Device,
|
||||
device: &device::Device,
|
||||
address: u64,
|
||||
_access_size: device::memory::AccessSize,
|
||||
) -> u32 {
|
||||
@@ -147,7 +146,6 @@ fn do_dma(device: &mut device::Device, dma: RspDma) {
|
||||
let mut dram_addr = dma.dramaddr & 0xfffff8;
|
||||
let offset = dma.memaddr & 0x1000;
|
||||
|
||||
ui::video::check_framebuffers(dram_addr);
|
||||
if dma.dir == DmaDir::Read {
|
||||
let mut j = 0;
|
||||
while j < count {
|
||||
|
||||
@@ -317,10 +317,9 @@ pub fn lw(device: &mut device::Device, opcode: u32) {
|
||||
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
|
||||
|
||||
let mut w = [0; 4];
|
||||
w[0] = device.rsp.mem[address as usize & 0xFFF];
|
||||
w[1] = device.rsp.mem[(address as usize + 1) & 0xFFF];
|
||||
w[2] = device.rsp.mem[(address as usize + 2) & 0xFFF];
|
||||
w[3] = device.rsp.mem[(address as usize + 3) & 0xFFF];
|
||||
for (i, item) in w.iter_mut().enumerate() {
|
||||
*item = device.rsp.mem[(address as usize + i) & 0xFFF];
|
||||
}
|
||||
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] =
|
||||
((w[0] as u32) << 24) | ((w[1] as u32) << 16) | ((w[2] as u32) << 8) | (w[3] as u32)
|
||||
@@ -349,10 +348,9 @@ pub fn lwu(device: &mut device::Device, opcode: u32) {
|
||||
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
|
||||
|
||||
let mut w = [0; 4];
|
||||
w[0] = device.rsp.mem[address as usize & 0xFFF];
|
||||
w[1] = device.rsp.mem[(address as usize + 1) & 0xFFF];
|
||||
w[2] = device.rsp.mem[(address as usize + 2) & 0xFFF];
|
||||
w[3] = device.rsp.mem[(address as usize + 3) & 0xFFF];
|
||||
for (i, item) in w.iter_mut().enumerate() {
|
||||
*item = device.rsp.mem[(address as usize + i) & 0xFFF];
|
||||
}
|
||||
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] =
|
||||
((w[0] as u32) << 24) | ((w[1] as u32) << 16) | ((w[2] as u32) << 8) | (w[3] as u32)
|
||||
@@ -378,14 +376,10 @@ pub fn sw(device: &mut device::Device, opcode: u32) {
|
||||
let address =
|
||||
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
|
||||
|
||||
device.rsp.mem[address as usize & 0xFFF] =
|
||||
(device.rsp.cpu.gpr[rt(opcode) as usize] >> 24) as u8;
|
||||
device.rsp.mem[(address as usize + 1) & 0xFFF] =
|
||||
(device.rsp.cpu.gpr[rt(opcode) as usize] >> 16) as u8;
|
||||
device.rsp.mem[(address as usize + 2) & 0xFFF] =
|
||||
(device.rsp.cpu.gpr[rt(opcode) as usize] >> 8) as u8;
|
||||
device.rsp.mem[(address as usize + 3) & 0xFFF] =
|
||||
(device.rsp.cpu.gpr[rt(opcode) as usize]) as u8;
|
||||
for i in 0..4 {
|
||||
device.rsp.mem[(address as usize + i) & 0xFFF] =
|
||||
(device.rsp.cpu.gpr[rt(opcode) as usize] >> ((3 - i) * 8)) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sll(device: &mut device::Device, opcode: u32) {
|
||||
@@ -606,19 +600,19 @@ pub fn mfc2(device: &mut device::Device, opcode: u32) {
|
||||
pub fn cfc2(device: &mut device::Device, opcode: u32) {
|
||||
let hi;
|
||||
let lo;
|
||||
let mut zero = unsafe { _mm_setzero_si128() };
|
||||
let zero = unsafe { _mm_setzero_si128() };
|
||||
match rd(opcode) & 3 {
|
||||
0x00 => {
|
||||
hi = &mut device.rsp.cpu.vcoh;
|
||||
lo = &mut device.rsp.cpu.vcol;
|
||||
hi = &device.rsp.cpu.vcoh;
|
||||
lo = &device.rsp.cpu.vcol;
|
||||
}
|
||||
0x01 => {
|
||||
hi = &mut device.rsp.cpu.vcch;
|
||||
lo = &mut device.rsp.cpu.vccl;
|
||||
hi = &device.rsp.cpu.vcch;
|
||||
lo = &device.rsp.cpu.vccl;
|
||||
}
|
||||
0x02 | 0x03 => {
|
||||
hi = &mut zero;
|
||||
lo = &mut device.rsp.cpu.vce;
|
||||
hi = &zero;
|
||||
lo = &device.rsp.cpu.vce;
|
||||
}
|
||||
_ => {
|
||||
panic!("unknown cfc2")
|
||||
|
||||
+2
-2
@@ -146,7 +146,7 @@ pub fn probe(device: &mut device::Device) {
|
||||
}
|
||||
|
||||
fn tlb_unmap(device: &mut device::Device, index: u64) {
|
||||
let e = &mut device.cpu.cop0.tlb_entries[index as usize];
|
||||
let e = &device.cpu.cop0.tlb_entries[index as usize];
|
||||
|
||||
if e.v_even != 0 {
|
||||
let mut i = e.start_even;
|
||||
@@ -184,7 +184,7 @@ fn tlb_unmap(device: &mut device::Device, index: u64) {
|
||||
}
|
||||
|
||||
fn tlb_map(device: &mut device::Device, index: u64) {
|
||||
let e = &mut device.cpu.cop0.tlb_entries[index as usize];
|
||||
let e = &device.cpu.cop0.tlb_entries[index as usize];
|
||||
|
||||
if e.v_even != 0
|
||||
&& e.start_even < e.end_even
|
||||
|
||||
+10
-2
@@ -1,7 +1,7 @@
|
||||
use crate::device;
|
||||
|
||||
pub fn read_mem(
|
||||
_device: &mut device::Device,
|
||||
pub fn read_mem_fast(
|
||||
_device: &device::Device,
|
||||
address: u64,
|
||||
_access_size: device::memory::AccessSize,
|
||||
) -> u32 {
|
||||
@@ -9,4 +9,12 @@ pub fn read_mem(
|
||||
value | (value << 16)
|
||||
}
|
||||
|
||||
pub fn read_mem(
|
||||
device: &mut device::Device,
|
||||
address: u64,
|
||||
access_size: device::memory::AccessSize,
|
||||
) -> u32 {
|
||||
read_mem_fast(device, address, access_size)
|
||||
}
|
||||
|
||||
pub fn write_mem(_device: &mut device::Device, _address: u64, _value: u32, _mask: u32) {}
|
||||
|
||||
+5
-6
@@ -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;
|
||||
|
||||
@@ -86,7 +85,8 @@ fn set_current_line(device: &mut device::Device) {
|
||||
}
|
||||
/* update current field */
|
||||
device.vi.regs[VI_CURRENT_REG as usize] =
|
||||
(device.vi.regs[VI_CURRENT_REG as usize] & !1) | device.vi.field
|
||||
(device.vi.regs[VI_CURRENT_REG as usize] & !1) | device.vi.field;
|
||||
ui::video::set_register(VI_CURRENT_REG, device.vi.regs[VI_CURRENT_REG as usize])
|
||||
}
|
||||
|
||||
pub fn read_regs(
|
||||
@@ -177,8 +177,7 @@ pub fn init(device: &mut device::Device) {
|
||||
|
||||
fn speed_limiter(device: &mut device::Device) {
|
||||
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 {
|
||||
|
||||
+16
-67
@@ -73,10 +73,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 +81,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 +100,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;
|
||||
}
|
||||
@@ -151,63 +140,23 @@ async fn main() {
|
||||
}
|
||||
} else if args.game.is_some() {
|
||||
let file_path = std::path::Path::new(args.game.as_ref().unwrap());
|
||||
let rom_contents = device::get_rom_contents(file_path);
|
||||
if rom_contents.is_empty() {
|
||||
if let Some(rom_contents) = device::get_rom_contents(file_path) {
|
||||
let handle = std::thread::Builder::new()
|
||||
.name("n64".to_string())
|
||||
.stack_size(env!("N64_STACK_SIZE").parse().unwrap())
|
||||
.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);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
handle.join().unwrap();
|
||||
} else {
|
||||
println!("Could not read rom file");
|
||||
return;
|
||||
}
|
||||
let handle = std::thread::Builder::new()
|
||||
.name("n64".to_string())
|
||||
.stack_size(env!("N64_STACK_SIZE").parse().unwrap())
|
||||
.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);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
handle.join().unwrap();
|
||||
} else {
|
||||
let options = eframe::NativeOptions {
|
||||
wgpu_options: eframe::egui_wgpu::WgpuConfiguration {
|
||||
wgpu_setup: eframe::egui_wgpu::WgpuSetup::CreateNew(
|
||||
eframe::egui_wgpu::WgpuSetupCreateNew {
|
||||
instance_descriptor: eframe::egui_wgpu::wgpu::InstanceDescriptor {
|
||||
backends: eframe::wgpu::Backends::GL,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
+4
-9
@@ -144,7 +144,7 @@ pub fn get_input(device: &mut device::Device, channel: usize) -> ui::input::Inpu
|
||||
let timeout = std::time::Instant::now() + std::time::Duration::from_secs(10);
|
||||
let mut request_timer = std::time::Instant::now() - std::time::Duration::from_millis(5);
|
||||
while input.is_none() {
|
||||
process_incoming(netplay, &mut device.ui); // we execute process_incoming before request_input so that we send an accurate buffer count
|
||||
process_incoming(netplay, &device.ui); // we execute process_incoming before request_input so that we send an accurate buffer count
|
||||
if std::time::Instant::now() > request_timer {
|
||||
// sends a request packet every 5ms
|
||||
request_input(netplay, channel);
|
||||
@@ -155,7 +155,7 @@ pub fn get_input(device: &mut device::Device, channel: usize) -> ui::input::Inpu
|
||||
.remove(&netplay.player_data[channel].count);
|
||||
|
||||
if std::time::Instant::now() > timeout {
|
||||
ui::audio::play_netplay_audio(&mut device.ui, NETPLAY_ERROR_LOST_CONNECTION);
|
||||
ui::audio::play_netplay_audio(&device.ui, NETPLAY_ERROR_LOST_CONNECTION);
|
||||
input = Some(InputEvent {
|
||||
input: 0,
|
||||
plugin: 0,
|
||||
@@ -186,7 +186,7 @@ fn request_input(netplay: &Netplay, channel: usize) {
|
||||
netplay.udp_socket.send(&request).unwrap();
|
||||
}
|
||||
|
||||
fn process_incoming(netplay: &mut Netplay, ui: &mut ui::Ui) {
|
||||
fn process_incoming(netplay: &mut Netplay, ui: &ui::Ui) {
|
||||
let mut buf: [u8; 1024] = [0; 1024];
|
||||
while let Ok(_incoming) = netplay.udp_socket.recv(&mut buf) {
|
||||
match buf[0] {
|
||||
@@ -246,12 +246,7 @@ fn process_incoming(netplay: &mut Netplay, ui: &mut 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))
|
||||
|
||||
+112
-104
@@ -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>
|
||||
@@ -70,7 +70,8 @@ where
|
||||
}
|
||||
|
||||
pub fn create_savestate(device: &device::Device) {
|
||||
ui::video::full_sync();
|
||||
let mut rdp_state: Vec<u8> = vec![0; ui::video::state_size()];
|
||||
ui::video::save_state(rdp_state.as_mut_ptr());
|
||||
|
||||
let data: &[(&[u8], &str)] = &[
|
||||
(&postcard::to_stdvec(device).unwrap(), "device"),
|
||||
@@ -78,6 +79,7 @@ pub fn create_savestate(device: &device::Device) {
|
||||
&postcard::to_stdvec(&device.ui.storage.saves).unwrap(),
|
||||
"saves",
|
||||
),
|
||||
(&rdp_state, "rdp_state"),
|
||||
];
|
||||
let compressed_file = ui::storage::compress_file(data);
|
||||
std::fs::write(
|
||||
@@ -85,119 +87,120 @@ 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(&mut 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 state: device::Device = postcard::from_bytes(&device_bytes).unwrap();
|
||||
let savestate = std::fs::read(&device.ui.storage.paths.savestate_file_path);
|
||||
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();
|
||||
|
||||
device.ui.storage.saves = postcard::from_bytes(&save_bytes).unwrap();
|
||||
device.cpu = state.cpu;
|
||||
device.pif = state.pif;
|
||||
|
||||
device.cpu = state.cpu;
|
||||
device.pif = state.pif;
|
||||
let rom = device.cart.rom.clone();
|
||||
device.cart = state.cart;
|
||||
device.cart.rom = rom;
|
||||
|
||||
let rom = device.cart.rom.clone();
|
||||
device.cart = state.cart;
|
||||
device.cart.rom = rom;
|
||||
device.memory = state.memory;
|
||||
device.rsp = state.rsp;
|
||||
device.rdp = state.rdp;
|
||||
device.rdram = state.rdram;
|
||||
device.mi = state.mi;
|
||||
device.pi = state.pi;
|
||||
device.vi = state.vi;
|
||||
device.ai = state.ai;
|
||||
device.si = state.si;
|
||||
device.ri = state.ri;
|
||||
device.vru = state.vru;
|
||||
|
||||
device.memory = state.memory;
|
||||
device.rsp = state.rsp;
|
||||
device.rdp = state.rdp;
|
||||
device.rdram = state.rdram;
|
||||
device.mi = state.mi;
|
||||
device.pi = state.pi;
|
||||
device.vi = state.vi;
|
||||
device.ai = state.ai;
|
||||
device.si = state.si;
|
||||
device.ri = state.ri;
|
||||
device.vru = state.vru;
|
||||
let mut tpak_rom = [vec![], vec![], vec![], vec![]];
|
||||
for (i, item) in tpak_rom.iter_mut().enumerate() {
|
||||
*item = device.transferpaks[i].cart.rom.clone();
|
||||
}
|
||||
device.transferpaks = state.transferpaks;
|
||||
for (i, item) in tpak_rom.iter().enumerate() {
|
||||
device.transferpaks[i].cart.rom = item.clone();
|
||||
}
|
||||
|
||||
let mut tpak_rom = [vec![], vec![], vec![], vec![]];
|
||||
for (i, item) in tpak_rom.iter_mut().enumerate() {
|
||||
*item = device.transferpaks[i].cart.rom.clone();
|
||||
}
|
||||
device.transferpaks = state.transferpaks;
|
||||
for (i, item) in tpak_rom.iter().enumerate() {
|
||||
device.transferpaks[i].cart.rom = item.clone();
|
||||
}
|
||||
device::memory::init(device);
|
||||
device::vi::set_expected_refresh_rate(device);
|
||||
device::cpu::map_instructions(device);
|
||||
device::cop0::map_instructions(device);
|
||||
device::cop1::map_instructions(device);
|
||||
device::cop2::map_instructions(device);
|
||||
device::rsp_cpu::map_instructions(device);
|
||||
|
||||
device::memory::init(device);
|
||||
device::vi::set_expected_refresh_rate(device);
|
||||
device::cpu::map_instructions(device);
|
||||
device::cop0::map_instructions(device);
|
||||
device::cop1::map_instructions(device);
|
||||
device::cop2::map_instructions(device);
|
||||
device::rsp_cpu::map_instructions(device);
|
||||
let mut mem_addr = 0x1000;
|
||||
while mem_addr < 0x2000 {
|
||||
let data =
|
||||
u32::from_be_bytes(device.rsp.mem[mem_addr..mem_addr + 4].try_into().unwrap());
|
||||
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].func =
|
||||
device::rsp_cpu::decode_opcode(device, data);
|
||||
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].opcode = data;
|
||||
mem_addr += 4;
|
||||
}
|
||||
|
||||
let mut mem_addr = 0x1000;
|
||||
while mem_addr < 0x2000 {
|
||||
let data =
|
||||
u32::from_be_bytes(device.rsp.mem[mem_addr..mem_addr + 4].try_into().unwrap());
|
||||
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].func =
|
||||
device::rsp_cpu::decode_opcode(device, data);
|
||||
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].opcode = data;
|
||||
mem_addr += 4;
|
||||
}
|
||||
|
||||
for line_index in 0..512 {
|
||||
device.memory.icache[line_index].instruction[0] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[0]);
|
||||
device.memory.icache[line_index].instruction[1] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[1]);
|
||||
device.memory.icache[line_index].instruction[2] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[2]);
|
||||
device.memory.icache[line_index].instruction[3] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[3]);
|
||||
device.memory.icache[line_index].instruction[4] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[4]);
|
||||
device.memory.icache[line_index].instruction[5] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[5]);
|
||||
device.memory.icache[line_index].instruction[6] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[6]);
|
||||
device.memory.icache[line_index].instruction[7] =
|
||||
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[7]);
|
||||
}
|
||||
|
||||
device::pif::connect_pif_channels(device);
|
||||
for i in 0..4 {
|
||||
if device.pif.channels[i].pak_handler.is_some() {
|
||||
if device.pif.channels[i].pak_handler.unwrap().pak_type
|
||||
== device::controller::PakType::RumblePak
|
||||
{
|
||||
let rumblepak_handler = device::controller::PakHandler {
|
||||
read: device::controller::rumble::read,
|
||||
write: device::controller::rumble::write,
|
||||
pak_type: device::controller::PakType::RumblePak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(rumblepak_handler);
|
||||
} else if device.pif.channels[i].pak_handler.unwrap().pak_type
|
||||
== device::controller::PakType::MemPak
|
||||
{
|
||||
let mempak_handler = device::controller::PakHandler {
|
||||
read: device::controller::mempak::read,
|
||||
write: device::controller::mempak::write,
|
||||
pak_type: device::controller::PakType::MemPak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(mempak_handler);
|
||||
} else if device.pif.channels[i].pak_handler.unwrap().pak_type
|
||||
== device::controller::PakType::TransferPak
|
||||
{
|
||||
let tpak_handler = device::controller::PakHandler {
|
||||
read: device::controller::transferpak::read,
|
||||
write: device::controller::transferpak::write,
|
||||
pak_type: device::controller::PakType::TransferPak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(tpak_handler);
|
||||
for line_index in 0..512 {
|
||||
for i in 0..8 {
|
||||
device.memory.icache[line_index].instruction[i] = device::cpu::decode_opcode(
|
||||
device,
|
||||
device.memory.icache[line_index].words[i],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui::audio::close(&mut device.ui);
|
||||
ui::audio::init(&mut device.ui, device.ai.freq);
|
||||
ui::video::load_state(device);
|
||||
device::pif::connect_pif_channels(device);
|
||||
for i in 0..4 {
|
||||
if device.pif.channels[i].pak_handler.is_some() {
|
||||
if device.pif.channels[i].pak_handler.unwrap().pak_type
|
||||
== device::controller::PakType::RumblePak
|
||||
{
|
||||
let rumblepak_handler = device::controller::PakHandler {
|
||||
read: device::controller::rumble::read,
|
||||
write: device::controller::rumble::write,
|
||||
pak_type: device::controller::PakType::RumblePak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(rumblepak_handler);
|
||||
} else if device.pif.channels[i].pak_handler.unwrap().pak_type
|
||||
== device::controller::PakType::MemPak
|
||||
{
|
||||
let mempak_handler = device::controller::PakHandler {
|
||||
read: device::controller::mempak::read,
|
||||
write: device::controller::mempak::write,
|
||||
pak_type: device::controller::PakType::MemPak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(mempak_handler);
|
||||
} else if device.pif.channels[i].pak_handler.unwrap().pak_type
|
||||
== device::controller::PakType::TransferPak
|
||||
{
|
||||
let tpak_handler = device::controller::PakHandler {
|
||||
read: device::controller::transferpak::read,
|
||||
write: device::controller::transferpak::write,
|
||||
pak_type: device::controller::PakType::TransferPak,
|
||||
};
|
||||
device.pif.channels[i].pak_handler = Some(tpak_handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,11 +219,16 @@ where
|
||||
[device::cop0::reserved; N]
|
||||
}
|
||||
|
||||
pub fn default_memory_read_fast()
|
||||
-> [fn(&device::Device, u64, device::memory::AccessSize) -> u32; 0x2000] {
|
||||
[device::unmapped::read_mem_fast; 0x2000]
|
||||
}
|
||||
|
||||
pub fn default_memory_read()
|
||||
-> [fn(&mut device::Device, u64, device::memory::AccessSize) -> u32; 0x2000] {
|
||||
[device::rdram::read_mem; 0x2000]
|
||||
[device::unmapped::read_mem; 0x2000]
|
||||
}
|
||||
|
||||
pub fn default_memory_write() -> [fn(&mut device::Device, u64, u32, u32); 0x2000] {
|
||||
[device::rdram::write_mem; 0x2000]
|
||||
[device::unmapped::write_mem; 0x2000]
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ pub mod audio;
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ pub struct Audio {
|
||||
pub event_audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
|
||||
pub audio_device: u32,
|
||||
pub event_audio: audio::EventAudio,
|
||||
pub gain: f32,
|
||||
}
|
||||
|
||||
pub struct Input {
|
||||
pub num_joysticks: i32,
|
||||
pub joysticks: *mut sdl3_sys::joystick::SDL_JoystickID,
|
||||
pub joysticks: Vec<sdl3_sys::joystick::SDL_JoystickID>,
|
||||
pub keyboard_state: *const bool,
|
||||
pub controllers: [input::Controllers; 4],
|
||||
}
|
||||
@@ -53,7 +53,6 @@ impl Drop for Ui {
|
||||
fn drop(&mut self) {
|
||||
if self.with_sdl {
|
||||
unsafe {
|
||||
sdl3_sys::stdinc::SDL_free(self.input.joysticks as *mut std::ffi::c_void);
|
||||
sdl3_sys::init::SDL_Quit();
|
||||
}
|
||||
}
|
||||
@@ -67,7 +66,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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,27 +76,23 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
impl Ui {
|
||||
fn construct_ui(num_joysticks: i32, joysticks: *mut u32, with_sdl: bool) -> Ui {
|
||||
fn construct_ui(joysticks: Vec<u32>, with_sdl: bool) -> Ui {
|
||||
let dirs = get_dirs();
|
||||
|
||||
Ui {
|
||||
@@ -125,7 +120,6 @@ impl Ui {
|
||||
},
|
||||
],
|
||||
keyboard_state: std::ptr::null_mut(),
|
||||
num_joysticks,
|
||||
joysticks,
|
||||
},
|
||||
storage: Storage {
|
||||
@@ -188,6 +182,7 @@ impl Ui {
|
||||
audio_stream: std::ptr::null_mut(),
|
||||
event_audio_stream: std::ptr::null_mut(),
|
||||
audio_device: 0,
|
||||
gain: 1.0,
|
||||
},
|
||||
video: Video {
|
||||
window: std::ptr::null_mut(),
|
||||
@@ -199,7 +194,7 @@ impl Ui {
|
||||
}
|
||||
|
||||
pub fn default() -> Ui {
|
||||
Self::construct_ui(0, std::ptr::null_mut(), false)
|
||||
Self::construct_ui(vec![], false)
|
||||
}
|
||||
|
||||
pub fn new() -> Ui {
|
||||
@@ -209,6 +204,11 @@ impl Ui {
|
||||
if joysticks.is_null() {
|
||||
panic!("Could not get joystick list");
|
||||
}
|
||||
Self::construct_ui(num_joysticks, joysticks, true)
|
||||
let mut joystick_vec = vec![];
|
||||
for i in 0..num_joysticks {
|
||||
joystick_vec.push(unsafe { *joysticks.add(i as usize) });
|
||||
}
|
||||
unsafe { sdl3_sys::stdinc::SDL_free(joysticks as *mut std::ffi::c_void) }
|
||||
Self::construct_ui(joystick_vec, true)
|
||||
}
|
||||
}
|
||||
|
||||
+32
-5
@@ -51,7 +51,8 @@ pub fn init(ui: &mut ui::Ui, frequency: u64) {
|
||||
return;
|
||||
}
|
||||
if !unsafe {
|
||||
sdl3_sys::audio::SDL_BindAudioStream(ui.audio.audio_device, ui.audio.audio_stream)
|
||||
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");
|
||||
}
|
||||
@@ -65,7 +66,11 @@ pub fn init(ui: &mut ui::Ui, frequency: u64) {
|
||||
ui.audio.event_audio_stream =
|
||||
unsafe { sdl3_sys::audio::SDL_CreateAudioStream(&wav_audio_spec, &dst) };
|
||||
if !unsafe {
|
||||
sdl3_sys::audio::SDL_BindAudioStream(ui.audio.audio_device, ui.audio.event_audio_stream)
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.event_audio_stream, ui.audio.gain)
|
||||
&& sdl3_sys::audio::SDL_BindAudioStream(
|
||||
ui.audio.audio_device,
|
||||
ui.audio.event_audio_stream,
|
||||
)
|
||||
} {
|
||||
panic!("Could not bind audio stream");
|
||||
}
|
||||
@@ -86,7 +91,29 @@ pub fn close(ui: &mut ui::Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play_netplay_audio(ui: &mut ui::Ui, error: u32) {
|
||||
pub fn lower_audio_volume(ui: &mut ui::Ui) {
|
||||
unsafe {
|
||||
ui.audio.gain = sdl3_sys::audio::SDL_GetAudioStreamGain(ui.audio.audio_stream) - 0.05;
|
||||
if ui.audio.gain < 0.0 {
|
||||
ui.audio.gain = 0.0;
|
||||
}
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain);
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.event_audio_stream, ui.audio.gain);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn raise_audio_volume(ui: &mut ui::Ui) {
|
||||
unsafe {
|
||||
ui.audio.gain = sdl3_sys::audio::SDL_GetAudioStreamGain(ui.audio.audio_stream) + 0.05;
|
||||
if ui.audio.gain > 2.0 {
|
||||
ui.audio.gain = 2.0;
|
||||
}
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain);
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.event_audio_stream, ui.audio.gain);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play_netplay_audio(ui: &ui::Ui, error: u32) {
|
||||
if ui.audio.event_audio_stream.is_null() {
|
||||
return;
|
||||
}
|
||||
@@ -118,7 +145,7 @@ pub fn play_netplay_audio(ui: &mut ui::Ui, error: u32) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play_pak_switch(ui: &mut ui::Ui, pak: device::controller::PakType) {
|
||||
pub fn play_pak_switch(ui: &ui::Ui, pak: device::controller::PakType) {
|
||||
if ui.audio.event_audio_stream.is_null() {
|
||||
return;
|
||||
}
|
||||
@@ -140,7 +167,7 @@ pub fn play_pak_switch(ui: &mut ui::Ui, pak: device::controller::PakType) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play_audio(device: &mut device::Device, dram_addr: usize, length: u64) {
|
||||
pub fn play_audio(device: &device::Device, dram_addr: usize, length: u64) {
|
||||
if device.ui.audio.audio_stream.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
+2
-2
@@ -80,8 +80,8 @@ 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;
|
||||
}
|
||||
|
||||
+433
-636
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
// 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 About inherits Page {
|
||||
callback wiki_button_clicked;
|
||||
callback discord_button_clicked;
|
||||
callback newversion_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;
|
||||
Text {
|
||||
text: AboutData.version;
|
||||
}
|
||||
}
|
||||
|
||||
if AboutData.has_update:HorizontalBox {
|
||||
alignment: center;
|
||||
Button {
|
||||
text: @tr("New version available! Click to download");
|
||||
primary: true;
|
||||
clicked => {
|
||||
newversion_button_clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { LocalGame, ControllerConfig, Netplay, 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";
|
||||
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 open_rom_button_clicked;
|
||||
callback saves_folder_button_clicked;
|
||||
callback input_profile_button_clicked;
|
||||
callback create_session_button_clicked;
|
||||
callback join_session_button_clicked;
|
||||
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 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;
|
||||
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" => "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();
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
if(side-bar.current-item == 3): Settings { }
|
||||
if(side-bar.current-item == 4): About {
|
||||
wiki_button_clicked => {
|
||||
wiki_button_clicked();
|
||||
}
|
||||
discord_button_clicked => {
|
||||
discord_button_clicked();
|
||||
}
|
||||
newversion_button_clicked => {
|
||||
newversion_button_clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,942 +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: Some(2),
|
||||
}),
|
||||
};
|
||||
let (mut socket, _response) =
|
||||
tungstenite::connect(&app.netplay.server.ip).expect("Can't connect");
|
||||
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() {
|
||||
let (mut sock, _response) =
|
||||
tungstenite::connect(&app.netplay.server.ip).expect("Can't connect");
|
||||
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);
|
||||
}
|
||||
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>) {
|
||||
let rom_contents = device::get_rom_contents(file.path());
|
||||
if !rom_contents.is_empty() {
|
||||
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 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();
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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 State {
|
||||
in-out property <bool> game_running;
|
||||
}
|
||||
|
||||
export component LocalGame inherits Page {
|
||||
callback open_rom_button_clicked;
|
||||
callback saves_folder_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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
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 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);
|
||||
|
||||
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 { }
|
||||
|
||||
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.password);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
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 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 Protected" },
|
||||
];
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { VerticalBox, HorizontalBox, Button, StandardButton } 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> 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 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;
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { LocalGame } from "localgame_page.slint";
|
||||
export { ControllerConfig } from "controller_page.slint";
|
||||
export { Netplay } from "netplay_page.slint";
|
||||
export { Settings } from "settings_page.slint";
|
||||
export { About } from "about_page.slint";
|
||||
@@ -0,0 +1,94 @@
|
||||
// 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 <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
-19
@@ -276,7 +276,7 @@ fn set_buttons_from_controller(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_rumble(ui: &mut ui::Ui, channel: usize, rumble: u8) {
|
||||
pub fn set_rumble(ui: &ui::Ui, channel: usize, rumble: u8) {
|
||||
if !ui.input.controllers[channel].rumble {
|
||||
return;
|
||||
}
|
||||
@@ -334,7 +334,35 @@ fn change_paks(
|
||||
pressed
|
||||
}
|
||||
|
||||
pub fn get(ui: &mut ui::Ui, channel: usize) -> InputData {
|
||||
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();
|
||||
let mut keys = 0;
|
||||
@@ -393,10 +421,10 @@ pub fn get(ui: &mut ui::Ui, channel: usize) -> InputData {
|
||||
}
|
||||
|
||||
pub fn assign_controller(ui: &mut ui::Ui, controller: i32, port: usize) {
|
||||
if controller < ui.input.num_joysticks {
|
||||
if controller < ui.input.joysticks.len() as i32 {
|
||||
let path = unsafe {
|
||||
std::ffi::CStr::from_ptr(sdl3_sys::joystick::SDL_GetJoystickPathForID(
|
||||
*(ui.input.joysticks.offset(controller as isize)),
|
||||
ui.input.joysticks[controller as usize],
|
||||
))
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
@@ -448,16 +476,14 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
|
||||
let mut open_joysticks: Vec<*mut sdl3_sys::joystick::SDL_Joystick> = Vec::new();
|
||||
let mut open_controllers: Vec<*mut sdl3_sys::gamepad::SDL_Gamepad> = Vec::new();
|
||||
|
||||
for offset in 0..ui.input.num_joysticks as isize {
|
||||
for joystick in ui.input.joysticks.iter() {
|
||||
if !dinput {
|
||||
let controller =
|
||||
unsafe { sdl3_sys::gamepad::SDL_OpenGamepad(*ui.input.joysticks.offset(offset)) };
|
||||
let controller = unsafe { sdl3_sys::gamepad::SDL_OpenGamepad(*joystick) };
|
||||
if !controller.is_null() {
|
||||
open_controllers.push(controller);
|
||||
}
|
||||
} else {
|
||||
let joystick =
|
||||
unsafe { sdl3_sys::joystick::SDL_OpenJoystick(*ui.input.joysticks.offset(offset)) };
|
||||
let joystick = unsafe { sdl3_sys::joystick::SDL_OpenJoystick(*joystick) };
|
||||
if !joystick.is_null() {
|
||||
open_joysticks.push(joystick);
|
||||
}
|
||||
@@ -482,8 +508,6 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
|
||||
if !unsafe { sdl3_sys::video::SDL_ShowWindow(window) } {
|
||||
panic!("Could not show window")
|
||||
}
|
||||
let font =
|
||||
rusttype::Font::try_from_bytes(include_bytes!("../../data/Roboto-Regular.ttf")).unwrap();
|
||||
|
||||
let key_labels: [(&str, usize); PROFILE_SIZE] = [
|
||||
("A", A_BUTTON),
|
||||
@@ -540,6 +564,10 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
|
||||
id: 0,
|
||||
axis: 0,
|
||||
};
|
||||
|
||||
let font =
|
||||
ab_glyph::FontRef::try_from_slice(include_bytes!("../../data/Roboto-Regular.ttf")).unwrap();
|
||||
|
||||
for (key, value) in key_labels.iter() {
|
||||
let mut event: sdl3_sys::events::SDL_Event = Default::default();
|
||||
while unsafe { sdl3_sys::events::SDL_PollEvent(&mut event) } {} // clear events
|
||||
@@ -859,24 +887,25 @@ pub fn init(ui: &mut ui::Ui) {
|
||||
panic!("Could not get keyboard state");
|
||||
}
|
||||
|
||||
let mut taken = [false; 4];
|
||||
for i in 0..4 {
|
||||
let controller_assignment = &ui.config.input.controller_assignment[i];
|
||||
if controller_assignment.is_some() && ui.config.input.controller_enabled[i] {
|
||||
let mut joystick_id = 0;
|
||||
let assigned_path = controller_assignment.as_ref().unwrap();
|
||||
|
||||
for offset in 0..ui.input.num_joysticks as isize {
|
||||
for joystick in ui.input.joysticks.iter() {
|
||||
let path = unsafe {
|
||||
std::ffi::CStr::from_ptr(sdl3_sys::joystick::SDL_GetJoystickPathForID(
|
||||
*(ui.input.joysticks.offset(offset)),
|
||||
*joystick,
|
||||
))
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
};
|
||||
if path == *assigned_path && !taken[offset as usize] {
|
||||
joystick_id = unsafe { *(ui.input.joysticks.offset(offset)) };
|
||||
taken[offset as usize] = true;
|
||||
if path == *assigned_path
|
||||
&& unsafe { sdl3_sys::joystick::SDL_GetJoystickFromID(*joystick) }.is_null()
|
||||
&& unsafe { sdl3_sys::gamepad::SDL_GetGamepadFromID(*joystick) }.is_null()
|
||||
{
|
||||
joystick_id = *joystick;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -888,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 =
|
||||
@@ -907,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 =
|
||||
|
||||
@@ -0,0 +1,911 @@
|
||||
#![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,
|
||||
};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use sha2::{Digest, Sha256};
|
||||
use slint::{ComponentHandle, Model};
|
||||
use tokio_tungstenite::tungstenite::Bytes;
|
||||
use tokio_tungstenite::tungstenite::protocol::Message;
|
||||
|
||||
const NETPLAY_VERSION: i32 = 17;
|
||||
const EMU_NAME: &str = "gopher64";
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
|
||||
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, Clone, Debug)]
|
||||
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>>,
|
||||
}
|
||||
|
||||
trait NetplayPages {
|
||||
fn set_server_names(&self, names: slint::ModelRc<slint::SharedString>);
|
||||
fn set_server_urls(&self, urls: slint::ModelRc<slint::SharedString>);
|
||||
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_rom_path(&self, rom_path: slint::SharedString);
|
||||
fn set_peer_addr(&self, peer_addr: slint::SharedString);
|
||||
fn refresh_sessions(&self, _server: slint::SharedString) {
|
||||
// Default implementation does nothing
|
||||
}
|
||||
}
|
||||
|
||||
impl NetplayPages for NetplayCreate {
|
||||
fn set_server_names(&self, names: slint::ModelRc<slint::SharedString>) {
|
||||
self.set_server_names(names);
|
||||
}
|
||||
fn set_server_urls(&self, urls: slint::ModelRc<slint::SharedString>) {
|
||||
self.set_server_urls(urls);
|
||||
}
|
||||
fn set_ping(&self, ping: slint::SharedString) {
|
||||
self.set_ping(ping);
|
||||
}
|
||||
fn set_game_name(&self, game_name: slint::SharedString) {
|
||||
self.set_game_name(game_name);
|
||||
}
|
||||
fn set_game_hash(&self, game_hash: slint::SharedString) {
|
||||
self.set_game_hash(game_hash);
|
||||
}
|
||||
fn set_rom_path(&self, rom_path: slint::SharedString) {
|
||||
self.set_rom_path(rom_path);
|
||||
}
|
||||
fn set_peer_addr(&self, peer_addr: slint::SharedString) {
|
||||
self.set_peer_addr(peer_addr);
|
||||
}
|
||||
}
|
||||
|
||||
impl NetplayPages for NetplayJoin {
|
||||
fn set_server_names(&self, names: slint::ModelRc<slint::SharedString>) {
|
||||
self.set_server_names(names);
|
||||
}
|
||||
fn set_server_urls(&self, urls: slint::ModelRc<slint::SharedString>) {
|
||||
self.set_server_urls(urls);
|
||||
}
|
||||
fn set_ping(&self, ping: slint::SharedString) {
|
||||
self.set_ping(ping);
|
||||
}
|
||||
fn refresh_sessions(&self, server: slint::SharedString) {
|
||||
self.invoke_refresh_session(server);
|
||||
}
|
||||
fn set_game_name(&self, game_name: slint::SharedString) {
|
||||
self.set_game_name(game_name);
|
||||
}
|
||||
fn set_game_hash(&self, game_hash: slint::SharedString) {
|
||||
self.set_game_hash(game_hash);
|
||||
}
|
||||
fn set_rom_path(&self, rom_path: slint::SharedString) {
|
||||
self.set_rom_path(rom_path);
|
||||
}
|
||||
fn set_peer_addr(&self, peer_addr: slint::SharedString) {
|
||||
self.set_peer_addr(peer_addr);
|
||||
}
|
||||
}
|
||||
|
||||
fn populate_server_names<T: ComponentHandle + NetplayPages + 'static>(weak: slint::Weak<T>) {
|
||||
let task = reqwest::get("https://m64p.s3.amazonaws.com/serverstest.json");
|
||||
tokio::spawn(async move {
|
||||
let mut local_servers: Vec<(String, String)> = vec![];
|
||||
|
||||
let broadcast_sock = tokio::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0))
|
||||
.await
|
||||
.unwrap();
|
||||
broadcast_sock.set_broadcast(true).unwrap();
|
||||
let data: [u8; 1] = [1];
|
||||
broadcast_sock
|
||||
.send_to(&data, (std::net::Ipv4Addr::BROADCAST, 45000))
|
||||
.await
|
||||
.unwrap();
|
||||
let mut buffer = [0; 1024];
|
||||
if let Ok(Ok(result)) = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(200),
|
||||
broadcast_sock.recv(&mut buffer),
|
||||
)
|
||||
.await
|
||||
{
|
||||
let data: std::collections::HashMap<String, String> =
|
||||
serde_json::from_slice(&buffer[..result]).unwrap();
|
||||
for server in data.iter() {
|
||||
local_servers.push((server.0.into(), server.1.into()));
|
||||
}
|
||||
}
|
||||
|
||||
let response = task.await;
|
||||
if let Ok(response) = response {
|
||||
let servers: std::collections::HashMap<String, String> = response.json().await.unwrap();
|
||||
|
||||
let weak2 = weak.clone();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
let server_names: slint::VecModel<slint::SharedString> = slint::VecModel::default();
|
||||
let server_urls: slint::VecModel<slint::SharedString> = slint::VecModel::default();
|
||||
for local_server in local_servers {
|
||||
server_names.push(local_server.0.into());
|
||||
server_urls.push(local_server.1.into());
|
||||
}
|
||||
for server in servers {
|
||||
server_names.push(server.0.into());
|
||||
server_urls.push(server.1.into());
|
||||
}
|
||||
update_ping(weak2, server_urls.row_data(0).unwrap().into());
|
||||
handle.refresh_sessions(server_urls.row_data(0).unwrap());
|
||||
let server_names_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
|
||||
std::rc::Rc::new(server_names);
|
||||
let server_urls_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
|
||||
std::rc::Rc::new(server_urls);
|
||||
handle.set_server_names(slint::ModelRc::from(server_names_model));
|
||||
handle.set_server_urls(slint::ModelRc::from(server_urls_model));
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn select_rom<T: ComponentHandle + NetplayPages + 'static>(weak: slint::Weak<T>) {
|
||||
let select_rom = rfd::AsyncFileDialog::new()
|
||||
.set_title("Select ROM")
|
||||
.pick_file();
|
||||
tokio::spawn(async move {
|
||||
if let Some(file) = select_rom.await {
|
||||
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);
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_game_name(game_name.into());
|
||||
handle.set_game_hash(hash.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 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();
|
||||
|
||||
handle.set_game_name("".into());
|
||||
handle.set_game_hash("".into());
|
||||
handle.set_rom_path("".into());
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn update_ping<T: ComponentHandle + NetplayPages + 'static>(
|
||||
weak: slint::Weak<T>,
|
||||
server_url: String,
|
||||
) {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_ping("Ping: Unknown".into());
|
||||
})
|
||||
.unwrap();
|
||||
tokio::spawn(async move {
|
||||
if let Ok(Ok((mut sock, _response))) = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(1),
|
||||
tokio_tungstenite::connect_async(server_url),
|
||||
)
|
||||
.await
|
||||
{
|
||||
sock.send(Message::Ping(Vec::new().into())).await.unwrap();
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
if let Some(Ok(_response)) = sock.next().await {
|
||||
let elapsed = start.elapsed();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_ping(format!("Ping: {:.0} ms", elapsed.as_millis()).into());
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
sock.close(None).await.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn setup_create_window(
|
||||
create_window: &NetplayCreate,
|
||||
overclock_setting: bool,
|
||||
fullscreen: bool,
|
||||
weak_app: slint::Weak<AppWindow>,
|
||||
) {
|
||||
let (netplay_read_sender, netplay_read_receiver): (
|
||||
tokio::sync::broadcast::Sender<NetplayMessage>,
|
||||
tokio::sync::broadcast::Receiver<NetplayMessage>,
|
||||
) = tokio::sync::broadcast::channel(5);
|
||||
|
||||
let (netplay_write_sender, netplay_write_receiver): (
|
||||
tokio::sync::broadcast::Sender<Option<NetplayMessage>>,
|
||||
tokio::sync::broadcast::Receiver<Option<NetplayMessage>>,
|
||||
) = tokio::sync::broadcast::channel(5);
|
||||
|
||||
populate_server_names(create_window.as_weak());
|
||||
let weak = create_window.as_weak();
|
||||
create_window.on_get_ping(move |server_url| {
|
||||
update_ping(weak.clone(), server_url.to_string());
|
||||
});
|
||||
let weak = create_window.as_weak();
|
||||
create_window.on_select_rom(move || {
|
||||
select_rom(weak.clone());
|
||||
});
|
||||
|
||||
let weak = create_window.as_weak();
|
||||
create_window.on_create_session(
|
||||
move |server_url, session_name, player_name, game_name, game_hash, password| {
|
||||
let _ = netplay_write_sender.send(None); // close current websocket if any
|
||||
manage_websocket(
|
||||
server_url.to_string(),
|
||||
netplay_read_sender.clone(),
|
||||
netplay_write_receiver.resubscribe(),
|
||||
weak.clone(),
|
||||
);
|
||||
|
||||
create_session(
|
||||
netplay_write_sender.clone(),
|
||||
netplay_read_receiver.resubscribe(),
|
||||
session_name.to_string(),
|
||||
player_name.to_string(),
|
||||
game_name.to_string(),
|
||||
game_hash.to_string(),
|
||||
password.to_string(),
|
||||
overclock_setting,
|
||||
fullscreen,
|
||||
weak_app.clone(),
|
||||
weak.clone(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
create_window.show().unwrap();
|
||||
}
|
||||
|
||||
fn manage_websocket<T: ComponentHandle + NetplayPages + 'static>(
|
||||
server_url: String,
|
||||
netplay_read_sender: tokio::sync::broadcast::Sender<NetplayMessage>,
|
||||
mut netplay_write_receiver: tokio::sync::broadcast::Receiver<Option<NetplayMessage>>,
|
||||
weak: slint::Weak<T>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
if let Ok(Ok((socket, _response))) = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
tokio_tungstenite::connect_async(server_url.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
match socket.get_ref() {
|
||||
tokio_tungstenite::MaybeTlsStream::Plain(stream) => {
|
||||
let addr = stream.peer_addr().unwrap().to_string();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_peer_addr(addr.into());
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
|
||||
let (mut write, mut read) = socket.split();
|
||||
tokio::spawn(async move {
|
||||
while let Some(Ok(response)) = read.next().await {
|
||||
if let Ok(message) = serde_json::from_slice(&response.into_data()) {
|
||||
let _ = netplay_read_sender.send(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
tokio::spawn(async move {
|
||||
while let Ok(Some(response)) = netplay_write_receiver.recv().await {
|
||||
write
|
||||
.send(Message::Binary(Bytes::from(
|
||||
serde_json::to_vec(&response).unwrap(),
|
||||
)))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
write.close().await.unwrap();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn show_netplay_error(message: String) {
|
||||
let message_dialog = NetplayDialog::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(message.into());
|
||||
message_dialog.show().unwrap();
|
||||
}
|
||||
|
||||
fn clear_sessions(handle: &NetplayJoin, message: Option<String>) {
|
||||
handle.set_sessions(slint::ModelRc::default());
|
||||
handle.set_ports(slint::ModelRc::default());
|
||||
handle.set_current_session(-1);
|
||||
if let Some(message) = message {
|
||||
show_netplay_error(message);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_sessions(
|
||||
netplay_write_sender: tokio::sync::broadcast::Sender<Option<NetplayMessage>>,
|
||||
mut netplay_read_receiver: tokio::sync::broadcast::Receiver<NetplayMessage>,
|
||||
weak: slint::Weak<NetplayJoin>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
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,
|
||||
};
|
||||
|
||||
netplay_write_sender.send(Some(request_rooms)).unwrap();
|
||||
|
||||
if let Ok(Ok(message)) = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
netplay_read_receiver.recv(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
if message.accept.unwrap() == 0 {
|
||||
if let Some(rooms) = message.rooms {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
let sessions_vec = slint::VecModel::default();
|
||||
let ports_vec = slint::VecModel::default();
|
||||
for room in rooms {
|
||||
let session_vec = slint::VecModel::default();
|
||||
session_vec.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(room.room_name.unwrap()),
|
||||
));
|
||||
session_vec.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(room.game_name.unwrap()),
|
||||
));
|
||||
session_vec.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(if room.protected.unwrap() {
|
||||
"True"
|
||||
} else {
|
||||
"False"
|
||||
}),
|
||||
));
|
||||
let session_model: std::rc::Rc<
|
||||
slint::VecModel<slint::StandardListViewItem>,
|
||||
> = std::rc::Rc::new(session_vec);
|
||||
sessions_vec.push(slint::ModelRc::from(session_model));
|
||||
ports_vec.push(room.port.unwrap());
|
||||
}
|
||||
let rooms_model: std::rc::Rc<
|
||||
slint::VecModel<slint::ModelRc<slint::StandardListViewItem>>,
|
||||
> = std::rc::Rc::new(sessions_vec);
|
||||
let ports_model: std::rc::Rc<slint::VecModel<i32>> =
|
||||
std::rc::Rc::new(ports_vec);
|
||||
handle.set_sessions(slint::ModelRc::from(rooms_model));
|
||||
handle.set_ports(slint::ModelRc::from(ports_model));
|
||||
handle.set_current_session(-1);
|
||||
})
|
||||
.unwrap();
|
||||
} else {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
clear_sessions(&handle, None);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
} else {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
clear_sessions(&handle, message.message);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
} else {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
clear_sessions(&handle, Some("Server did not respond".to_string()));
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn create_session(
|
||||
netplay_write_sender: tokio::sync::broadcast::Sender<Option<NetplayMessage>>,
|
||||
mut netplay_read_receiver: tokio::sync::broadcast::Receiver<NetplayMessage>,
|
||||
session_name: String,
|
||||
player_name: String,
|
||||
game_name: String,
|
||||
game_hash: String,
|
||||
password: String,
|
||||
overclock: bool,
|
||||
fullscreen: bool,
|
||||
weak_app: slint::Weak<AppWindow>,
|
||||
weak: slint::Weak<NetplayCreate>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
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());
|
||||
|
||||
let create_room = NetplayMessage {
|
||||
message_type: "request_create_room".to_string(),
|
||||
player_name: Some(player_name),
|
||||
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(session_name),
|
||||
password: Some(password),
|
||||
game_name: Some(game_name),
|
||||
md5: Some(game_hash),
|
||||
protected: None,
|
||||
port: None,
|
||||
features: Some(features),
|
||||
buffer_target: None,
|
||||
}),
|
||||
};
|
||||
|
||||
netplay_write_sender.send(Some(create_room)).unwrap();
|
||||
|
||||
if let Ok(Ok(message)) = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
netplay_read_receiver.recv(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
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();
|
||||
setup_wait_window(
|
||||
netplay_write_sender,
|
||||
netplay_read_receiver,
|
||||
session.room_name.as_ref().unwrap().into(),
|
||||
session.game_name.as_ref().unwrap().into(),
|
||||
handle.get_rom_path(),
|
||||
message.player_name.as_ref().unwrap().into(),
|
||||
session.port.unwrap(),
|
||||
true,
|
||||
fullscreen,
|
||||
overclock == "true",
|
||||
handle.get_peer_addr(),
|
||||
weak_app,
|
||||
);
|
||||
handle.window().hide().unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
} else {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_pending_session(false);
|
||||
if let Some(message) = message.message {
|
||||
show_netplay_error(message);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
} else {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_pending_session(false);
|
||||
show_netplay_error("Server did not respond".to_string());
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn join_session(
|
||||
netplay_write_sender: tokio::sync::broadcast::Sender<Option<NetplayMessage>>,
|
||||
mut netplay_read_receiver: tokio::sync::broadcast::Receiver<NetplayMessage>,
|
||||
player_name: String,
|
||||
game_hash: String,
|
||||
password: String,
|
||||
port: i32,
|
||||
fullscreen: bool,
|
||||
weak_app: slint::Weak<AppWindow>,
|
||||
weak: slint::Weak<NetplayJoin>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let join_room = NetplayMessage {
|
||||
message_type: "request_join_room".to_string(),
|
||||
player_name: Some(player_name),
|
||||
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(password),
|
||||
game_name: None,
|
||||
md5: Some(game_hash),
|
||||
protected: None,
|
||||
port: Some(port),
|
||||
features: None,
|
||||
buffer_target: None,
|
||||
}),
|
||||
};
|
||||
|
||||
netplay_write_sender.send(Some(join_room)).unwrap();
|
||||
|
||||
if let Ok(Ok(message)) = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
netplay_read_receiver.recv(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
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();
|
||||
setup_wait_window(
|
||||
netplay_write_sender,
|
||||
netplay_read_receiver,
|
||||
session.room_name.as_ref().unwrap().into(),
|
||||
session.game_name.as_ref().unwrap().into(),
|
||||
handle.get_rom_path(),
|
||||
message.player_name.as_ref().unwrap().into(),
|
||||
session.port.unwrap(),
|
||||
false,
|
||||
fullscreen,
|
||||
overclock == "true",
|
||||
handle.get_peer_addr(),
|
||||
weak_app,
|
||||
);
|
||||
handle.window().hide().unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
} else {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_pending_session(false);
|
||||
if let Some(message) = message.message {
|
||||
show_netplay_error(message);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
} else {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_pending_session(false);
|
||||
show_netplay_error("Server did not respond".to_string());
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn setup_wait_window(
|
||||
netplay_write_sender: tokio::sync::broadcast::Sender<Option<NetplayMessage>>,
|
||||
mut netplay_read_receiver: tokio::sync::broadcast::Receiver<NetplayMessage>,
|
||||
session_name: slint::SharedString,
|
||||
game_name: slint::SharedString,
|
||||
rom_path: slint::SharedString,
|
||||
player_name: slint::SharedString,
|
||||
port: i32,
|
||||
can_start: bool,
|
||||
fullscreen: bool,
|
||||
overclock: bool,
|
||||
peer_addr: slint::SharedString,
|
||||
weak_app: slint::Weak<AppWindow>,
|
||||
) {
|
||||
let local_player = player_name.clone();
|
||||
|
||||
let mut socket_addr: std::net::SocketAddr = peer_addr.to_string().parse().unwrap();
|
||||
socket_addr.set_port(port as u16);
|
||||
let wait = NetplayWait::new().unwrap();
|
||||
wait.set_session_name(session_name);
|
||||
wait.set_game_name(game_name);
|
||||
wait.set_rom_path(rom_path);
|
||||
wait.set_port(port);
|
||||
wait.set_can_start(can_start);
|
||||
|
||||
let sender = netplay_write_sender.clone();
|
||||
wait.on_send_chat_message(move |message| {
|
||||
let send_chat = NetplayMessage {
|
||||
message_type: "request_chat_message".to_string(),
|
||||
player_name: Some(player_name.to_string()),
|
||||
client_sha: None,
|
||||
netplay_version: None,
|
||||
player_names: None,
|
||||
rooms: None,
|
||||
emulator: None,
|
||||
accept: None,
|
||||
message: Some(message.into()),
|
||||
auth_time: None,
|
||||
auth: None,
|
||||
room: Some(NetplayRoom {
|
||||
room_name: None,
|
||||
password: None,
|
||||
game_name: None,
|
||||
md5: None,
|
||||
protected: None,
|
||||
port: Some(port),
|
||||
features: None,
|
||||
buffer_target: None,
|
||||
}),
|
||||
};
|
||||
sender.send(Some(send_chat)).unwrap();
|
||||
});
|
||||
|
||||
let sender = netplay_write_sender.clone();
|
||||
wait.on_begin_game(move || {
|
||||
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: Some(port),
|
||||
features: None,
|
||||
buffer_target: None,
|
||||
}),
|
||||
};
|
||||
sender.send(Some(begin_game)).unwrap();
|
||||
});
|
||||
|
||||
let sender = netplay_write_sender.clone();
|
||||
wait.window().on_close_requested(move || {
|
||||
let _ = sender.send(None); // close current websocket if any
|
||||
slint::CloseRequestResponse::HideWindow
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
netplay_write_sender.send(Some(motd_message)).unwrap();
|
||||
|
||||
let weak = wait.as_weak();
|
||||
tokio::spawn(async move {
|
||||
while let Ok(response) = netplay_read_receiver.recv().await {
|
||||
match response.message_type.as_str() {
|
||||
"reply_motd" => {
|
||||
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: Some(port),
|
||||
features: None,
|
||||
buffer_target: None,
|
||||
}),
|
||||
};
|
||||
netplay_write_sender.send(Some(request_players)).unwrap();
|
||||
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
#[allow(clippy::regex_creation_in_loops)]
|
||||
let re = regex::Regex::new(r"<[^>]*>").unwrap();
|
||||
let motd = re
|
||||
.replace_all(response.message.unwrap().as_str(), "")
|
||||
.into_owned();
|
||||
handle.set_motd(motd.into());
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
"reply_players" => {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
if let Some(player_names) = response.player_names {
|
||||
let players_vec: slint::VecModel<slint::SharedString> =
|
||||
slint::VecModel::default();
|
||||
for player in player_names {
|
||||
players_vec.push(player.into());
|
||||
}
|
||||
let players_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
|
||||
std::rc::Rc::new(players_vec);
|
||||
handle.set_players(slint::ModelRc::from(players_model));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
"reply_chat_message" => {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
let mut chat_text = handle.get_chat_text();
|
||||
chat_text.push_str(&format!("{}\n", response.message.unwrap()));
|
||||
handle.set_chat_text(chat_text);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
"reply_begin_game" => {
|
||||
if response.accept.unwrap() == 0 {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.window().hide().unwrap();
|
||||
let _ = netplay_write_sender.send(None);
|
||||
|
||||
let mut player_number = 4;
|
||||
let players = handle.get_players();
|
||||
for (i, player) in players.iter().enumerate() {
|
||||
if player == local_player {
|
||||
player_number = i;
|
||||
}
|
||||
}
|
||||
if player_number > 3 {
|
||||
panic!("Could not determine player number");
|
||||
}
|
||||
|
||||
run_rom(
|
||||
GbPaths {
|
||||
rom: [None, None, None, None],
|
||||
ram: [None, None, None, None],
|
||||
},
|
||||
handle.get_rom_path().as_str().into(),
|
||||
fullscreen,
|
||||
overclock,
|
||||
VruChannel {
|
||||
vru_window_notifier: None,
|
||||
vru_word_receiver: None,
|
||||
},
|
||||
Some(NetplayDevice {
|
||||
peer_addr: socket_addr,
|
||||
player_number: player_number as u8,
|
||||
}),
|
||||
weak_app,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
return;
|
||||
} else {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_can_start(can_start);
|
||||
if let Some(message) = response.message {
|
||||
show_netplay_error(message);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
println!("Unknown netplay message type: {}", response.message_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
wait.show().unwrap();
|
||||
}
|
||||
|
||||
pub fn setup_join_window(
|
||||
join_window: &NetplayJoin,
|
||||
fullscreen: bool,
|
||||
weak_app: slint::Weak<AppWindow>,
|
||||
) {
|
||||
let (netplay_read_sender, netplay_read_receiver): (
|
||||
tokio::sync::broadcast::Sender<NetplayMessage>,
|
||||
tokio::sync::broadcast::Receiver<NetplayMessage>,
|
||||
) = tokio::sync::broadcast::channel(5);
|
||||
|
||||
let (netplay_write_sender, netplay_write_receiver): (
|
||||
tokio::sync::broadcast::Sender<Option<NetplayMessage>>,
|
||||
tokio::sync::broadcast::Receiver<Option<NetplayMessage>>,
|
||||
) = tokio::sync::broadcast::channel(5);
|
||||
|
||||
populate_server_names(join_window.as_weak());
|
||||
let weak = join_window.as_weak();
|
||||
join_window.on_get_ping(move |server_url| {
|
||||
update_ping(weak.clone(), server_url.to_string());
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.invoke_refresh_session(server_url);
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
let weak = join_window.as_weak();
|
||||
join_window.on_select_rom(move || {
|
||||
select_rom(weak.clone());
|
||||
});
|
||||
|
||||
let sender = netplay_write_sender.clone();
|
||||
join_window.window().on_close_requested(move || {
|
||||
let _ = sender.send(None); // close current websocket if any
|
||||
slint::CloseRequestResponse::HideWindow
|
||||
});
|
||||
|
||||
let weak = join_window.as_weak();
|
||||
let sender = netplay_write_sender.clone();
|
||||
let receiver = netplay_read_receiver.resubscribe();
|
||||
join_window.on_refresh_session(move |server_url| {
|
||||
let _ = sender.send(None); // close current websocket if any
|
||||
manage_websocket(
|
||||
server_url.to_string(),
|
||||
netplay_read_sender.clone(),
|
||||
netplay_write_receiver.resubscribe(),
|
||||
weak.clone(),
|
||||
);
|
||||
update_sessions(sender.clone(), receiver.resubscribe(), weak.clone());
|
||||
});
|
||||
let weak = join_window.as_weak();
|
||||
join_window.on_join_session(move |player_name, game_hash, password, port| {
|
||||
join_session(
|
||||
netplay_write_sender.clone(),
|
||||
netplay_read_receiver.resubscribe(),
|
||||
player_name.to_string(),
|
||||
game_hash.to_string(),
|
||||
password.to_string(),
|
||||
port,
|
||||
fullscreen,
|
||||
weak_app.clone(),
|
||||
weak.clone(),
|
||||
);
|
||||
});
|
||||
|
||||
join_window.show().unwrap();
|
||||
}
|
||||
+23
-27
@@ -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 {
|
||||
@@ -123,12 +123,9 @@ fn get_save_type(rom: &[u8], game_id: &str) -> Vec<SaveTypes> {
|
||||
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
|
||||
}
|
||||
@@ -196,30 +193,29 @@ 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(&mut ui.storage.paths.eep_file_path);
|
||||
if eep.is_ok() {
|
||||
ui.storage.saves.eeprom.data = eep.unwrap();
|
||||
let eep = std::fs::read(&ui.storage.paths.eep_file_path);
|
||||
if let Ok(eep) = eep {
|
||||
ui.storage.saves.eeprom.data = eep;
|
||||
}
|
||||
let sra = std::fs::read(&mut ui.storage.paths.sra_file_path);
|
||||
if sra.is_ok() {
|
||||
ui.storage.saves.sram.data = sra.unwrap();
|
||||
let sra = std::fs::read(&ui.storage.paths.sra_file_path);
|
||||
if let Ok(sra) = sra {
|
||||
ui.storage.saves.sram.data = sra;
|
||||
}
|
||||
let fla = std::fs::read(&mut ui.storage.paths.fla_file_path);
|
||||
if fla.is_ok() {
|
||||
ui.storage.saves.flash.data = fla.unwrap();
|
||||
let fla = std::fs::read(&ui.storage.paths.fla_file_path);
|
||||
if let Ok(fla) = fla {
|
||||
ui.storage.saves.flash.data = fla;
|
||||
}
|
||||
let mempak = std::fs::read(&mut ui.storage.paths.pak_file_path);
|
||||
if mempak.is_ok() {
|
||||
ui.storage.saves.mempak.data = mempak.unwrap();
|
||||
let mempak = std::fs::read(&ui.storage.paths.pak_file_path);
|
||||
if let Ok(mempak) = mempak {
|
||||
ui.storage.saves.mempak.data = mempak;
|
||||
}
|
||||
let sdcard = std::fs::read(&mut ui.storage.paths.sdcard_file_path);
|
||||
if sdcard.is_ok() {
|
||||
ui.storage.saves.sdcard.data = sdcard.unwrap();
|
||||
let sdcard = std::fs::read(&ui.storage.paths.sdcard_file_path);
|
||||
if let Ok(sdcard) = sdcard {
|
||||
ui.storage.saves.sdcard.data = sdcard;
|
||||
}
|
||||
let romsave = std::fs::read(&mut ui.storage.paths.romsave_file_path);
|
||||
if romsave.is_ok() {
|
||||
ui.storage.saves.romsave.data =
|
||||
postcard::from_bytes(romsave.unwrap().as_ref()).unwrap();
|
||||
let romsave = std::fs::read(&ui.storage.paths.romsave_file_path);
|
||||
if let Ok(romsave) = romsave {
|
||||
ui.storage.saves.romsave.data = postcard::from_bytes(romsave.as_ref()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+49
-19
@@ -1,6 +1,7 @@
|
||||
#![allow(non_snake_case)]
|
||||
include!(concat!(env!("OUT_DIR"), "/parallel_bindings.rs"));
|
||||
use crate::{device, ui};
|
||||
use ab_glyph::{Font, ScaleFont};
|
||||
|
||||
pub fn init(device: &mut device::Device) {
|
||||
ui::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
|
||||
@@ -41,7 +42,13 @@ pub fn init(device: &mut device::Device) {
|
||||
if !unsafe { sdl3_sys::video::SDL_ShowWindow(device.ui.video.window) } {
|
||||
panic!("Could not show window");
|
||||
}
|
||||
unsafe { sdl3_sys::everything::SDL_HideCursor() };
|
||||
unsafe {
|
||||
sdl3_sys::everything::SDL_HideCursor();
|
||||
sdl3_sys::everything::SDL_SetHint(
|
||||
sdl3_sys::everything::SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS,
|
||||
std::ffi::CString::new("1").unwrap().as_ptr(),
|
||||
);
|
||||
}
|
||||
|
||||
let gfx_info = GFX_INFO {
|
||||
RDRAM: device.rdram.mem.as_mut_ptr(),
|
||||
@@ -74,15 +81,15 @@ pub fn update_screen() {
|
||||
unsafe { rdp_update_screen() }
|
||||
}
|
||||
|
||||
pub fn check_framebuffers(address: u32) {
|
||||
unsafe { rdp_check_framebuffers(address) }
|
||||
pub fn state_size() -> usize {
|
||||
unsafe { rdp_state_size() }
|
||||
}
|
||||
|
||||
pub fn full_sync() {
|
||||
unsafe { rdp_full_sync() }
|
||||
pub fn save_state(rdp_state: *mut u8) {
|
||||
unsafe { rdp_save_state(rdp_state) }
|
||||
}
|
||||
|
||||
pub fn load_state(device: &mut device::Device) {
|
||||
pub fn load_state(device: &mut device::Device, rdp_state: *const u8) {
|
||||
let gfx_info = GFX_INFO {
|
||||
RDRAM: device.rdram.mem.as_mut_ptr(),
|
||||
DMEM: device.rsp.mem.as_mut_ptr(),
|
||||
@@ -100,6 +107,10 @@ pub fn load_state(device: &mut device::Device) {
|
||||
};
|
||||
unsafe {
|
||||
rdp_new_processor(gfx_info);
|
||||
rdp_load_state(rdp_state);
|
||||
for reg in 0..device::vi::VI_REGS_COUNT {
|
||||
rdp_set_vi_register(reg, device.vi.regs[reg as usize])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +125,12 @@ pub fn check_callback(device: &mut device::Device) {
|
||||
}
|
||||
device.vi.enable_speed_limiter = callback.enable_speedlimiter;
|
||||
}
|
||||
|
||||
if callback.lower_volume {
|
||||
ui::audio::lower_audio_volume(&mut device.ui);
|
||||
} else if callback.raise_volume {
|
||||
ui::audio::raise_audio_volume(&mut device.ui);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_register(reg: u32, value: u32) {
|
||||
@@ -126,12 +143,11 @@ pub fn process_rdp_list() -> u64 {
|
||||
unsafe { rdp_process_commands() }
|
||||
}
|
||||
|
||||
pub fn draw_text(text: &str, renderer: *mut sdl3_sys::render::SDL_Renderer, font: &rusttype::Font) {
|
||||
let text_size = 32;
|
||||
let scale = rusttype::Scale::uniform(text_size as f32);
|
||||
let v_metrics = font.v_metrics(scale);
|
||||
let offset = rusttype::point(10.0, 10.0 + v_metrics.ascent);
|
||||
|
||||
pub fn draw_text(
|
||||
text: &str,
|
||||
renderer: *mut sdl3_sys::render::SDL_Renderer,
|
||||
font: &ab_glyph::FontRef,
|
||||
) {
|
||||
// Clear the canvas
|
||||
unsafe {
|
||||
sdl3_sys::render::SDL_SetRenderDrawColor(
|
||||
@@ -144,12 +160,20 @@ pub fn draw_text(text: &str, renderer: *mut sdl3_sys::render::SDL_Renderer, font
|
||||
sdl3_sys::render::SDL_RenderClear(renderer);
|
||||
};
|
||||
|
||||
for glyph in font.layout(text, scale, offset) {
|
||||
if let Some(bb) = glyph.pixel_bounding_box() {
|
||||
glyph.draw(|x, y, v| {
|
||||
let x = x as i32 + bb.min.x;
|
||||
let y = y as i32 + bb.min.y + (240 - text_size);
|
||||
if v > 0.5 {
|
||||
let text_size = 40.0;
|
||||
let (mut w, mut h) = (0, 0);
|
||||
unsafe { sdl3_sys::render::SDL_GetRenderOutputSize(renderer, &mut w, &mut h) };
|
||||
let x_start = 20.0;
|
||||
let y_start = (h / 2) as f32;
|
||||
|
||||
let mut x_offset = 0.0;
|
||||
for c in text.chars() {
|
||||
let q_glyph_id = font.glyph_id(c);
|
||||
let q_glyph = q_glyph_id.with_scale(text_size);
|
||||
|
||||
if let Some(q) = font.outline_glyph(q_glyph) {
|
||||
q.draw(|x, y, c| {
|
||||
if c > 0.5 {
|
||||
unsafe {
|
||||
sdl3_sys::render::SDL_SetRenderDrawColor(
|
||||
renderer,
|
||||
@@ -158,11 +182,17 @@ pub fn draw_text(text: &str, renderer: *mut sdl3_sys::render::SDL_Renderer, font
|
||||
255,
|
||||
sdl3_sys::pixels::SDL_ALPHA_OPAQUE,
|
||||
);
|
||||
sdl3_sys::render::SDL_RenderPoint(renderer, x as f32, y as f32);
|
||||
sdl3_sys::render::SDL_RenderPoint(
|
||||
renderer,
|
||||
x_start + x_offset + x as f32 - q.px_bounds().width()
|
||||
+ q.px_bounds().max.x,
|
||||
y_start + y as f32 - q.px_bounds().height() + q.px_bounds().max.y,
|
||||
);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
x_offset += font.as_scaled(text_size).h_advance(q_glyph_id);
|
||||
}
|
||||
|
||||
// Present the canvas
|
||||
|
||||
+2
-6
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user