Compare commits

...

102 Commits

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

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

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

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

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

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

* add win aarch64 (commented out)

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

* more

* more

* more

* more

* more

* more

* more

* more

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

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

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

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

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

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

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

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

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

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.27
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 05:36:29 +02:00
Logan McNaughton 79e4cb1807 Update mac runner 2025-06-13 11:04:05 +02:00
Logan McNaughton f3e819158a fix formatting 2025-06-12 18:41:44 +02:00
Logan McNaughton e83bcbb4ce remove session reference for netplay 2025-06-12 18:39:31 +02:00
Logan McNaughton eb5d255fd7 move get_controller_names 2025-06-12 13:34:59 +02:00
dependabot[bot] 776d127a84 Bump bindgen from 0.71.1 to 0.72.0 (#420)
Bumps [bindgen](https://github.com/rust-lang/rust-bindgen) from 0.71.1 to 0.72.0.
- [Release notes](https://github.com/rust-lang/rust-bindgen/releases)
- [Changelog](https://github.com/rust-lang/rust-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/rust-bindgen/compare/v0.71.1...v0.72.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-09 06:56:56 +02:00
Logan McNaughton af5f292459 Bump to 1.0.18 (#418) 2025-06-05 12:34:20 +02:00
Logan McNaughton c25a56df50 Fix netplay crash when server is down (#417)
* Fix netplay crash when server is down

* more
2025-06-05 12:16:11 +02:00
Logan McNaughton 9d9f965b41 fix sevenz-rust2 version 2025-06-04 09:21:59 +02:00
Logan McNaughton 68197d235b update cargo packages 2025-06-03 16:11:58 +02:00
Logan McNaughton 1c91abbf2d Potential fix for code scanning alert no. 2: Workflow does not contain permissions (#416)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-06-03 16:06:38 +02:00
Logan McNaughton 9348594eb0 Potential fix for code scanning alert no. 4: Workflow does not contain permissions (#413)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-06-03 15:45:47 +02:00
dependabot[bot] e97f4e7f16 Bump zip from 3.0.0 to 4.0.0 (#404)
Bumps [zip](https://github.com/zip-rs/zip2) from 3.0.0 to 4.0.0.
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/compare/v3.0.0...v4.0.0)

---
updated-dependencies:
- dependency-name: zip
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-22 06:28:35 +02:00
Logan McNaughton cea55914d9 update linux deps (#401) 2025-05-16 13:43:36 +02:00
Logan McNaughton 77a22f08b3 ignore settings.json 2025-05-16 13:32:54 +02:00
Logan McNaughton cf0f325ced Update rust to 1.87 (#400) 2025-05-15 20:57:22 +02:00
Logan McNaughton 21f1a274b4 update cargo packages 2025-05-06 13:02:59 +02:00
Logan McNaughton def8932922 bump to 1.0.17 (#384) 2025-04-15 23:31:22 +02:00
Logan McNaughton 5b6f9b76a6 fix interlaced image (#383) 2025-04-15 23:23:47 +02:00
Logan McNaughton 5a838464a4 update cargo packages 2025-04-15 22:44:24 +02:00
Logan McNaughton 383b568012 remove framebuffer tracking (#381)
* remove framebuffer tracking

* more
2025-04-15 11:08:40 +02:00
Logan McNaughton 27bb66df4c Cleanup some controller code (#378)
* controller hotplugging

* more

* more
2025-04-13 21:07:54 +02:00
Logan McNaughton 33f74e5062 update cargo packages 2025-04-08 07:52:02 +02:00
Logan McNaughton 68f90e1cd7 update 7z package (#370) 2025-04-05 09:30:12 +02:00
dependabot[bot] cc5f636912 Bump zip from 2.5.0 to 2.6.0 (#369)
Bumps [zip](https://github.com/zip-rs/zip2) from 2.5.0 to 2.6.0.
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/compare/v2.5.0...v2.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-04 06:51:11 +02:00
Logan McNaughton e3a9849ad7 bump to 1.0.16 (#368) 2025-04-03 12:13:20 +02:00
Logan McNaughton 7ed6867691 Update rust to 1.86.0 (#367) 2025-04-03 12:01:15 +02:00
Logan McNaughton 11a8e96564 better default for address tracking 2025-04-02 14:06:43 +02:00
Logan McNaughton 7111cc9761 update cargo packages 2025-04-02 12:26:48 +02:00
Logan McNaughton b4832b941f sync parallel rdp for texture images (#361)
* sync parallel rdp for texture images

* more

* more

* more

* more

* more
2025-04-02 12:24:30 +02:00
dependabot[bot] 82b1888f82 Bump governor from 0.9.0 to 0.10.0 (#362)
Bumps [governor](https://github.com/boinkor-net/governor) from 0.9.0 to 0.10.0.
- [Release notes](https://github.com/boinkor-net/governor/releases)
- [Changelog](https://github.com/boinkor-net/governor/blob/master/release.toml)
- [Commits](https://github.com/boinkor-net/governor/compare/v0.9.0...v0.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-28 07:47:22 +01:00
Logan McNaughton f35869190b gather controller input even without focus (#360) 2025-03-27 08:55:52 +01:00
Logan McNaughton 64eda1a1e3 bump to 1.0.15 (#358) 2025-03-26 11:27:02 +01:00
Logan McNaughton 8026bf13d6 update cargo packages 2025-03-26 11:18:05 +01:00
Logan McNaughton 83103130e7 cleanup 2025-03-25 12:59:32 +01:00
dependabot[bot] 87717c4c7e Bump governor from 0.8.1 to 0.9.0 (#357)
Bumps [governor](https://github.com/boinkor-net/governor) from 0.8.1 to 0.9.0.
- [Release notes](https://github.com/boinkor-net/governor/releases)
- [Changelog](https://github.com/boinkor-net/governor/blob/master/release.toml)
- [Commits](https://github.com/boinkor-net/governor/compare/v0.8.1...v0.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-25 06:00:08 +01:00
Logan McNaughton 6355885a56 let server determine netplay buffer target (#356) 2025-03-24 15:03:50 +01:00
dependabot[bot] 5d180dfb65 Bump zip from 2.4.2 to 2.5.0 (#354)
Bumps [zip](https://github.com/zip-rs/zip2) from 2.4.2 to 2.5.0.
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/compare/v2.4.2...v2.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 11:28:28 +01:00
Logan McNaughton fd6965ff3b remove rusttype (#355)
* remove rusttype

* more
2025-03-24 11:06:17 +01:00
Logan McNaughton 2fcf202a5c move variable into function 2025-03-23 22:07:32 +01:00
Logan McNaughton 93630c4156 fix audit workflow (#350) 2025-03-23 21:31:24 +01:00
Logan McNaughton a66e8184e4 add workflow dispatch option 2025-03-23 19:25:15 +01:00
Logan McNaughton e4f05af38d add audit action (#349) 2025-03-23 19:24:17 +01:00
Logan McNaughton 8a09cb711f Add RDP to savestate (#348)
* organize RDP device

* more

* more

* more

* more

* more

* more

* more

* more
2025-03-22 21:24:39 +01:00
Logan McNaughton a58eed7dc9 add a few more loops (#347)
* add a few more loops

* more
2025-03-22 08:23:48 +01:00
Logan McNaughton 09ba7e4d13 use loop for cache (#346) 2025-03-21 22:37:04 +01:00
Logan McNaughton 7316e41c7e pin rust version 2025-03-21 15:37:11 +01:00
Logan McNaughton 1aa00de1b2 fix crash when attempting to read zip without rom (#345)
* fix crash when attempting to read zip without rom

* more
2025-03-19 18:37:35 +01:00
Logan McNaughton f71ec9d54a add cic entry for libdragon ipl3 (#344) 2025-03-19 14:15:00 +01:00
Logan McNaughton bf2734e59c update cargo packages 2025-03-19 09:56:00 +01:00
dependabot[bot] 7ee26961f1 Bump zip from 2.2.3 to 2.4.1 (#341)
Bumps [zip](https://github.com/zip-rs/zip2) from 2.2.3 to 2.4.1.
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/compare/v2.2.3...v2.4.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-18 09:21:31 +01:00
Logan McNaughton 5d692318fe better region check 2025-03-17 10:06:26 +01:00
Logan McNaughton f2966230f9 Fix freeze in Bass Hunter 64 (#339) 2025-03-17 09:46:09 +01:00
Logan McNaughton 48575f9c52 fix windows cross compile (#336) 2025-03-16 20:31:57 +01:00
Logan McNaughton dbab58f4a7 catch possible error in loading save state (#335) 2025-03-16 17:10:16 +01:00
Logan McNaughton f6b0b59bbc fix savestate default memory 2025-03-16 10:32:32 +01:00
Logan McNaughton ca61695dd1 update cargo packages 2025-03-16 10:13:23 +01:00
Logan McNaughton a182fbb1f9 cleanup function 2025-03-16 10:11:50 +01:00
Logan McNaughton da9b2247f8 remove more mut (#334) 2025-03-16 10:10:16 +01:00
Logan McNaughton 1bcadb3a8b remove un-needed mut (#333)
* remove un-needed mut

* more

* more
2025-03-16 09:48:32 +01:00
Logan McNaughton 433f08c7e0 Update README.md 2025-03-16 07:55:10 +01:00
Logan McNaughton b07dde8768 disable mempak for Chameleon Twist (#332) 2025-03-16 07:35:37 +01:00
Logan McNaughton cba7bc7477 ensure gain setting persists 2025-03-16 07:18:05 +01:00
Logan McNaughton ea6f203e33 allow up to 200% audio volume (#331) 2025-03-15 20:21:46 +01:00
Logan McNaughton e48ad9bdff add way to change audio volume (#330)
* add way to change audio volume

* more
2025-03-15 11:11:18 +01:00
Logan McNaughton 11b1137412 update netplay server name 2025-03-13 21:03:50 +01:00
Logan McNaughton 5528f233c0 fix compile warning 2025-03-13 16:51:41 +01:00
Logan McNaughton 9d18673257 bump to 1.0.14 (#328) 2025-03-13 07:20:29 +01:00
66 changed files with 6763 additions and 3269 deletions
+11 -5
View File
@@ -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:
+5 -2
View File
@@ -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
+18
View File
@@ -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
View File
@@ -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
+2722 -901
View File
File diff suppressed because it is too large Load Diff
+16 -13
View File
@@ -1,32 +1,34 @@
[package]
name = "gopher64"
version = "1.0.13"
version = "1.0.19"
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.16", default-features = false }
chrono = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-big-array = "0.5"
eframe = { version = "0.31", default-features = false, features = ["wayland", "x11", "glow"] }
slint = { version = "1.12", default-features = false, features = ["compat-1-2", "std", "backend-winit", "renderer-femtovg-wgpu"] }
open = "5.3"
sha2 = "0.10"
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.46", features = ["rt-multi-thread", "macros"] }
spin_sleep = "1.3"
encoding_rs = "0.8"
mimalloc = "0.1"
regex = "1.11"
tungstenite = "0.26"
socket2 = "0.5"
tokio-tungstenite = "0.27"
futures = "0.3"
socket2 = { version = "0.6", features = ["all"] }
rand_chacha = "0.9"
fatfs = "0.3"
semver = "1.0"
@@ -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]
+10 -8
View File
@@ -1,7 +1,9 @@
# gopher64
## download
https://github.com/gopher64/gopher64/releases
Windows: https://github.com/gopher64/gopher64/releases/latest/download/gopher64-windows-x86_64.exe
Linux: https://github.com/gopher64/gopher64/releases/latest/download/gopher64-linux-x86_64
## wiki
@@ -15,17 +17,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 +41,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 +54,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
+27 -8
View File
@@ -2,6 +2,9 @@ fn main() {
println!("cargo::rerun-if-changed=parallel-rdp");
println!("cargo::rerun-if-changed=src/compat");
let slint_config = slint_build::CompilerConfiguration::new().with_style("cosmic".into());
slint_build::compile_with_config("src/ui/gui/appwindow.slint", slint_config).unwrap();
let mut simd_build = cc::Build::new();
let mut build = cc::Build::new();
build
@@ -59,17 +62,32 @@ 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")
if env == "msvc" {
build.flag("/arch:armv8.2");
simd_build.flag("/arch:armv8.2");
} else if env == "gnu" {
build.flag("-march=armv8.2-a");
simd_build.flag("-march=armv8.2-a");
} else {
panic!("unknown env")
}
} else {
panic!("unknown arch")
}
build.flag("-DVK_USE_PLATFORM_WIN32_KHR");
winres::WindowsResource::new()
winresource::WindowsResource::new()
.set_icon("data/icon.ico")
.compile()
.unwrap();
@@ -79,8 +97,6 @@ fn main() {
} else if arch == "aarch64" {
build.flag("-march=armv8.2-a");
simd_build.flag("-march=armv8.2-a");
simd_build.flag("-DSSE2NEON_SUPPRESS_WARNINGS");
simd_build.file("src/compat/aarch64.c");
} else {
panic!("unknown arch")
}
@@ -101,8 +117,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");
@@ -158,6 +175,8 @@ fn main() {
.write_to_file(out_path.join("simd_bindings.rs"))
.expect("Couldn't write bindings!");
simd_build.flag("-DSSE2NEON_SUPPRESS_WARNINGS");
simd_build.file("src/compat/aarch64.c");
simd_build.file(std::env::temp_dir().join("bindgen").join("extern.c"));
simd_build.include(".");
simd_build.compile("simd");
@@ -169,7 +188,7 @@ fn main() {
.unwrap();
let git_hash = String::from_utf8(git_output.stdout).unwrap();
println!("cargo:rustc-env=GIT_HASH={}", git_hash);
println!("cargo:rustc-env=GIT_HASH={git_hash}");
println!("cargo:rustc-env=N64_STACK_SIZE={}", 8 * 1024 * 1024);
}
+1
View File
File diff suppressed because one or more lines are too long
+58 -103
View File
@@ -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 std::vector<bool> 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;
rdram_dirty.assign(gfx_info.RDRAM_SIZE / 8, false);
gfx_info = _gfx_info;
if (processor)
{
delete processor;
@@ -449,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);
rdram_dirty.assign(gfx_info.RDRAM_SIZE / 8, false);
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()
@@ -498,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;
@@ -507,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
@@ -524,83 +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])
{
std::fill_n(rdram_dirty.begin() + frame_buffer_info.framebuffer_address, frame_buffer_info.framebuffer_size, true);
}
if (frame_buffer_info.depthbuffer_enabled && !rdram_dirty[frame_buffer_info.depthbuffer_address])
{
std::fill_n(rdram_dirty.begin() + frame_buffer_info.depthbuffer_address, frame_buffer_info.depthbuffer_size, true);
}
}
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;
+6 -2
View File
@@ -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
}
+1 -1
View File
@@ -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]);
}
+2
View File
@@ -0,0 +1,2 @@
[toolchain]
channel = "1.88.0"
+189
View File
@@ -0,0 +1,189 @@
use crate::device;
use crate::ui;
#[derive(serde::Serialize, serde::Deserialize, Copy, Clone)]
pub struct DecodedCheat {
code_type: u8,
address: u32,
data: u16,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Cheats {
pub cheats: Vec<Vec<DecodedCheat>>,
pub boot: bool,
pub enabled: bool,
}
pub fn init(
device: &mut device::Device,
cheat_settings: std::collections::HashMap<String, Option<String>>,
) {
let cheats =
serde_json::from_slice::<ui::cheats::Cheats>(include_bytes!("../data/cheats.json"))
.unwrap()
.get(&ui::storage::get_game_crc(&device.cart.rom))
.cloned()
.unwrap_or_default();
let re = regex::Regex::new(r"\?+").unwrap();
for cheat_setting in cheat_settings.iter() {
if let Some(cheat_data) = cheats.get(cheat_setting.0) {
let mut option_value = None;
if let Some(option) = cheat_setting.1 {
if let Some(found_option_value) = cheat_data
.options
.as_ref()
.cloned()
.unwrap_or_default()
.get(option)
{
option_value = Some(found_option_value.to_string());
} else {
panic!("Cheat option: {option} not found");
}
}
println!(
"Cheat: {} Option: {}",
cheat_setting.0,
cheat_setting.1.clone().unwrap_or("none".to_string())
);
let mut decoded_cheat: Vec<DecodedCheat> = vec![];
for code in cheat_data.data.iter() {
let mut result = code.clone();
if let Some(option_value) = option_value.as_ref() {
result = re.replace_all(code, option_value).into_owned();
}
let mut split = result.split_whitespace();
let first_part = u32::from_str_radix(split.next().unwrap(), 16).unwrap();
decoded_cheat.push(DecodedCheat {
code_type: (first_part >> 24) as u8,
address: first_part & 0x00FFFFFF,
data: u16::from_str_radix(split.next().unwrap(), 16).unwrap(),
});
}
device.cheats.cheats.push(decoded_cheat);
} else {
println!("Could not find cheat: {}", cheat_setting.0);
}
}
if !device.cheats.cheats.is_empty() {
device.cheats.enabled = true;
ui::audio::play_cheat_event(&device.ui);
}
}
fn write_byte(device: &mut device::Device, cheat_line: &DecodedCheat) {
*device
.rdram
.mem
.get_mut(cheat_line.address as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = cheat_line.data as u8;
}
fn write_half(device: &mut device::Device, cheat_line: &DecodedCheat) {
*device
.rdram
.mem
.get_mut(cheat_line.address as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = (cheat_line.data >> 8) as u8;
*device
.rdram
.mem
.get_mut((cheat_line.address + 1) as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = cheat_line.data as u8;
}
fn equal_byte(device: &mut device::Device, cheat_line: &DecodedCheat) -> bool {
let current_value = *device
.rdram
.mem
.get(cheat_line.address as usize ^ device.byte_swap)
.unwrap_or(&0);
current_value == (cheat_line.data as u8)
}
fn equal_half(device: &mut device::Device, cheat_line: &DecodedCheat) -> bool {
let current_value1 = *device
.rdram
.mem
.get(cheat_line.address as usize ^ device.byte_swap)
.unwrap_or(&0);
let current_value2 = *device
.rdram
.mem
.get((cheat_line.address + 1) as usize ^ device.byte_swap)
.unwrap_or(&0);
current_value1 == (cheat_line.data >> 8) as u8 && current_value2 == cheat_line.data as u8
}
pub fn execute_cheats(device: &mut device::Device, cheats: Vec<Vec<DecodedCheat>>) {
for cheat in cheats.iter() {
let mut valid = true;
let mut cheat_iter = cheat.iter().peekable();
while let Some(cheat_line) = cheat_iter.next() {
match cheat_line.code_type {
0x50 => {
if valid {
let mut expanded_cheat: Vec<DecodedCheat> = vec![];
let compressed_cheat = *cheat_iter.peek().unwrap();
let count = (cheat_line.address & 0xFF00) >> 8;
let offset = cheat_line.address & 0xFF;
for i in 0..count {
let line = DecodedCheat {
code_type: compressed_cheat.code_type,
address: compressed_cheat.address + (i * offset),
data: compressed_cheat.data
+ (i as i16 * cheat_line.data as i16) as u16,
};
expanded_cheat.push(line);
}
execute_cheats(device, [expanded_cheat].to_vec());
}
valid = false; // skip the next line
}
0x80 | 0xA0 => {
if valid {
write_byte(device, cheat_line);
}
valid = true;
}
0x81 | 0xA1 => {
if valid {
write_half(device, cheat_line);
}
valid = true;
}
0xF0 => {
if device.cheats.boot && valid {
write_byte(device, cheat_line);
valid = true;
}
}
0xF1 => {
if device.cheats.boot && valid {
write_half(device, cheat_line);
valid = true;
}
}
0xD0 => {
valid = equal_byte(device, cheat_line);
}
0xD1 => {
valid = equal_half(device, cheat_line);
}
0xD2 => {
valid = !equal_byte(device, cheat_line);
}
0xD3 => {
valid = !equal_half(device, cheat_line);
}
0x88 | 0x89 | 0xD8 | 0xD9 | 0xDA | 0xDB => { /* GS button not emulated */ }
_ => panic!("Unknown cheat code type: {:X}", cheat_line.code_type),
}
}
}
device.cheats.boot = false;
}
+31 -18
View File
@@ -12,7 +12,7 @@ use std::arch::x86_64::*;
use rand_chacha::rand_core::{RngCore, SeedableRng};
use crate::{netplay, ui};
use crate::{cheats, netplay, ui};
use std::{collections::HashMap, fs, io::Read};
pub mod ai;
@@ -44,9 +44,12 @@ pub mod tlb;
pub mod unmapped;
pub mod vi;
pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, fullscreen: bool, overclock: bool) {
device.ui.video.fullscreen = fullscreen;
device.cpu.overclock = overclock;
pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, game_settings: ui::gui::GameSettings) {
device.ui.video.fullscreen = game_settings.fullscreen;
device.cpu.overclock = game_settings.overclock;
if game_settings.disable_expansion_pak {
device.rdram.size = 0x400000;
}
init_rng(device);
@@ -61,7 +64,7 @@ pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, fullscreen: bool, ov
mi::init(device);
pif::init(device);
if device.ui.config.input.emulate_vru {
if device.ui.config.input.emulate_vru && device.netplay.is_none() {
controller::vru::init(device);
}
memory::init(device);
@@ -74,6 +77,8 @@ pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, fullscreen: bool, ov
ui::storage::init(&mut device.ui, &device.cart.rom);
ui::storage::load_saves(&mut device.ui, &mut device.netplay);
cheats::init(device, game_settings.cheats);
cpu::run(device);
ui::input::close(&mut device.ui);
@@ -99,11 +104,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 +116,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 +124,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 +151,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::ArchiveReader::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::ArchiveEntry, reader: &mut dyn std::io::Read| {
let name = entry.name().to_ascii_lowercase();
if !found
&& (name.ends_with("z64") || name.ends_with("n64") || name.ends_with("v64"))
@@ -173,7 +177,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)]
@@ -182,7 +190,7 @@ pub struct Device {
pub netplay: Option<netplay::Netplay>,
#[serde(skip, default = "ui::Ui::default")]
pub ui: ui::Ui,
byte_swap: usize,
pub byte_swap: usize,
pub save_state: bool,
pub load_state: bool,
pub cpu: cpu::Cpu,
@@ -203,6 +211,7 @@ pub struct Device {
pub vru: controller::vru::Vru,
pub vru_window: controller::vru::VruWindow,
pub transferpaks: [controller::transferpak::TransferPak; 4],
pub cheats: cheats::Cheats,
}
pub fn zero_m128i() -> __m128i {
@@ -348,7 +357,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 +485,6 @@ impl Device {
vru_window: controller::vru::VruWindow {
window_notifier: None,
word_receiver: None,
gui_ctx: None,
},
vru: controller::vru::Vru {
status: 0,
@@ -495,6 +503,11 @@ impl Device {
controller::transferpak::TransferPak::default(),
controller::transferpak::TransferPak::default(),
],
cheats: cheats::Cheats {
cheats: vec![],
boot: true,
enabled: false,
},
}
}
}
+3 -3
View File
@@ -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;
}
@@ -146,8 +146,8 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
AI_DACRATE_REG => {
if device.ai.regs[reg as usize] != value & mask {
device.ai.freq = device.vi.clock / (1 + (value & mask)) as u64;
ui::audio::close(&mut device.ui);
ui::audio::init(&mut device.ui, device.ai.freq)
ui::audio::close_game_audio(&mut device.ui);
ui::audio::init_game_audio(&mut device.ui, device.ai.freq)
}
device::memory::masked_write_32(&mut device.ai.regs[reg as usize], value, mask)
}
+36 -160
View File
@@ -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 {
+18 -7
View File
@@ -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 {
@@ -91,7 +91,11 @@ pub fn dma_read(
for i in 0..length {
device.ui.storage.saves.romsave.data.insert(
cart_addr + i,
device.rdram.mem[(dram_addr + i) as usize ^ device.byte_swap],
*device
.rdram
.mem
.get((dram_addr + i) as usize ^ device.byte_swap)
.unwrap_or(&0),
);
}
device.ui.storage.saves.romsave.written = true;
@@ -113,10 +117,17 @@ pub fn dma_write(
let mut j = cart_addr;
while i < dram_addr + length {
if let Some(value) = device.ui.storage.saves.romsave.data.get(&j) {
device.rdram.mem[i as usize ^ device.byte_swap] = *value;
*device
.rdram
.mem
.get_mut(i as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = *value;
} else {
device.rdram.mem[i as usize ^ device.byte_swap] =
*device.cart.rom.get(j as usize).unwrap_or(&0);
*device
.rdram
.mem
.get_mut(i as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = *device.cart.rom.get(j as usize).unwrap_or(&0);
}
i += 1;
j += 1;
@@ -186,7 +197,7 @@ fn set_cic(device: &mut device::Device) {
}
_ => {
device.cart.cic_seed = 0x3F; // CicNus6102
println!("unknown IPL3 {}", hash)
//println!("unknown IPL3 {}", hash)
}
}
}
+13 -5
View File
@@ -51,7 +51,7 @@ pub fn read_regs(
match reg as u32 {
SC64_SCR_REG | SC64_DATA0_REG | SC64_DATA1_REG => device.cart.sc64.regs[reg as usize],
SC64_IDENTIFIER_REG => 0x53437632,
_ => panic!("unknown read reg {} address {:#x}", reg, address),
_ => panic!("unknown read reg {reg} address {address:#x}"),
}
}
@@ -308,8 +308,12 @@ pub fn dma_read(
let mut i = dram_addr;
let mut j = cart_addr;
while i < dram_addr + length && i < device.rdram.size {
buffer[j as usize] = device.rdram.mem[i as usize ^ device.byte_swap];
while i < dram_addr + length {
buffer[j as usize] = *device
.rdram
.mem
.get(i as usize ^ device.byte_swap)
.unwrap_or(&0);
i += 1;
j += 1;
}
@@ -335,8 +339,12 @@ pub fn dma_write(
let mut i = dram_addr;
let mut j = cart_addr;
while i < dram_addr + length && i < device.rdram.size {
device.rdram.mem[i as usize ^ device.byte_swap] = buffer[j as usize];
while i < dram_addr + length {
*device
.rdram
.mem
.get_mut(i as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = buffer[j as usize];
i += 1;
j += 1;
}
+33 -13
View File
@@ -66,7 +66,7 @@ fn read_mem_flash(device: &device::Device, address: u64) -> u32 {
{
/* flashram MMIO read are not supported except for the "dummy" read @0x0000 done before DMA.
* returns a "dummy" value. */
return 0;
0
} else {
/* other accesses are not implemented */
panic!("unknown flashram read")
@@ -150,9 +150,12 @@ fn dma_read_sram(device: &mut device::Device, mut cart_addr: u32, mut dram_addr:
format_sram(device);
while i < dram_addr + length && i < device.rdram.size {
device.ui.storage.saves.sram.data[j as usize] =
device.rdram.mem[i as usize ^ device.byte_swap];
while i < dram_addr + length {
device.ui.storage.saves.sram.data[j as usize] = *device
.rdram
.mem
.get(i as usize ^ device.byte_swap)
.unwrap_or(&0);
i += 1;
j += 1;
}
@@ -169,8 +172,11 @@ fn dma_read_flash(device: &mut device::Device, cart_addr: u32, dram_addr: u32, l
{
/* load page buf using DMA */
for i in 0..length {
device.cart.flashram.page_buf[i as usize] =
device.rdram.mem[(dram_addr + i) as usize ^ device.byte_swap];
device.cart.flashram.page_buf[i as usize] = *device
.rdram
.mem
.get((dram_addr + i) as usize ^ device.byte_swap)
.unwrap_or(&0);
}
} else {
/* other accesses are not implemented */
@@ -206,9 +212,12 @@ fn dma_write_sram(
format_sram(device);
while i < dram_addr + length && i < device.rdram.size {
device.rdram.mem[i as usize ^ device.byte_swap] =
device.ui.storage.saves.sram.data[j as usize];
while i < dram_addr + length {
*device
.rdram
.mem
.get_mut(i as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = device.ui.storage.saves.sram.data[j as usize];
i += 1;
j += 1;
}
@@ -227,10 +236,18 @@ fn dma_write_flash(
&& device.cart.flashram.mode == FlashramMode::ReadSiliconId
{
/* read Silicon ID using DMA */
device.rdram.mem[dram_addr as usize..dram_addr as usize + 4]
device
.rdram
.mem
.get_mut(dram_addr as usize..dram_addr as usize + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&device.cart.flashram.silicon_id[0].to_ne_bytes());
dram_addr += 4;
device.rdram.mem[dram_addr as usize..dram_addr as usize + 4]
device
.rdram
.mem
.get_mut(dram_addr as usize..dram_addr as usize + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&device.cart.flashram.silicon_id[1].to_ne_bytes());
} else if (cart_addr & 0x1ffff) < 0x10000
&& device.cart.flashram.mode == FlashramMode::ReadArray
@@ -250,8 +267,11 @@ fn dma_write_flash(
/* do actual DMA */
for i in 0..length {
device.rdram.mem[(dram_addr + i) as usize ^ device.byte_swap] =
device.ui.storage.saves.flash.data[(cart_addr + i) as usize];
*device
.rdram
.mem
.get_mut((dram_addr + i) as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = device.ui.storage.saves.flash.data[(cart_addr + i) as usize];
}
} else {
/* other accesses are not implemented */
+10 -10
View File
@@ -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;
}
}
+14 -14
View File
@@ -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}"),
}
}
+2 -2
View File
@@ -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);
}
}
}
+5 -5
View File
@@ -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);
}
}
}
+16 -8
View File
@@ -1,5 +1,4 @@
use crate::{device, ui};
use eframe::egui;
use std::collections::HashMap;
const JCMD_VRU_READ: u8 = 0x09;
@@ -33,11 +32,9 @@ pub struct Vru {
#[derive(serde::Serialize, serde::Deserialize)]
pub struct VruWindow {
#[serde(skip)]
pub window_notifier: Option<tokio::sync::mpsc::Sender<Vec<String>>>,
pub window_notifier: Option<tokio::sync::mpsc::Sender<Option<Vec<String>>>>,
#[serde(skip)]
pub word_receiver: Option<tokio::sync::mpsc::Receiver<String>>,
#[serde(skip)]
pub gui_ctx: Option<egui::Context>,
}
pub fn init(device: &mut device::Device) {
@@ -129,7 +126,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
let (res, _enc, errors) = encoding_rs::SHIFT_JIS.decode(&data);
if errors {
panic!("Failed to decode Japanese word {:X?}", data);
panic!("Failed to decode Japanese word {data:X?}");
} else {
device.vru.words.push(res.to_string());
}
@@ -147,7 +144,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
if let Some(result) = word {
device.vru.words.push(result.clone());
} else {
panic!("Unknown VRU word {}", data);
panic!("Unknown VRU word {data}");
}
}
} else {
@@ -195,7 +192,6 @@ pub fn process(device: &mut device::Device, channel: usize) {
&device.vru.words,
device.vru_window.window_notifier.as_ref().unwrap(),
device.vru_window.word_receiver.as_mut().unwrap(),
device.vru_window.gui_ctx.as_ref().unwrap(),
)
} else {
0x7FFF
@@ -253,7 +249,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
}
device.vru.status = 0; /* status is always set to 0 after a write */
}
_ => panic!("unknown VRU command {}", cmd),
_ => panic!("unknown VRU command {cmd}"),
}
}
@@ -2730,5 +2726,17 @@ fn create_word_mappings(device: &mut device::Device) {
String::from("82B582E382C182CF82C282AF82A282A982A2"),
String::from("restricted-depart"),
),
(
String::from("009903720366006903C903FF0045033C"),
String::from("objection"),
),
(
String::from("041100CF02EE0381004503B7"),
String::from("hold-it"),
),
(
String::from("03BA019203C9036C0087000303C6"),
String::from("take-that"),
),
]);
}
+1
View File
@@ -227,6 +227,7 @@ pub fn init(device: &mut device::Device) {
device.cpu.clock_rate = if !device.cpu.overclock {
93750000
} else {
println!("Overclocking enabled, setting clock rate to 125 MHz");
125000000
};
+2 -2
View File
@@ -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
}
+2 -2
View File
@@ -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")]
+6 -3
View File
@@ -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 {
@@ -222,7 +221,11 @@ fn unknown_dma_write(
dram_addr &= device::rdram::RDRAM_MASK as u32;
for i in 0..length {
device.rdram.mem[(dram_addr + i) as usize ^ device.byte_swap] = 0;
*device
.rdram
.mem
.get_mut((dram_addr + i) as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = 0;
}
device::pi::calculate_cycles(device, 1, length)
}
+22 -9
View File
@@ -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;
}
}
+45 -27
View File
@@ -1,5 +1,4 @@
use crate::device;
use crate::ui;
use std::alloc::{Layout, alloc_zeroed};
//const RDRAM_CONFIG_REG: u32 = 0;
@@ -24,15 +23,19 @@ pub struct Rdram {
}
pub fn read_mem_fast(
device: &mut device::Device,
device: &device::Device,
address: u64,
_access_size: device::memory::AccessSize,
) -> u32 {
let masked_address = address as usize & RDRAM_MASK;
u32::from_ne_bytes(
device.rdram.mem[masked_address..masked_address + 4]
device
.rdram
.mem
.get(masked_address..masked_address + 4)
.unwrap_or_default()
.try_into()
.unwrap(),
.unwrap_or_default(),
)
}
@@ -46,30 +49,35 @@ pub fn read_mem(
rdram_calculate_cycles(access_size as u64) / (access_size as u64 / 4),
);
let masked_address = address as usize & RDRAM_MASK;
if address < device.rdram.size as u64 {
ui::video::check_framebuffers(masked_address as u32);
u32::from_ne_bytes(
device.rdram.mem[masked_address..masked_address + 4]
.try_into()
.unwrap(),
)
} else {
0
}
u32::from_ne_bytes(
device
.rdram
.mem
.get(masked_address..masked_address + 4)
.unwrap_or_default()
.try_into()
.unwrap_or_default(),
)
}
pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u32) {
if address < device.rdram.size as u64 {
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()
.unwrap(),
);
device::memory::masked_write_32(&mut data, value, mask);
device.rdram.mem[address as usize..(address + 4) as usize]
.copy_from_slice(&data.to_ne_bytes());
}
let mut data = u32::from_ne_bytes(
device
.rdram
.mem
.get(address as usize..(address + 4) as usize)
.unwrap_or_default()
.try_into()
.unwrap_or_default(),
);
device::memory::masked_write_32(&mut data, value, mask);
device
.rdram
.mem
.get_mut(address as usize..(address + 4) as usize)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&data.to_ne_bytes());
}
pub fn read_regs(
@@ -106,9 +114,19 @@ pub fn init(device: &mut device::Device) {
unsafe { Vec::from_raw_parts(ptr, device.rdram.size as usize, device.rdram.size as usize) };
// hack, skip RDRAM initialization
device.rdram.mem[0x318..0x318 + 4].copy_from_slice(&device.rdram.size.to_ne_bytes());
device
.rdram
.mem
.get_mut(0x318..0x318 + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&device.rdram.size.to_ne_bytes());
// hack, skip RDRAM initialization
device.rdram.mem[0x3f0..0x3f0 + 4].copy_from_slice(&device.rdram.size.to_ne_bytes());
device
.rdram
.mem
.get_mut(0x3f0..0x3f0 + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&device.rdram.size.to_ne_bytes());
device.ri.regs[device::ri::RI_MODE_REG as usize] = 0x0e;
device.ri.regs[device::ri::RI_CONFIG_REG as usize] = 0x40;
+16 -12
View File
@@ -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 {
@@ -159,7 +157,11 @@ fn do_dma(device: &mut device::Device, dma: RspDma) {
.try_into()
.unwrap(),
);
device.rdram.mem[dram_addr as usize..dram_addr as usize + 4]
device
.rdram
.mem
.get_mut(dram_addr as usize..dram_addr as usize + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&data.to_ne_bytes());
mem_addr += 4;
dram_addr += 4;
@@ -173,14 +175,16 @@ fn do_dma(device: &mut device::Device, dma: RspDma) {
while j < count {
let mut i = 0;
while i < length {
let mut data = 0;
if dram_addr < device.rdram.size {
data = u32::from_ne_bytes(
device.rdram.mem[dram_addr as usize..dram_addr as usize + 4]
.try_into()
.unwrap(),
);
}
let data = u32::from_ne_bytes(
device
.rdram
.mem
.get(dram_addr as usize..dram_addr as usize + 4)
.unwrap_or_default()
.try_into()
.unwrap_or_default(),
);
if offset != 0 {
// imem being updated
device.rsp.cpu.instructions[((mem_addr & 0xFFF) / 4) as usize].func =
+25 -31
View File
@@ -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")
@@ -715,7 +709,7 @@ pub fn lsv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 2 == 0 {
if element.is_multiple_of(2) {
modify_vpr16(
&mut device.rsp.cpu.vpr[rt(opcode) as usize],
element / 2,
@@ -742,7 +736,7 @@ pub fn llv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 4 == 0 {
if element.is_multiple_of(4) {
let mut value = 0;
for i in 0..4 {
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u32) << (8 * (3 - i));
@@ -772,7 +766,7 @@ pub fn ldv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 8 == 0 {
if element.is_multiple_of(8) {
let mut value = 0;
for i in 0..8 {
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u64) << (8 * (7 - i));
@@ -802,7 +796,7 @@ pub fn lqv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element == 0 && address % 16 == 0 {
if element == 0 && address.is_multiple_of(16) {
let mut value = 0;
for i in 0..16 {
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u128) << (8 * (15 - i));
@@ -981,7 +975,7 @@ pub fn ssv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 2 == 0 {
if element.is_multiple_of(2) {
let start = (address & 0xFFF) as usize;
device.rsp.mem[start..start + 2].copy_from_slice(
&get_vpr16(device.rsp.cpu.vpr[rt(opcode) as usize], element / 2).to_be_bytes(),
@@ -1003,7 +997,7 @@ pub fn slv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 4 == 0 {
if element.is_multiple_of(4) {
let start = (address & 0xFFF) as usize;
device.rsp.mem[start..start + 4].copy_from_slice(
&get_vpr32(device.rsp.cpu.vpr[rt(opcode) as usize], element / 4).to_be_bytes(),
@@ -1025,7 +1019,7 @@ pub fn sdv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 8 == 0 {
if element.is_multiple_of(8) {
let start = (address & 0xFFF) as usize;
device.rsp.mem[start..start + 8].copy_from_slice(
&get_vpr64(device.rsp.cpu.vpr[rt(opcode) as usize], element / 8).to_be_bytes(),
@@ -1047,7 +1041,7 @@ pub fn sqv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element == 0 && address % 16 == 0 {
if element == 0 && address.is_multiple_of(16) {
let start = (address & 0xFFF) as usize;
device.rsp.mem[start..start + 16]
.copy_from_slice(&get_vpr128(device.rsp.cpu.vpr[rt(opcode) as usize]).to_be_bytes());
+12 -3
View File
@@ -86,9 +86,13 @@ fn copy_pif_rdram(device: &mut device::Device) {
let mut i = 0;
while i < device::pif::PIF_RAM_SIZE {
let data = u32::from_ne_bytes(
device.rdram.mem[dram_addr + i..dram_addr + i + 4]
device
.rdram
.mem
.get(dram_addr + i..dram_addr + i + 4)
.unwrap_or_default()
.try_into()
.unwrap(),
.unwrap_or_default(),
);
device.pif.ram[i..i + 4].copy_from_slice(&data.to_be_bytes());
i += 4;
@@ -97,7 +101,12 @@ fn copy_pif_rdram(device: &mut device::Device) {
let mut i = 0;
while i < device::pif::PIF_RAM_SIZE {
let data = u32::from_be_bytes(device.pif.ram[i..i + 4].try_into().unwrap());
device.rdram.mem[dram_addr + i..dram_addr + i + 4].copy_from_slice(&data.to_ne_bytes());
device
.rdram
.mem
.get_mut(dram_addr + i..dram_addr + i + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&data.to_ne_bytes());
i += 4;
}
} else {
+2 -2
View File
@@ -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
View File
@@ -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) {}
+28 -20
View File
@@ -1,4 +1,4 @@
use crate::{device, netplay, ui};
use crate::{cheats, device, netplay, ui};
use governor::clock::Clock;
const VI_STATUS_REG: u32 = 0;
@@ -72,10 +72,9 @@ fn set_vertical_interrupt(device: &mut device::Device) {
fn set_current_line(device: &mut device::Device) {
let delay = device.vi.delay;
let next_vi = device::events::get_event(device, device::events::EVENT_TYPE_VI);
if next_vi.is_some() {
if let Some(next_vi) = next_vi {
device.vi.regs[VI_CURRENT_REG as usize] = ((delay
- (next_vi.unwrap().count
- device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize]))
- (next_vi.count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize]))
/ device.vi.count_per_scanline)
as u32;
@@ -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(
@@ -134,16 +134,22 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
}
pub fn vertical_interrupt_event(device: &mut device::Device) {
ui::video::check_callback(device);
if device.cheats.enabled {
cheats::execute_cheats(device, device.cheats.cheats.clone());
}
let mut speed_limiter_toggled = ui::video::check_callback(device);
if device.netplay.is_some() {
netplay::send_sync_check(device);
device.vi.enable_speed_limiter = !device.netplay.as_ref().unwrap().fast_forward;
if device.vi.enable_speed_limiter == device.netplay.as_ref().unwrap().fast_forward {
speed_limiter_toggled = true;
device.vi.enable_speed_limiter = !device.netplay.as_ref().unwrap().fast_forward;
}
}
device.vi.vi_counter += 1;
if device.vi.vi_counter % device.vi.limit_freq == 0 && device.vi.enable_speed_limiter {
speed_limiter(device);
if device.vi.vi_counter.is_multiple_of(device.vi.limit_freq) && device.vi.enable_speed_limiter {
speed_limiter(device, speed_limiter_toggled);
}
ui::video::update_screen();
@@ -175,10 +181,9 @@ pub fn init(device: &mut device::Device) {
}
}
fn speed_limiter(device: &mut device::Device) {
fn speed_limiter(device: &mut device::Device, speed_limiter_toggled: bool) {
let result = device.vi.limiter.as_ref().unwrap().check();
if result.is_err() {
let outcome = result.unwrap_err();
if let Err(outcome) = result {
let dur = outcome.wait_time_from(governor::clock::DefaultClock::default().now());
spin_sleep::sleep(dur);
if dur < device.vi.min_wait_time {
@@ -195,14 +200,17 @@ fn speed_limiter(device: &mut device::Device) {
.as_secs_f64()
> 1.0
{
if device.vi.min_wait_time.as_secs_f64() == 0.0 && device.vi.limit_freq < MAX_LIMIT_FREQ {
device.vi.limit_freq += 1;
create_limiter(device);
} else if device.vi.min_wait_time.as_secs_f64() > device.vi.frame_time
&& device.vi.limit_freq > 1
{
device.vi.limit_freq -= 1;
create_limiter(device);
if !speed_limiter_toggled {
if device.vi.min_wait_time.as_secs_f64() == 0.0 && device.vi.limit_freq < MAX_LIMIT_FREQ
{
device.vi.limit_freq += 1;
create_limiter(device);
} else if device.vi.min_wait_time.as_secs_f64() > device.vi.frame_time
&& device.vi.limit_freq > 1
{
device.vi.limit_freq -= 1;
create_limiter(device);
}
}
//println!("limit freq: {}", device.vi.limit_freq);
+33 -55
View File
@@ -3,6 +3,7 @@
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
mod cheats;
mod device;
mod netplay;
mod savestates;
@@ -73,10 +74,6 @@ async fn main() {
if result.is_err() {
panic!("could not create config dir: {}", result.err().unwrap())
}
result = std::fs::create_dir_all(dirs.cache_dir.clone());
if result.is_err() {
panic!("could not create cache dir: {}", result.err().unwrap())
}
result = std::fs::create_dir_all(dirs.data_dir.join("saves"));
if result.is_err() {
panic!("could not create save dir: {}", result.err().unwrap())
@@ -85,13 +82,6 @@ async fn main() {
if result.is_err() {
panic!("could not create state dir: {}", result.err().unwrap())
}
let running_file = dirs.cache_dir.join("game_running");
if running_file.exists() {
result = std::fs::remove_file(running_file);
if result.is_err() {
panic!("could not remove running file: {}", result.err().unwrap())
}
}
let args = Args::parse();
let args_as_strings: Vec<String> = std::env::args().collect();
@@ -111,9 +101,9 @@ async fn main() {
}
}
if args.list_controllers {
let controllers = gui::get_controller_names(&ui);
let controllers = ui::input::get_controller_names(&ui);
for (i, controller) in controllers.iter().enumerate() {
println!("Controller {}: {}", i, controller);
println!("Controller {i}: {controller}");
}
return;
}
@@ -151,51 +141,39 @@ 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;
let disable_expansion_pak = device.ui.config.emulation.disable_expansion_pak;
let game_cheats = {
let game_crc = ui::storage::get_game_crc(&rom_contents);
let cheats = ui::config::Cheats::new();
cheats.cheats.get(&game_crc).cloned().unwrap_or_default()
};
device::run_game(
&mut device,
rom_contents,
ui::gui::GameSettings {
fullscreen: args.fullscreen,
overclock,
disable_expansion_pak,
cheats: game_cheats,
},
);
})
.unwrap();
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 {
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();
}
}
+6 -11
View File
@@ -113,7 +113,7 @@ pub fn receive_save(netplay: &mut Netplay, save_type: &str, save_data: &mut Vec<
pub fn send_sync_check(device: &mut device::Device) {
let netplay = device.netplay.as_mut().unwrap();
if netplay.vi_counter % 600 == 0 {
if netplay.vi_counter.is_multiple_of(600) {
let mut request: Vec<u8> = [UDP_SYNC_DATA].to_vec();
request.extend_from_slice(&(netplay.vi_counter).to_be_bytes());
@@ -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,18 +246,13 @@ 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))
.expect("couldn't bind to address");
socket2::SockRef::from(&udp_socket)
.set_tos(CS4 << 2)
.set_tos_v4(CS4 << 2)
.unwrap();
} else {
udp_socket = std::net::UdpSocket::bind((std::net::Ipv6Addr::UNSPECIFIED, 0))
+113 -104
View File
@@ -12,7 +12,7 @@ impl<'de, const N: usize> Visitor<'de> for M128iArrayVisitor<N> {
type Value = [__m128i; N];
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str(&format!("an array of {} 128-bit integers", N))
formatter.write_str(&format!("an array of {N} 128-bit integers"))
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
@@ -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,121 @@ 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.cheats = state.cheats;
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 +220,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]
}
+17 -13
View File
@@ -1,27 +1,29 @@
pub mod audio;
pub mod cheats;
pub mod config;
pub mod gui;
pub mod input;
pub mod netplay;
pub mod storage;
pub mod video;
pub mod vru;
pub struct Dirs {
pub config_dir: std::path::PathBuf,
pub cache_dir: std::path::PathBuf,
pub data_dir: std::path::PathBuf,
}
pub struct Audio {
pub audio_device_spec: sdl3_sys::audio::SDL_AudioSpec,
pub audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
pub event_audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
pub audio_device: u32,
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 +55,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 +68,7 @@ pub fn sdl_init(flag: sdl3_sys::init::SDL_InitFlags) {
let err = std::ffi::CStr::from_ptr(sdl3_sys::error::SDL_GetError())
.to_str()
.unwrap();
panic!("Could not initialize SDL subsystem: {}, {}", flag, err);
panic!("Could not initialize SDL subsystem: {flag}, {err}");
}
}
}
@@ -77,27 +78,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 +122,6 @@ impl Ui {
},
],
keyboard_state: std::ptr::null_mut(),
num_joysticks,
joysticks,
},
storage: Storage {
@@ -184,10 +180,13 @@ impl Ui {
include_bytes!("../data/netplay_p3_disconnected.wav").to_vec(),
include_bytes!("../data/netplay_p4_disconnected.wav").to_vec(),
],
cheats_enabled: include_bytes!("../data/cheats_enabled.wav").to_vec(),
},
audio_device_spec: Default::default(),
audio_stream: std::ptr::null_mut(),
event_audio_stream: std::ptr::null_mut(),
audio_device: 0,
gain: 1.0,
},
video: Video {
window: std::ptr::null_mut(),
@@ -199,7 +198,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 +208,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)
}
}
+87 -32
View File
@@ -9,16 +9,12 @@ pub struct EventAudio {
pub netplay_desync: Vec<u8>,
pub netplay_lost_connection: Vec<u8>,
pub netplay_disconnected: [Vec<u8>; 4],
pub cheats_enabled: Vec<u8>,
}
pub fn init(ui: &mut ui::Ui, frequency: u64) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_AUDIO);
let game_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: frequency as i32,
channels: 2,
};
let device_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: 48000,
@@ -34,49 +30,61 @@ pub fn init(ui: &mut ui::Ui, frequency: u64) {
panic!("Could not open audio device");
}
let mut dst = Default::default();
if !unsafe {
sdl3_sys::audio::SDL_GetAudioDeviceFormat(
ui.audio.audio_device,
&mut dst,
&mut ui.audio.audio_device_spec,
std::ptr::null_mut(),
)
} {
panic!("Could not get audio device format");
}
ui.audio.audio_stream =
unsafe { sdl3_sys::audio::SDL_CreateAudioStream(&game_audio_spec, &dst) };
if ui.audio.audio_stream.is_null() {
return;
}
if !unsafe {
sdl3_sys::audio::SDL_BindAudioStream(ui.audio.audio_device, ui.audio.audio_stream)
} {
panic!("Could not bind audio stream");
}
let wav_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: 24000,
channels: 1,
};
ui.audio.event_audio_stream =
unsafe { sdl3_sys::audio::SDL_CreateAudioStream(&wav_audio_spec, &dst) };
ui.audio.event_audio_stream = unsafe {
sdl3_sys::audio::SDL_CreateAudioStream(&wav_audio_spec, &ui.audio.audio_device_spec)
};
if !unsafe {
sdl3_sys::audio::SDL_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");
}
init_game_audio(ui, frequency);
}
pub fn init_game_audio(ui: &mut ui::Ui, frequency: u64) {
let game_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: frequency as i32,
channels: 2,
};
ui.audio.audio_stream = unsafe {
sdl3_sys::audio::SDL_CreateAudioStream(&game_audio_spec, &ui.audio.audio_device_spec)
};
if ui.audio.audio_stream.is_null() {
return;
}
if !unsafe {
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain)
&& sdl3_sys::audio::SDL_BindAudioStream(ui.audio.audio_device, ui.audio.audio_stream)
} {
panic!("Could not bind audio stream");
}
}
pub fn close(ui: &mut ui::Ui) {
close_game_audio(ui);
unsafe {
if !ui.audio.audio_stream.is_null() {
sdl3_sys::audio::SDL_DestroyAudioStream(ui.audio.audio_stream);
ui.audio.audio_stream = std::ptr::null_mut();
}
if !ui.audio.event_audio_stream.is_null() {
sdl3_sys::audio::SDL_DestroyAudioStream(ui.audio.event_audio_stream);
ui.audio.event_audio_stream = std::ptr::null_mut();
@@ -86,7 +94,54 @@ pub fn close(ui: &mut ui::Ui) {
}
}
pub fn play_netplay_audio(ui: &mut ui::Ui, error: u32) {
pub fn close_game_audio(ui: &mut ui::Ui) {
unsafe {
if !ui.audio.audio_stream.is_null() {
sdl3_sys::audio::SDL_DestroyAudioStream(ui.audio.audio_stream);
ui.audio.audio_stream = std::ptr::null_mut();
}
}
}
pub fn lower_audio_volume(ui: &mut ui::Ui) {
unsafe {
ui.audio.gain = sdl3_sys::audio::SDL_GetAudioStreamGain(ui.audio.audio_stream) - 0.05;
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_cheat_event(ui: &ui::Ui) {
if ui.audio.event_audio_stream.is_null() {
return;
}
let sound = &ui.audio.event_audio.cheats_enabled;
if !unsafe {
sdl3_sys::audio::SDL_PutAudioStreamData(
ui.audio.event_audio_stream,
sound.as_ptr() as *const std::ffi::c_void,
sound.len() as i32,
)
} {
panic!("Could not play audio");
}
}
pub fn play_netplay_audio(ui: &ui::Ui, error: u32) {
if ui.audio.event_audio_stream.is_null() {
return;
}
@@ -118,7 +173,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 +195,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;
}
@@ -149,12 +204,12 @@ pub fn play_audio(device: &mut device::Device, dram_addr: usize, length: u64) {
let mut i = 0;
while i < length as usize / 2 {
// Left channel
primary_buffer[i] = device.rdram.mem[dram_addr + (i * 2) + 2] as i16
| ((device.rdram.mem[dram_addr + (i * 2) + 3] as i16) << 8);
primary_buffer[i] = *device.rdram.mem.get(dram_addr + (i * 2) + 2).unwrap_or(&0) as i16
| ((*device.rdram.mem.get(dram_addr + (i * 2) + 3).unwrap_or(&0) as i16) << 8);
// Right channel
primary_buffer[i + 1] = device.rdram.mem[dram_addr + (i * 2)] as i16
| ((device.rdram.mem[dram_addr + (i * 2) + 1] as i16) << 8);
primary_buffer[i + 1] = *device.rdram.mem.get(dram_addr + (i * 2)).unwrap_or(&0) as i16
| ((*device.rdram.mem.get(dram_addr + (i * 2) + 1).unwrap_or(&0) as i16) << 8);
i += 2;
}
+148
View File
@@ -0,0 +1,148 @@
use crate::device;
use crate::ui;
use crate::ui::gui::AppWindow;
use crate::ui::gui::ErrorDialog;
use slint::ComponentHandle;
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct CheatData {
note: String,
pub data: Vec<String>,
pub options: Option<std::collections::BTreeMap<String, String>>,
}
pub type Cheats = std::collections::BTreeMap<String, std::collections::BTreeMap<String, CheatData>>;
pub fn cheats_window(app: &AppWindow) {
let weak = app.as_weak();
app.on_cheats_select_rom_clicked(move || {
let select_rom = rfd::AsyncFileDialog::new()
.set_title("Select ROM")
.pick_file();
let weak = weak.clone();
tokio::spawn(async move {
if let Some(file) = select_rom.await
&& let Some(rom_contents) = device::get_rom_contents(file.path())
{
let game_crc = ui::storage::get_game_crc(&rom_contents);
let game_name = ui::storage::get_game_name(&rom_contents);
weak.upgrade_in_event_loop(move |handle| {
handle.set_cheat_game_name(game_name.into());
})
.unwrap();
let cheats: Cheats =
serde_json::from_slice(include_bytes!("../../data/cheats.json")).unwrap();
if let Some(cheat) = cheats.get(&game_crc) {
let cheat = cheat.clone();
weak.upgrade_in_event_loop(move |handle| {
let cheat_settings = ui::config::Cheats::new();
let game_cheats = cheat_settings.cheats.get(&game_crc).cloned();
handle.set_cheat_game_crc(game_crc.into());
let cheats_vec = slint::VecModel::default();
for item in cheat.iter() {
let mut cheat_enabled = false;
if let Some(game_cheat) = game_cheats.as_ref()
&& game_cheat.contains_key(item.0)
{
cheat_enabled = true;
}
let options_vec = slint::VecModel::default();
if let Some(options) = item.1.options.as_ref() {
for option in options.iter() {
let mut option_enabled = false;
if let Some(game_cheat) = game_cheats.as_ref()
&& game_cheat.contains_key(item.0)
&& let Some(opt) = game_cheat.get(item.0)
&& let Some(opt) = opt
&& opt == option.0
{
option_enabled = true;
}
options_vec.push((option_enabled, option.0.into()));
}
}
let options = slint::ModelRc::from(std::rc::Rc::new(options_vec));
cheats_vec.push((
item.0.clone().into(),
cheat_enabled,
item.1.clone().note.into(),
options,
));
}
#[allow(clippy::type_complexity)]
let cheats_model: std::rc::Rc<
slint::VecModel<(
slint::SharedString,
bool,
slint::SharedString,
slint::ModelRc<(bool, slint::SharedString)>,
)>,
> = std::rc::Rc::new(cheats_vec);
handle.set_cheats(slint::ModelRc::from(cheats_model));
})
.unwrap();
} else {
clear_cheats(&weak, false);
}
} else {
clear_cheats(&weak, true);
weak.upgrade_in_event_loop(move |_handle| {
let message_dialog = ErrorDialog::new().unwrap();
let weak_dialog = message_dialog.as_weak();
message_dialog.on_close_clicked(move || {
weak_dialog.unwrap().window().hide().unwrap();
});
message_dialog.set_text("Could not read ROM".into());
message_dialog.show().unwrap();
})
.unwrap();
}
});
});
let weak = app.as_weak();
app.on_cheats_clear_clicked(move || {
let mut cheats = ui::config::Cheats::new();
cheats.cheats.clear();
clear_cheats(&weak, true);
});
app.on_cheat_toggled(move |game_crc, cheat_name, option, enabled| {
let mut cheats = ui::config::Cheats::new();
let mut game_cheats = cheats
.cheats
.remove(&game_crc.to_string())
.unwrap_or_default();
if enabled {
let cheat_option = if option.is_empty() {
None
} else {
Some(option.into())
};
game_cheats.insert(cheat_name.into(), cheat_option);
} else {
game_cheats.remove(&cheat_name.to_string());
}
cheats.cheats.insert(game_crc.into(), game_cheats);
});
}
fn clear_cheats(weak: &slint::Weak<AppWindow>, clear_name: bool) {
weak.upgrade_in_event_loop(move |handle| {
let cheats_vec = slint::VecModel::default();
#[allow(clippy::type_complexity)]
let cheats_model: std::rc::Rc<
slint::VecModel<(
slint::SharedString,
bool,
slint::SharedString,
slint::ModelRc<(bool, slint::SharedString)>,
)>,
> = std::rc::Rc::new(cheats_vec);
handle.set_cheats(slint::ModelRc::from(cheats_model));
if clear_name {
handle.set_cheat_game_name("".into());
}
})
.unwrap();
}
+45 -5
View File
@@ -33,7 +33,7 @@ pub struct InputProfile {
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Input {
pub input_profiles: std::collections::HashMap<String, InputProfile>,
pub input_profiles: std::collections::BTreeMap<String, InputProfile>,
pub input_profile_binding: [String; 4],
pub controller_assignment: [Option<String>; 4],
pub controller_enabled: [bool; 4],
@@ -53,6 +53,13 @@ pub struct Video {
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Emulation {
pub overclock: bool,
pub disable_expansion_pak: bool,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Cheats {
pub cheats:
std::collections::HashMap<String, std::collections::HashMap<String, Option<String>>>,
}
#[derive(serde::Serialize, serde::Deserialize)]
@@ -62,6 +69,36 @@ pub struct Config {
pub emulation: Emulation,
}
impl Drop for Cheats {
fn drop(&mut self) {
write_cheats(self);
}
}
fn write_cheats(cheats: &Cheats) {
let dirs = ui::get_dirs();
let file_path = dirs.config_dir.join("cheats.json");
let f = std::fs::File::create(file_path).unwrap();
serde_json::to_writer_pretty(f, &cheats).unwrap();
}
impl Cheats {
pub fn new() -> Cheats {
let dirs = ui::get_dirs();
let file_path = dirs.config_dir.join("cheats.json");
let cheats_file = std::fs::read(file_path);
if let Ok(cheats_file) = cheats_file {
let result = serde_json::from_slice(cheats_file.as_ref());
if let Ok(result) = result {
return result;
}
}
Cheats {
cheats: std::collections::HashMap::new(),
}
}
}
impl Drop for Config {
fn drop(&mut self) {
write_config(self);
@@ -80,13 +117,13 @@ impl Config {
let dirs = ui::get_dirs();
let file_path = dirs.config_dir.join("config.json");
let config_file = std::fs::read(file_path);
if config_file.is_ok() {
let result = serde_json::from_slice(config_file.unwrap().as_ref());
if let Ok(config_file) = config_file {
let result = serde_json::from_slice(config_file.as_ref());
if let Ok(result) = result {
return result;
}
}
let mut input_profiles = std::collections::HashMap::new();
let mut input_profiles = std::collections::BTreeMap::new();
input_profiles.insert("default".to_string(), ui::input::get_default_profile());
Config {
input: Input {
@@ -109,7 +146,10 @@ impl Config {
widescreen: false,
crt: false,
},
emulation: Emulation { overclock: false },
emulation: Emulation {
overclock: false,
disable_expansion_pak: false,
},
}
}
}
+422 -632
View File
File diff suppressed because it is too large Load Diff
+98
View File
@@ -0,0 +1,98 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Button, HorizontalBox, VerticalBox } from "std-widgets.slint";
import { Page } from "page.slint";
export global AboutData {
in property <string> version;
in property <bool> has_update;
}
export component NewVersionButton {
callback newversion_button_clicked;
HorizontalBox {
alignment: center;
Button {
text: @tr("New version available! Click here to download");
primary: true;
clicked => {
newversion_button_clicked();
}
}
}
}
export component About inherits Page {
callback wiki_button_clicked;
callback discord_button_clicked;
callback newversion_button_clicked;
callback patreon_button_clicked;
callback github_sponsors_button_clicked;
callback source_code_button_clicked;
title: @tr("About");
VerticalBox {
alignment: start;
HorizontalBox {
alignment: center;
Button {
text: @tr("Wiki");
clicked => {
wiki_button_clicked();
}
}
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Discord Server");
clicked => {
discord_button_clicked();
}
}
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Source Code");
clicked => {
source_code_button_clicked();
}
}
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Donate (via Patreon)");
clicked => {
patreon_button_clicked();
}
}
Button {
text: @tr("Donate (via GitHub)");
clicked => {
github_sponsors_button_clicked();
}
}
}
HorizontalBox {
alignment: center;
Text {
text: AboutData.version;
}
}
if AboutData.has_update:NewVersionButton {
newversion_button_clicked => {
newversion_button_clicked();
}
}
Rectangle { }
}
}
+149
View File
@@ -0,0 +1,149 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { StandardButton } from "std-widgets.slint";
import { LocalGame, ControllerConfig, Netplay, Cheats, Settings, About } from "pages.slint";
import { SideBar } from "side_bar.slint";
import { SettingsData } from "settings_page.slint";
import { AboutData } from "about_page.slint";
import { ControllerData } from "controller_page.slint";
import { State } from "localgame_page.slint";
import { CheatsData } from "cheat_page.slint";
export { InputProfileDialog } from "input_profile.slint";
export { VruDialog } from "vru_dialog.slint";
export { NetplayCreate } from "netplay_create.slint";
export { NetplayJoin } from "netplay_join.slint";
export { NetplayWait } from "netplay_wait.slint";
export component AppWindow inherits Window {
callback wiki_button_clicked;
callback discord_button_clicked;
callback newversion_button_clicked;
callback patreon_button_clicked;
callback github_sponsors_button_clicked;
callback source_code_button_clicked;
callback open_rom_button_clicked;
callback saves_folder_button_clicked;
callback input_profile_button_clicked;
callback create_session_button_clicked;
callback join_session_button_clicked;
callback netplay_discord_button_clicked;
callback netplay_feedback_button_clicked;
callback cheats_select_rom_clicked;
callback cheats_clear_clicked;
callback cheat_toggled(string, string, string, bool);
in property version <=> AboutData.version;
in property has_update <=> AboutData.has_update;
in-out property integer_scaling <=> SettingsData.integer_scaling;
in-out property fullscreen <=> SettingsData.fullscreen;
in-out property widescreen <=> SettingsData.widescreen;
in-out property apply_crt_shader <=> SettingsData.apply_crt_shader;
in-out property overclock_n64_cpu <=> SettingsData.overclock_n64_cpu;
in-out property disable_expansion_pak <=> SettingsData.disable_expansion_pak;
in-out property resolution <=> SettingsData.resolution;
in-out property emulate_vru <=> ControllerData.emulate_vru;
in-out property controller_enabled <=> ControllerData.controller_enabled;
in-out property transferpak <=> ControllerData.transferpak;
in-out property selected_profile_binding <=> ControllerData.selected_profile_binding;
in-out property input_profiles <=> ControllerData.input_profiles;
in-out property controller_names <=> ControllerData.controller_names;
in-out property selected_controller <=> ControllerData.selected_controller;
in-out property game_running <=> State.game_running;
in-out property blank_profiles <=> ControllerData.blank_profiles;
in-out property cheat_game_name <=> CheatsData.cheat_game_name;
in-out property cheat_game_crc <=> CheatsData.cheat_game_crc;
in-out property cheats <=> CheatsData.cheats;
preferred-width: 700px;
preferred-height: 500px;
title: @tr("Gopher64");
icon: @image-url("../../../data/gopher64.png");
HorizontalLayout {
side-bar := SideBar {
title: @tr("Gopher64");
model: [
@tr("Menu" => "Local Game"),
@tr("Menu" => "Controller Config"),
@tr("Menu" => "Netplay"),
@tr("Menu" => "Cheats"),
@tr("Menu" => "Settings"),
@tr("Menu" => "About")
];
}
if(side-bar.current-item == 0): LocalGame {
open_rom_button_clicked => {
open_rom_button_clicked();
}
saves_folder_button_clicked => {
saves_folder_button_clicked();
}
newversion_button_clicked => {
newversion_button_clicked();
}
}
if(side-bar.current-item == 1): ControllerConfig {
input_profile_button_clicked => {
input_profile_button_clicked();
}
}
if(side-bar.current-item == 2): Netplay {
create_session_button_clicked => {
create_session_button_clicked();
}
join_session_button_clicked => {
join_session_button_clicked();
}
netplay_discord_button_clicked => {
netplay_discord_button_clicked();
}
netplay_feedback_button_clicked => {
netplay_feedback_button_clicked();
}
}
if(side-bar.current-item == 3): Cheats {
cheats_select_rom_clicked => {
cheats_select_rom_clicked();
}
cheats_clear_clicked => {
cheats_clear_clicked();
}
cheat_toggled(game_crc, cheat_name, option, enabled) => {
cheat_toggled(game_crc,cheat_name,option,enabled);
}
}
if(side-bar.current-item == 4): Settings { }
if(side-bar.current-item == 5): About {
wiki_button_clicked => {
wiki_button_clicked();
}
discord_button_clicked => {
discord_button_clicked();
}
newversion_button_clicked => {
newversion_button_clicked();
}
patreon_button_clicked => {
patreon_button_clicked();
}
github_sponsors_button_clicked => {
github_sponsors_button_clicked();
}
source_code_button_clicked => {
source_code_button_clicked();
}
}
}
}
export component ErrorDialog inherits Dialog {
in-out property <string> text;
title: @tr("Error");
Text {
text: text;
}
StandardButton {
kind: close;
}
}
+89
View File
@@ -0,0 +1,89 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { ListView, HorizontalBox, VerticalBox, Button, GroupBox, CheckBox } from "std-widgets.slint";
import { Page } from "page.slint";
export global CheatsData {
in-out property <string> cheat_game_name;
in-out property <string> cheat_game_crc;
in-out property <[{cheat_name:string, notes:string, enabled:bool, options:[{option:string, enabled:bool}]}]> cheats;
}
export component Cheats inherits Page {
title: @tr("Cheats");
callback cheats_select_rom_clicked;
callback cheats_clear_clicked;
callback cheat_toggled(string, string, string, bool);
VerticalBox {
alignment: start;
HorizontalBox {
alignment: start;
if CheatsData.cheat_game_name == "": Button {
text: @tr("Select ROM");
clicked => {
cheats_select_rom_clicked();
}
}
if CheatsData.cheat_game_name != "": Button {
text: CheatsData.cheat_game_name;
clicked => {
cheats_select_rom_clicked();
}
}
Rectangle {
width: 20px;
}
Button {
text: @tr("Clear Cheats For All ROMs");
clicked => {
cheats_clear_clicked();
}
}
}
Rectangle { }
if CheatsData.cheats.length > 0:ListView {
preferred-height: 500px;
preferred-width: 520px;
for data in CheatsData.cheats: VerticalBox {
Text {
text: data.cheat_name;
font-size: 16px;
wrap: word-wrap;
}
if data.notes != "":Text {
text: data.notes;
wrap: word-wrap;
}
if data.options.length == 0: CheckBox {
text: @tr("Enabled");
checked: data.enabled;
toggled => {
data.enabled = self.checked;
cheat_toggled(CheatsData.cheat_game_crc,data.cheat_name,"",self.checked);
}
}
for item[index] in data.options: CheckBox {
text: item.option;
enabled: !data.enabled || (data.enabled && item.enabled);
checked: item.enabled;
toggled => {
item.enabled = self.checked;
data.enabled = self.checked;
cheat_toggled(CheatsData.cheat_game_crc,data.cheat_name,item.option,self.checked);
}
}
Rectangle { }
}
}
if CheatsData.cheats.length == 0 && CheatsData.cheat_game_name != "": Text {
text: @tr("No Cheats Available");
}
}
}
+120
View File
@@ -0,0 +1,120 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { CheckBox, ComboBox, Button, HorizontalBox, VerticalBox, GroupBox } from "std-widgets.slint";
import { Page } from "page.slint";
import { State } from "localgame_page.slint";
export global ControllerData {
in-out property <bool> emulate_vru;
in-out property <[bool]> controller_enabled;
in-out property <[bool]> transferpak;
in-out property <[int]> selected_profile_binding: [-1, -1, -1, -1];
in-out property <[string]> input_profiles;
in-out property <[string]> controller_names;
in-out property <[int]> selected_controller;
in-out property <bool> blank_profiles;
}
export component ControllerConfig inherits Page {
callback input_profile_button_clicked;
title: @tr("Controller Configuration");
VerticalBox {
alignment: start;
HorizontalBox {
alignment: start;
Button {
text: @tr("Configure Input Profile");
enabled: !State.game_running;
clicked => {
input_profile_button_clicked();
}
}
}
Rectangle { }
HorizontalBox {
GroupBox {
title: @tr("Port");
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: Text {
text: player + 1;
vertical-alignment: center;
}
}
}
GroupBox {
title: @tr("Enabled");
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: CheckBox {
checked: ControllerData.controller_enabled[player];
toggled => {
ControllerData.controller_enabled[player] = self.checked;
}
}
}
}
GroupBox {
title: @tr("Emulate VRU");
VerticalLayout {
alignment: space-between;
for valid in [false, false, false, true]: CheckBox {
enabled: valid;
checked: valid && ControllerData.emulate_vru;
toggled => {
ControllerData.emulate_vru = self.checked;
}
}
}
}
GroupBox {
title: @tr("Transfer Pak");
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: CheckBox {
checked: ControllerData.transferpak[player];
toggled => {
ControllerData.transferpak[player] = self.checked;
}
}
}
}
if !ControllerData.blank_profiles: GroupBox {
title: @tr("Profile");
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: ComboBox {
model: ControllerData.input_profiles;
current-index: ControllerData.selected_profile_binding[player];
selected => {
ControllerData.selected_profile_binding[player] = self.current-index;
}
}
}
}
GroupBox {
title: @tr("Controller");
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: ComboBox {
model: ControllerData.controller_names;
width: self.preferred-width;
current-index: ControllerData.selected_controller[player];
selected => {
ControllerData.selected_controller[player] = self.current-index;
}
}
}
}
}
}
}
-942
View File
@@ -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();
};
});
}
+45
View File
@@ -0,0 +1,45 @@
import { StandardButton, HorizontalBox, VerticalBox, Button, LineEdit, CheckBox } from "std-widgets.slint";
export global InputProfileData {
in-out property <bool> dinput;
in-out property <string> profile_name;
}
export component InputProfileDialog inherits Window {
title: @tr("Create Input Profile");
callback profile_creation_button_clicked;
in-out property dinput <=> InputProfileData.dinput;
in-out property profile_name <=> InputProfileData.profile_name;
VerticalBox {
HorizontalBox {
LineEdit {
placeholder-text: @tr("Enter profile name");
preferred-width: 300px;
edited(text) => {
InputProfileData.profile_name = text;
}
}
}
HorizontalBox {
alignment: center;
CheckBox {
text: @tr("Use DirectInput");
checked: InputProfileData.dinput;
toggled => {
InputProfileData.dinput = self.checked;
}
}
}
HorizontalBox {
Button {
text: @tr("Start Profile Creation Wizard");
enabled: InputProfileData.profile_name != "" && InputProfileData.profile_name != "default";
clicked => {
profile_creation_button_clicked();
}
}
}
}
}
+48
View File
@@ -0,0 +1,48 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Button, HorizontalBox, VerticalBox } from "std-widgets.slint";
import { Page } from "page.slint";
import { AboutData, NewVersionButton } from "about_page.slint";
export global State {
in-out property <bool> game_running;
}
export component LocalGame inherits Page {
callback open_rom_button_clicked;
callback saves_folder_button_clicked;
callback newversion_button_clicked;
title: @tr("Local Game");
VerticalBox {
alignment: start;
HorizontalBox {
alignment: center;
Button {
text: @tr("Open ROM");
enabled: !State.game_running;
clicked => {
open_rom_button_clicked();
}
}
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Open Saves Folder");
clicked => {
saves_folder_button_clicked();
}
}
}
if AboutData.has_update:NewVersionButton {
newversion_button_clicked => {
newversion_button_clicked();
}
}
Rectangle { }
}
}
+119
View File
@@ -0,0 +1,119 @@
import { LineEdit, HorizontalBox, VerticalBox, Button, ComboBox } from "std-widgets.slint";
import { NetplayData } from "netplay_page.slint";
export component NetplayCreate inherits Window {
title: @tr("Netplay: Create Session");
in-out property server_names <=> NetplayData.server_names;
in-out property server_urls <=> NetplayData.server_urls;
in-out property ping <=> NetplayData.ping;
in-out property game_name <=> NetplayData.game_name;
in-out property game_hash <=> NetplayData.game_hash;
in-out property game_cheats <=> NetplayData.game_cheats;
in-out property pending_session <=> NetplayData.pending_session;
in-out property rom_path <=> NetplayData.rom_path;
in-out property peer_addr <=> NetplayData.peer_addr;
callback get_ping(string);
callback select_rom;
callback create_session(string, string, string, string, string, string, string);
VerticalBox {
HorizontalBox {
VerticalBox {
Text {
text: @tr("Session Name:");
}
LineEdit {
placeholder_text: @tr("Enter session name");
edited(text) => {
NetplayData.session_name = text;
}
}
}
VerticalBox {
Text {
text: @tr("Password (Optional):");
}
LineEdit {
placeholder_text: @tr("Enter session password");
edited(text) => {
NetplayData.password = text;
}
}
}
}
VerticalBox {
Text {
text: @tr("Player Name:");
}
LineEdit {
placeholder_text: @tr("Enter player name");
edited(text) => {
NetplayData.player_name = text;
}
}
}
VerticalBox {
Text {
text: @tr("Server:");
}
HorizontalBox {
server := ComboBox {
model: NetplayData.server_names;
selected => {
if (self.current-index >= 0) {
get_ping(NetplayData.server_urls[self.current-index]);
}
}
}
Text {
vertical-alignment: center;
text: NetplayData.ping;
}
}
}
if NetplayData.game_name == "": Button {
text: @tr("Select ROM");
clicked => {
select_rom();
}
}
if NetplayData.game_name != "": Button {
text: NetplayData.game_name;
clicked => {
select_rom();
}
}
Rectangle { }
if NetplayData.game_cheats != "":Text {
text: @tr("This game has cheats enabled. Cheats will be synchronized with other players.");
}
Rectangle { }
Button {
text: @tr("Create Session");
enabled: NetplayData.session_name != "" && NetplayData.player_name != "" && NetplayData.game_name != "" && !NetplayData.pending_session;
clicked => {
NetplayData.pending_session = true;
create_session(
NetplayData.server_urls[server.current-index],
NetplayData.session_name,
NetplayData.player_name,
NetplayData.game_name,
NetplayData.game_hash,
NetplayData.game_cheats,
NetplayData.password);
}
}
}
}
+131
View File
@@ -0,0 +1,131 @@
import { VerticalBox, LineEdit, StandardTableView, HorizontalBox, ComboBox, Button } from "std-widgets.slint";
import { NetplayData } from "netplay_page.slint";
export component NetplayJoin inherits Window {
title: @tr("Netplay: Join Session");
in-out property server_names <=> NetplayData.server_names;
in-out property server_urls <=> NetplayData.server_urls;
in-out property ping <=> NetplayData.ping;
in-out property sessions <=> NetplayData.sessions;
in-out property current_session <=> NetplayData.current_session;
in-out property ports <=> NetplayData.ports;
in-out property game_name <=> NetplayData.game_name;
in-out property game_hash <=> NetplayData.game_hash;
in-out property game_cheats <=> NetplayData.game_cheats;
in-out property peer_addr <=> NetplayData.peer_addr;
in-out property pending_session <=> NetplayData.pending_session;
in-out property rom_path <=> NetplayData.rom_path;
callback get_ping(string);
callback refresh_session(string);
callback select_rom;
callback join_session(string, string, string, int);
VerticalBox {
HorizontalBox {
VerticalBox {
Text {
text: @tr("Player Name:");
}
LineEdit {
placeholder_text: @tr("Enter player name");
edited(text) => {
NetplayData.player_name = text;
}
}
}
VerticalBox {
Text {
text: @tr("Server:");
}
HorizontalBox {
server := ComboBox {
model: NetplayData.server_names;
selected => {
if (self.current-index >= 0) {
get_ping(NetplayData.server_urls[self.current-index]);
}
}
}
Text {
vertical-alignment: center;
text: NetplayData.ping;
}
}
}
}
if NetplayData.sessions.length > 0: Text {
text: @tr("Click on a session below to select it");
horizontal-alignment: center;
vertical-alignment: center;
}
if NetplayData.sessions.length == 0: Text {
text: @tr("No sessions available. Please refresh or try a different server.");
horizontal-alignment: center;
vertical-alignment: center;
}
StandardTableView {
columns: [
{ title: "Session Name" },
{ title: "Game Name" },
{ title: "Password" },
{ title: "Cheats" }
];
rows: NetplayData.sessions;
current-row: NetplayData.current_session;
current-row-changed(current-row) => {
NetplayData.current_session = current-row;
}
}
HorizontalBox {
Rectangle { }
Button {
text: @tr("Refresh Session List");
clicked => {
refresh_session(NetplayData.server_urls[server.current-index]);
}
}
}
HorizontalBox {
LineEdit {
placeholder-text: @tr("Password (if required)");
edited(text) => {
NetplayData.password = text;
}
}
if NetplayData.game_name == "": Button {
text: @tr("Select ROM");
clicked => {
select_rom();
}
}
if NetplayData.game_name != "": Button {
text: NetplayData.game_name;
clicked => {
select_rom();
}
}
Button {
text: @tr("Join Session");
enabled: NetplayData.player_name != "" && NetplayData.current_session >= 0 && NetplayData.game_name != "" && !NetplayData.pending_session;
clicked => {
NetplayData.pending_session = true;
join_session(
NetplayData.player_name,
NetplayData.game_hash,
NetplayData.password,
NetplayData.ports[NetplayData.current_session]);
}
}
}
}
}
+75
View File
@@ -0,0 +1,75 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { VerticalBox, HorizontalBox, Button } from "std-widgets.slint";
import { Page } from "page.slint";
export global NetplayData {
in-out property <[string]> server_names;
in-out property <[string]> server_urls;
in-out property <string> ping: "Ping: Unknown";
in-out property <string> player_name;
in-out property <string> game_name;
in-out property <string> game_hash;
in-out property <string> game_cheats;
in-out property <string> rom_path;
in-out property <string> password;
in-out property <string> peer_addr;
in-out property <bool> pending_session;
in-out property <string> session_name;
in-out property <[[StandardListViewItem]]> sessions;
in-out property <[int]> ports;
in-out property <int> current_session: -1;
}
export component Netplay inherits Page {
callback create_session_button_clicked;
callback join_session_button_clicked;
callback netplay_discord_button_clicked;
callback netplay_feedback_button_clicked;
title: @tr("Netplay");
VerticalBox {
alignment: start;
HorizontalBox {
alignment: center;
Button {
text: @tr("Create Session");
clicked => {
create_session_button_clicked();
}
}
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Join Session");
clicked => {
join_session_button_clicked();
}
}
}
Rectangle {
height: 20px;
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Netplay Discord Channel");
clicked => {
netplay_discord_button_clicked();
}
}
Button {
text: @tr("Provide Feedback");
clicked => {
netplay_feedback_button_clicked();
}
}
}
}
}
+103
View File
@@ -0,0 +1,103 @@
import { VerticalBox, HorizontalBox, TextEdit, ScrollView, LineEdit, Button, GridBox } from "std-widgets.slint";
import { NetplayData } from "netplay_page.slint";
export global NetplayWaitData {
in-out property <string> motd;
in-out property <[string]> players;
in-out property <int> port;
in-out property <bool> can_start;
in-out property <string> chat_text;
}
export component NetplayWait inherits Window {
title: @tr("Netplay: Waiting Room");
preferred-width: 400px;
preferred-height: 400px;
in-out property players <=> NetplayWaitData.players;
in-out property session_name <=> NetplayData.session_name;
in-out property game_name <=> NetplayData.game_name;
in-out property rom_path <=> NetplayData.rom_path;
in-out property motd <=> NetplayWaitData.motd;
in-out property port <=> NetplayWaitData.port;
in-out property can_start <=> NetplayWaitData.can_start;
in-out property chat_text <=> NetplayWaitData.chat_text;
callback send_chat_message(string);
callback begin_game;
VerticalBox {
Text {
text: @tr("Session Name: ") + NetplayData.session_name;
font-size: 15px;
horizontal-alignment: center;
}
Text {
text: @tr("Game Name: ") + NetplayData.game_name;
font-size: 15px;
horizontal-alignment: center;
}
GridBox {
Row {
Text {
text: "Player 1: " + players[0];
}
Text {
text: "Player 2: " + players[1];
}
}
Row {
Text {
text: "Player 3: " + players[2];
}
Text {
text: "Player 4: " + players[3];
}
}
}
TextEdit {
read-only: true;
text: NetplayWaitData.chat_text;
}
HorizontalBox {
chat_message := LineEdit {
placeholder_text: @tr("Enter chat message");
key-released(event) => {
if (event.text == Key.Return && chat_message.text != "") {
send_chat_message(chat_message.text);
chat_message.text = "";
}
accept
}
}
Button {
text: @tr("Send Message");
enabled: chat_message.text != "";
clicked => {
send_chat_message(chat_message.text);
chat_message.text = "";
}
}
}
Text {
text: NetplayWaitData.motd;
wrap: word-wrap;
}
Button {
text: @tr("Start Game");
enabled: NetplayWaitData.can_start;
clicked => {
NetplayWaitData.can_start = false;
begin_game();
}
}
}
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Switch, GridBox, ListView, ScrollView, HorizontalBox, VerticalBox, Palette } from "std-widgets.slint";
export component Page inherits VerticalBox {
in property <string> title: "title";
accessible-role: tab-panel;
accessible-label: root.title;
HorizontalBox {
Text {
font-size: 20px;
text <=> root.title;
}
// Spacer
Rectangle { }
}
@children
}
+6
View File
@@ -0,0 +1,6 @@
export { LocalGame } from "localgame_page.slint";
export { ControllerConfig } from "controller_page.slint";
export { Netplay } from "netplay_page.slint";
export { Cheats } from "cheat_page.slint";
export { Settings } from "settings_page.slint";
export { About } from "about_page.slint";
+106
View File
@@ -0,0 +1,106 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { ComboBox, HorizontalBox, VerticalBox, CheckBox } from "std-widgets.slint";
import { Page } from "page.slint";
export global SettingsData {
in-out property <bool> integer_scaling;
in-out property <bool> fullscreen;
in-out property <bool> widescreen;
in-out property <bool> apply_crt_shader;
in-out property <bool> overclock_n64_cpu;
in-out property <bool> disable_expansion_pak;
in-out property <int> resolution;
}
export component Settings inherits Page {
title: @tr("Settings");
VerticalBox {
alignment: start;
HorizontalBox {
alignment: start;
Text {
text: @tr("Resolution:");
vertical-alignment: center;
}
ComboBox {
model: ["1x", "2x", "4x"];
current-index: SettingsData.resolution;
selected => {
SettingsData.resolution = self.current-index;
}
}
}
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Integer Scaling");
checked: SettingsData.integer_scaling;
toggled => {
SettingsData.integer_scaling = self.checked;
}
}
}
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Fullscreen (Esc closes game)");
checked: SettingsData.fullscreen;
toggled => {
SettingsData.fullscreen = self.checked;
}
}
}
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Widescreen (strech)");
checked: SettingsData.widescreen;
toggled => {
SettingsData.widescreen = self.checked;
}
}
}
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Apply CRT shader");
checked: SettingsData.apply_crt_shader;
toggled => {
SettingsData.apply_crt_shader = self.checked;
}
}
}
Rectangle { }
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Overclock N64 CPU (may cause bugs)");
checked: SettingsData.overclock_n64_cpu;
toggled => {
SettingsData.overclock_n64_cpu = self.checked;
}
}
}
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Disable Expansion Pak");
checked: SettingsData.disable_expansion_pak;
toggled => {
SettingsData.disable_expansion_pak = self.checked;
}
}
}
}
}
+145
View File
@@ -0,0 +1,145 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { HorizontalBox, VerticalBox, Palette } from "std-widgets.slint";
component SideBarItem inherits Rectangle {
in property <int> tab-index;
in property <bool> selected;
in property <bool> has-focus;
in-out property <string> text <=> label.text;
callback clicked <=> touch.clicked;
min-height: l.preferred-height;
accessible-role: tab;
accessible-label: root.text;
accessible-item-index: root.tab-index;
accessible-item-selectable: true;
accessible-item-selected: root.selected;
accessible-action-default => {
self.clicked();
}
states [
pressed when touch.pressed: {
state.opacity: 0.8;
}
hover when touch.has-hover: {
state.opacity: 0.6;
}
selected when root.selected: {
state.opacity: 1;
}
focused when root.has-focus: {
state.opacity: 0.8;
}
]
state := Rectangle {
opacity: 0;
background: Palette.background;
animate opacity { duration: 150ms; }
}
l := HorizontalBox {
y: (parent.height - self.height) / 2;
spacing: 0px;
label := Text {
vertical-alignment: center;
accessible-role: none;
}
}
touch := TouchArea {
width: 100%;
height: 100%;
}
}
export component SideBar inherits Rectangle {
in property <[string]> model: [];
in property <string> title <=> label.text;
out property <int> current-item: 0;
out property <int> current-focused: fs.has-focus ? fs.focused-tab : -1; // The currently focused tab
width: 180px;
forward-focus: fs;
Rectangle {
background: Palette.background.darker(0.2);
}
VerticalBox {
padding-left: 0px;
padding-right: 0px;
alignment: start;
label := Text {
font-size: 16px;
horizontal-alignment: center;
}
navigation := VerticalLayout {
alignment: start;
vertical-stretch: 0;
accessible-role: tab-list;
accessible-delegate-focus: root.current-focused >= 0 ? root.current-focused : root.current-item;
accessible-label: root.title;
accessible-item-count: root.model.length;
fs := FocusScope {
key-pressed(event) => {
if (event.text == "\n") {
root.current-item = root.current-focused;
return accept;
}
if (event.text == Key.UpArrow) {
self.focused-tab = Math.max(self.focused-tab - 1, 0);
return accept;
}
if (event.text == Key.DownArrow) {
self.focused-tab = Math.min(self.focused-tab + 1, root.model.length - 1);
return accept;
}
return reject;
}
key-released(event) => {
if (event.text == " ") {
root.current-item = root.current-focused;
return accept;
}
return reject;
}
property <int> focused-tab: 0;
x: 0;
width: 0; // Do not react on clicks
}
for item[index] in root.model: SideBarItem {
clicked => {
root.current-item = index;
}
tab-index: index;
has-focus: index == root.current-focused;
text: item;
selected: index == root.current-item;
}
}
VerticalLayout {
bottom := VerticalBox {
padding-top: 0px;
padding-bottom: 0px;
@children
}
}
}
}
+24
View File
@@ -0,0 +1,24 @@
import { Button, VerticalBox, ScrollView } from "std-widgets.slint";
export global VruData {
in-out property <[string]> words;
}
export component VruDialog inherits Window {
title: @tr("What Would You Like To Say?");
preferred-width: 400px;
preferred-height: 400px;
callback vru_button_clicked(string);
in-out property words <=> VruData.words;
ScrollView {
VerticalBox {
padding-bottom: 40px;
for word[i] in words: Button {
text: word;
clicked => {
vru_button_clicked(self.text);
}
}
}
}
}
+48 -19
View File
@@ -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 =
+1049
View File
File diff suppressed because it is too large Load Diff
+37 -27
View File
@@ -57,10 +57,10 @@ fn get_save_type(rom: &[u8], game_id: &str) -> Vec<SaveTypes> {
1 => return vec![SaveTypes::Eeprom4k],
2 => return vec![SaveTypes::Eeprom16k],
3 => return vec![SaveTypes::Sram],
4 => panic!("Unsupported save type: {}", save_type),
4 => panic!("Unsupported save type: {save_type}"),
5 => return vec![SaveTypes::Flash],
6 => panic!("Unsupported save type: {}", save_type),
_ => panic!("Unknown save type: {}", save_type),
6 => panic!("Unsupported save type: {save_type}"),
_ => panic!("Unknown save type: {save_type}"),
}
}
match game_id {
@@ -120,15 +120,26 @@ fn get_save_type(rom: &[u8], game_id: &str) -> Vec<SaveTypes> {
}
}
pub fn get_game_crc(rom: &[u8]) -> String {
let crc1 = format!(
"{:08X}",
u32::from_be_bytes(rom[0x10..0x14].try_into().unwrap())
);
let crc2 = format!(
"{:08X}",
u32::from_be_bytes(rom[0x14..0x18].try_into().unwrap())
);
let country_code = format!("{:02X}", &rom[0x3E]);
format!("{crc1}-{crc2}-C:{country_code}")
}
pub fn get_game_name(rom: &[u8]) -> String {
let mut game_name = "".to_owned();
let header_value = std::str::from_utf8(&rom[0x20..0x34]);
if header_value.is_ok() {
if let Ok(header_value) = header_value {
let re = regex::Regex::new(r"[^a-zA-Z0-9_ -]").unwrap();
game_name = re
.replace_all(header_value.unwrap(), "")
.trim()
.replace('\0', "");
game_name = re.replace_all(header_value, "").trim().replace('\0', "");
}
game_name
}
@@ -196,30 +207,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();
}
}
+56 -21
View File
@@ -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,10 +107,15 @@ 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])
}
}
}
pub fn check_callback(device: &mut device::Device) {
pub fn check_callback(device: &mut device::Device) -> bool {
let mut speed_limiter_toggled = false;
let callback = unsafe { rdp_check_callback() };
device.cpu.running = callback.emu_running;
if device.netplay.is_none() {
@@ -112,8 +124,18 @@ pub fn check_callback(device: &mut device::Device) {
} else if callback.load_state {
device.load_state = true;
}
device.vi.enable_speed_limiter = callback.enable_speedlimiter;
if device.vi.enable_speed_limiter != callback.enable_speedlimiter {
speed_limiter_toggled = true;
device.vi.enable_speed_limiter = callback.enable_speedlimiter;
}
}
if callback.lower_volume {
ui::audio::lower_audio_volume(&mut device.ui);
} else if callback.raise_volume {
ui::audio::raise_audio_volume(&mut device.ui);
}
speed_limiter_toggled
}
pub fn set_register(reg: u32, value: u32) {
@@ -126,12 +148,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 +165,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 +187,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
View File
@@ -1,18 +1,14 @@
use eframe::egui;
use crate::ui;
pub fn prompt_for_match(
words: &[String],
window_notifier: &tokio::sync::mpsc::Sender<Vec<String>>,
window_notifier: &tokio::sync::mpsc::Sender<Option<Vec<String>>>,
word_index_receiver: &mut tokio::sync::mpsc::Receiver<String>,
gui_ctx: &egui::Context,
) -> u16 {
let mut dedup_words = words.to_owned();
dedup_words.sort();
dedup_words.dedup();
window_notifier.try_send(dedup_words).unwrap();
gui_ctx.request_repaint(); // this is so the window pops up right away
window_notifier.try_send(Some(dedup_words)).unwrap();
let mut result = word_index_receiver.try_recv();
while result.is_err() {
result = word_index_receiver.try_recv();