Compare commits

...

133 Commits

Author SHA1 Message Date
Logan McNaughton 6c2ae4dc5a update mac signing command (#801) 2026-04-23 20:01:01 +02:00
Logan McNaughton 0a884e85df Fix macos sandboxing (#800)
* Fix macos sandboxing

* more

* more

* more

* more

* more

* more

* more
2026-04-23 19:32:53 +02:00
Logan McNaughton 47098f60dd use timer for message lengths (#799)
* use timer for message lengths

* more

* more

* more

* more

* more

* more
2026-04-23 18:00:15 +02:00
Logan McNaughton ca4a20f524 update cargo to v1.1.17 (#798) 2026-04-23 11:02:02 +02:00
Logan McNaughton 3b145909e8 show more detailed version in --version (#797) 2026-04-23 08:57:03 +02:00
Logan McNaughton 6eb3367f1b don't show hardcore switch if it is not enabled (#795) 2026-04-22 09:32:22 +02:00
Logan McNaughton 03f83fe917 fix git version with shallow clones (#794)
* fix git version with shallow clones

* more
2026-04-22 09:08:54 +02:00
Logan McNaughton c4771f91e0 print error to screen (#793)
* print error to screen

* mmore
2026-04-21 22:35:14 +02:00
Danny Kirkham bdd12352e1 Enable accessibility features (#791) 2026-04-21 18:23:10 +02:00
Logan McNaughton 224bd2df1d clean up RA variables (#789)
* clean up RA variables

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-04-21 12:57:41 +02:00
Logan McNaughton 412f928781 improve function order (#788)
* improve function order

* more
2026-04-20 22:53:42 +02:00
Logan McNaughton 779ef9fe77 update fullscreen note (#787) 2026-04-20 19:27:09 +02:00
Logan McNaughton 048d1d662f Track recent roms (#786)
* track recent roms

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-04-20 16:53:40 +02:00
Logan McNaughton cf3a27cecf Use switch instead of checkbox (#785) 2026-04-20 12:27:44 +02:00
Logan McNaughton b8c3556df6 cleanup loop (#784) 2026-04-20 10:34:27 +02:00
Logan McNaughton 9e337295ca don't launch game if already running (#783) 2026-04-20 10:13:15 +02:00
Logan McNaughton 8cb57811a6 make rfd optional (#782) 2026-04-20 09:27:29 +02:00
Logan McNaughton 82c02461f8 handle file dropping (#781)
* handle file dropping

* more
2026-04-20 09:11:09 +02:00
Logan McNaughton 26253b5ed7 just create config (#780) 2026-04-20 07:14:05 +02:00
Logan McNaughton f520aeab82 use tokio process (#779) 2026-04-20 07:00:39 +02:00
Logan McNaughton edb6960d4f Make Slint optional (#778)
* make slint optional

* more

* more
2026-04-19 23:15:56 +02:00
Logan McNaughton a488f0d91a fix some typos (#777) 2026-04-19 18:51:01 +02:00
Logan McNaughton 73d895a64a add vsync option (#776) 2026-04-19 18:25:36 +02:00
Logan McNaughton 805bcba249 run VRU dialog in main thread (#774)
* run VRU dialog in main thread

* more
2026-04-19 17:00:50 +02:00
Logan McNaughton 66abd14c3b add small note to netplay join (#773) 2026-04-19 14:09:27 +02:00
Logan McNaughton d856499f59 fix image positioning (#772)
* fix image positioning

* more
2026-04-19 07:57:57 +02:00
Logan McNaughton bc3dd4d39a Hotkey for onscreen FPS (#770)
* fps counter

* more
2026-04-18 21:55:37 +02:00
Logan McNaughton 5a91d09f9e improved transferpak setup (#769)
* improved transferpak setup

* more

* more

* more
2026-04-18 16:48:55 +02:00
Logan McNaughton b09f8bf891 update MACOSX_DEPLOYMENT_TARGET (#768) 2026-04-18 10:08:30 +02:00
Logan McNaughton 8ee10e40ad add provisionprofile (#767)
* add provisionprofile

* more
2026-04-18 08:02:30 +02:00
Logan McNaughton f8a7153fdd enable vsync if required (#765)
* enable vsync if required

* add note

* more

* more
2026-04-17 15:07:15 +02:00
Logan McNaughton a592d0eb84 remove uses of to_owned (#764)
* remove uses of to_owned

* more

* more
2026-04-17 09:59:32 +02:00
Logan McNaughton 28aa8ea280 use tokio::fs (#763) 2026-04-17 09:27:42 +02:00
Logan McNaughton cb87f79710 writeback GB ram files (#762) 2026-04-17 08:56:53 +02:00
Logan McNaughton be117214c3 Update README with notable features of Gopher64 (#761)
Added features list for Gopher64 emulator
2026-04-17 08:48:47 +02:00
Logan McNaughton bb056a77f4 bump version (#760) 2026-04-16 15:56:35 +02:00
Logan McNaughton d4a90155f9 Rust 1.95.0 (#759) 2026-04-16 15:21:01 +02:00
Logan McNaughton e24a9a8126 cleaner fix for MBC30 issue (#758)
* cleaner fix for MBC30 issue

* more
2026-04-16 13:31:17 +02:00
Logan McNaughton 83fab4c638 MBC30 support (#757) 2026-04-16 12:38:45 +02:00
Logan McNaughton 8196b52289 Fix MBC30 support (#756) 2026-04-16 11:56:57 +02:00
Logan McNaughton 8dadbbbf58 update slint to 1.16.0 (#755) 2026-04-16 11:09:09 +02:00
Logan McNaughton 093b39b17d Improve netplay RA guard (#752)
* remove double guard for netplay+RA

* more

* more
2026-04-15 19:00:39 +02:00
Logan McNaughton 7100750e2d show proper error on failed login (#751)
* show proper error on failed login

* more
2026-04-15 16:02:34 +02:00
Logan McNaughton 40e250386e add note about strange char* comparison (#750)
* add note about strange char* comparison

* more
2026-04-15 13:54:27 +02:00
Logan McNaughton d2e47509b2 remove unused param (#749) 2026-04-15 13:39:49 +02:00
Logan McNaughton 451d66beee video code cleanup (#748) 2026-04-15 13:25:04 +02:00
Logan McNaughton 20bad48894 store C strings in vars (#747) 2026-04-15 13:10:15 +02:00
Logan McNaughton b40ec6940f finish up RA support (#745)
* finish up RA support

* more

* capture some corner cases

* fix post data

* more

* more

* fix range

* more

* more

* more

* more

* show in-progress achievements

* more

* more

* more

* more
2026-04-15 12:57:52 +02:00
Logan McNaughton d12f16fe29 minor code cleanup (#746) 2026-04-15 12:36:21 +02:00
dependabot[bot] d061a856d2 Bump rand from 0.10.0 to 0.10.1 in the cargo group across 1 directory (#744)
Bumps the cargo group with 1 update in the / directory: [rand](https://github.com/rust-random/rand).


Updates `rand` from 0.10.0 to 0.10.1
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.10.0...0.10.1)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.10.1
  dependency-type: direct:production
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 07:26:44 +02:00
Logan McNaughton 7725aa2692 RA tweaks (#743)
* prevent crash

* more

* more
2026-04-13 22:38:31 +02:00
Logan McNaughton baa14de3f3 show leaderboard and challenge together (#742) 2026-04-13 21:22:36 +02:00
Logan McNaughton c14b461612 more RA (#741)
* more RA

* more

* more
2026-04-13 20:39:57 +02:00
Logan McNaughton 595089187d leaderboard tracker (#740)
* leaderboard tracker

* more

* more

* more

* more
2026-04-13 13:30:18 +02:00
Logan McNaughton c4b86eb4b1 challenge indicators (#739)
* Add Challenge Indicators

* more

* more

* use c++20

* shorten messages

* progress

* more

* more

* more

* more
2026-04-13 09:16:15 +02:00
Logan McNaughton 4240a99ad5 cache message images (#738) 2026-04-12 19:00:45 +02:00
Logan McNaughton 9d7db93c70 fix font clarity (#737) 2026-04-12 15:21:52 +02:00
Logan McNaughton 9b71d3aa0b print message for failed login (#736) 2026-04-12 12:33:11 +02:00
Logan McNaughton e8898c52fc More RA code (#733)
* implement RC_CLIENT_EVENT_SERVER_ERROR

* handle more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-04-12 12:10:15 +02:00
Logan McNaughton 4b5c8d03a9 use a const pointer (#732) 2026-04-11 16:35:04 +02:00
Logan McNaughton 2706f8f6a5 use sized savestate functions (#730)
* use sized savestate functions

* fix lint
2026-04-11 08:44:22 +02:00
Logan McNaughton e4391b3089 add some warning messages (#729) 2026-04-10 21:51:06 +02:00
Logan McNaughton 4b54ea279d Build on MacOS 15 (#728) 2026-04-10 20:21:12 +02:00
Logan McNaughton 9f6194d64f better startup message (#727)
* better startup message

* more
2026-04-10 07:56:18 +02:00
Logan McNaughton 8bb3179534 Improve startup messages (#726)
* improve startup messages

* more
2026-04-09 20:05:54 +02:00
Logan McNaughton 35dbd957cc reduce font size a bit (#725)
* reduce font size a bit

* remove duplicate messages
2026-04-09 17:22:26 +02:00
Logan McNaughton 125f79a0e6 show message for RA progress (#724) 2026-04-09 15:15:29 +02:00
Logan McNaughton f865ccfacd pull tags in github action (#723)
* pull tags in github action

* more detailed version

* more
2026-04-09 13:50:48 +02:00
Logan McNaughton 914798e717 more descriptive git naming (#722) 2026-04-09 13:13:56 +02:00
Logan McNaughton 406ebba93d run ra do_idle for VRU window (#721) 2026-04-09 11:31:00 +02:00
Logan McNaughton d8ec09d475 centralize hardcore check (#720) 2026-04-09 11:17:57 +02:00
Logan McNaughton a1da699580 add RA show profile button (#719)
* add RA show profile button

* lint
2026-04-09 09:37:57 +02:00
Logan McNaughton 1226052035 cleanup some cli options (#718) 2026-04-09 09:20:23 +02:00
Logan McNaughton 2917e662df better defaults for window size (#717) 2026-04-08 22:31:28 +02:00
Logan McNaughton 601713e102 RetroAchievements hardcore support (#716)
* enable hardcore mode

* more
2026-04-08 21:57:18 +02:00
Logan McNaughton 6ed53e4e9d Upload signed windows builds (#715)
* upload windows signed artifact

* more

* more

* more

* more
2026-04-08 13:09:32 +02:00
Logan McNaughton 650fd270e1 add messages for RA leaderboards (#714) 2026-04-08 09:54:08 +02:00
Logan McNaughton 54d82713ac dont check for update with flatpak (#713) 2026-04-07 19:42:07 +02:00
Logan McNaughton 11cb242ed2 sign mac bundle (#712)
* sign mac bundle

* xcrun

* use zip

* more
2026-04-07 19:31:04 +02:00
Logan McNaughton e932239a63 RetroAchievements (#710)
* retroachievements

* add rc_api_info

* disable hardcore

* clear password after login attempt

* proper check for hardcore

* more set_hardcore

* change checkbox

* add more stubs

* add more UI

* add button for RA website

* cleanup http callback
2026-04-07 08:59:03 +02:00
Logan McNaughton 5207bd4b27 remove governor crate (#709)
* remove governor crate

* more
2026-04-04 09:27:45 +02:00
Logan McNaughton d378ae187a write savestate in thread (#708) 2026-04-03 16:32:40 +02:00
Logan McNaughton e72ddf73f0 use 4 worker threads (#707) 2026-04-03 16:00:00 +02:00
Logan McNaughton 4bba36f0c3 add libpng to macos build (#706)
* add libpng to macos build

* more

* signing

* more

* more

* more
2026-04-02 14:52:16 +02:00
Logan McNaughton 20b5cf3e4d Improve MacOS opt flags (#705) 2026-04-02 08:19:32 +02:00
Logan McNaughton 973f4bb28d Create MacOS bundle (#701)
* Create MacOS bundle

* more

* more

* more

* Update framework link search path in build.rs

* Update framework link search path in build.rs

* Remove MoltenVK linking from build.rs

Removed MoltenVK framework linking from build script.

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-04-01 20:35:46 +02:00
Logan McNaughton de0db4b46d static link moltenvk (#700)
* static link moltenvk

* more

* more

* static freetype
2026-03-31 20:09:06 +02:00
Logan McNaughton 11d3e52779 use status vs spawn/wait (#699) 2026-03-31 19:12:06 +02:00
Logan McNaughton cef70faf1e move MACOSX_DEPLOYMENT_TARGET 2026-03-31 15:02:53 +02:00
Logan McNaughton 9b0b3e67a4 change dinput arg type 2026-03-31 13:58:56 +02:00
Logan McNaughton ebd22415cd honor setting 2026-03-31 11:40:50 +02:00
Logan McNaughton 5177aaab53 dont pass fullscreen 2026-03-31 11:38:36 +02:00
Logan McNaughton ad5dc8311a remove custom stack size (#698)
* remove custom stack size

* new command

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* config

* more

* more
2026-03-31 10:54:14 +02:00
Logan McNaughton cc538f942f use limiter clock 2026-03-30 09:20:44 +02:00
Logan McNaughton 9528473f11 use Duration for comparing time (#696) 2026-03-29 18:54:35 +02:00
Logan McNaughton 8b770cfd18 Update linux deps based on SDL wiki (#695) 2026-03-28 20:00:44 +01:00
Logan McNaughton 46cb6738a2 Use upstream sse2neon (#694)
* Use upstream sse2neon

* update commit
2026-03-28 19:24:53 +01:00
Logan McNaughton 9b8040873d Update parallel-rdp (#693)
* Update parallel-rdp

* more

* more
2026-03-26 17:02:52 +01:00
Logan McNaughton bff39ab04d Rust 1.94.1 (#692)
* Rust 1.94.1

* more

* more
2026-03-26 16:28:29 +01:00
Logan McNaughton 86ce5795fa fix netplay start if player 1 leaves (#691)
* fix netplay start if player 1 leaves

* more

* more
2026-03-25 10:56:17 +01:00
Logan McNaughton a834a4f9a4 Eliminate macOS Vulkan hint in SDL initialization (#690)
Removed macOS-specific Vulkan library hint from SDL initialization.
2026-03-24 18:06:49 +01:00
Logan McNaughton 1a4de8e911 Don't use caching for releases 2026-03-24 14:43:14 +01:00
Logan McNaughton 0f607e61aa Use MacOS 26 (#689) 2026-03-24 13:38:23 +01:00
Logan McNaughton 6220a566eb Enable Win ARM build (#686)
* Enable Win ARM build

* change ss2neon source

* update sse2neon
2026-03-24 13:29:02 +01:00
Logan McNaughton 5da6a6c3ee clippy fixes (#688)
* clippy fixes

* fix fmt
2026-03-24 11:09:16 +01:00
Logan McNaughton 567dd3a79d Reset hotkey (#682)
* Start soft reset work

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-03-21 19:57:27 +01:00
dependabot[bot] 4a0a80b045 Bump rustls-webpki in the cargo group across 1 directory (#685)
Bumps the cargo group with 1 update in the / directory: [rustls-webpki](https://github.com/rustls/webpki).


Updates `rustls-webpki` from 0.103.9 to 0.103.10
- [Release notes](https://github.com/rustls/webpki/releases)
- [Commits](https://github.com/rustls/webpki/compare/v/0.103.9...v/0.103.10)

---
updated-dependencies:
- dependency-name: rustls-webpki
  dependency-version: 0.103.10
  dependency-type: indirect
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-21 10:46:18 +01:00
Logan McNaughton 74b087e77e cargo update 2026-03-20 20:39:02 +01:00
Logan McNaughton faadefe96e format cpp code 2026-03-19 19:17:23 +01:00
Logan McNaughton 72df783d8c Add CLI option to load a save state (#681)
* Add CLI option to load a save state

* more

* more

* more
2026-03-19 14:06:53 +01:00
Logan McNaughton e7dfd02e17 cargo update 2026-03-17 19:38:00 +01:00
Logan McNaughton c6703244b3 better window cleanup 2026-03-17 19:15:19 +01:00
Logan McNaughton 8291a93337 draw every 100ms in input profile creation 2026-03-17 19:09:10 +01:00
Logan McNaughton 3e177ea4be move SDL_HINT_VULKAN_LIBRARY 2026-03-16 14:58:02 +01:00
Logan McNaughton 6de1ef3548 change message in input profile 2026-03-16 14:55:27 +01:00
Logan McNaughton 73a3e6a22a Add hint for MoltenVK (#678) 2026-03-15 10:49:24 +01:00
Logan McNaughton fcceca5976 fix mac build (#676)
* fix mac build

* add freetype

* set deployment target

* test

* test

* more

* test

* more

* more

* more

* more

* test

* put back

* link CoreFoundation

* more

* more

* more
2026-03-12 12:49:59 +01:00
Logan McNaughton acf5e08b97 cargo update 2026-03-10 09:29:49 +01:00
Logan McNaughton 768fcdaf7f bump to v1.1.16 2026-03-05 21:21:47 +01:00
Logan McNaughton 1c6e23b7f2 Update parallel-rdp 2026-03-05 20:42:24 +01:00
Logan McNaughton b4ae7db3dd Update to Rust 1.94.0 (#672) 2026-03-05 20:37:50 +01:00
Logan McNaughton e750b6eed1 upload unzipped artifact (#670) 2026-02-27 19:11:00 +01:00
Logan McNaughton f8b777248a update sdl 2026-02-22 08:12:10 +01:00
Logan McNaughton 51e9b3aa55 cargo update 2026-02-15 21:41:10 +01:00
Logan McNaughton efbeaeab88 rust 1.93.1 2026-02-12 22:49:42 +01:00
Logan McNaughton f24a5e7f05 reapply cheats when loading save state 2026-02-11 16:13:36 +01:00
Logan McNaughton 55170fcca3 use Xoshiro256PlusPlus (#668) 2026-02-09 12:16:10 +01:00
Logan McNaughton b952c45e0b cargo update 2026-02-09 10:44:19 +01:00
Logan McNaughton b13f37baf8 bump to 1.1.15 2026-02-06 09:53:09 +01:00
Logan McNaughton db40ec5034 cargo update (rand) 2026-02-06 09:51:12 +01:00
Logan McNaughton cedb2d5c22 update slint 2026-02-04 18:05:03 +01:00
Logan McNaughton 22d57677cb cargo update 2026-02-03 20:49:46 +01:00
Logan McNaughton f29f497405 bump to 1.1.14 2026-01-29 21:58:40 +01:00
68 changed files with 5114 additions and 2665 deletions
+9 -2
View File
@@ -1,16 +1,23 @@
[target.'cfg(all(target_arch = "aarch64", target_os = "linux"))']
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
[target.'cfg(target_os = "windows")']
[target.'cfg(any(target_os = "windows", target_os = "macos"))']
linker = "rust-lld"
[target.'cfg(target_os = "windows")']
rustflags = ["-C", "link-arg=/STACK:8388608"]
[target.'cfg(target_arch = "x86_64")']
rustflags = ["-C", "target-cpu=x86-64-v3"]
[target.'cfg(target_arch = "aarch64")']
[target.'cfg(all(target_arch = "aarch64", target_os = "macos"))']
rustflags = ["-C", "target-cpu=apple-m1"]
[target.'cfg(all(target_arch = "aarch64", not(target_os = "macos")))']
rustflags = ["-C", "target-cpu=cortex-a76"]
[env]
CC = "clang"
CXX = "clang++"
AR = "llvm-ar"
SDKROOT = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"
+1
View File
@@ -8,6 +8,7 @@ updates:
directory: "/"
schedule:
interval: "weekly"
# - package-ecosystem: "cargo"
# directory: "/"
# schedule:
+93 -30
View File
@@ -4,14 +4,15 @@ permissions:
on:
push:
branches: ["main"]
branches: [ "main" ]
tags:
- "v*"
pull_request:
branches: ["main"]
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
RA_HARDCORE: ${{ github.ref_type == 'tag' && 'true' || 'false' }}
jobs:
build-linux:
@@ -32,29 +33,35 @@ jobs:
- uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
rustflags: ""
cache: ${{ github.ref_type != 'tag' }}
- name: Set LLVM_VERSION
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut -d':' -f2 | xargs)" >> $GITHUB_ENV
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
-d':' -f2 | xargs)" >> $GITHUB_ENV
- name: Install SDL 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 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 libfreetype-dev libfontconfig-dev
packages: build-essential git make pkg-config cmake ninja-build
gnome-desktop-testing libasound2-dev libpulse-dev libaudio-dev
libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-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 libthai-dev libpipewire-0.3-dev libwayland-dev
libdecor-0-dev liburing-dev libfreetype-dev libfontconfig-dev
version: ${{ matrix.target }}-1.0
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v2
with:
version: ${{ env.LLVM_VERSION }}
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{ env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-Linux-${{ matrix.llvm }}.tar.xz
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-Linux-${{
matrix.llvm }}.tar.xz
- name: Build
run: |
cargo build --release -v --target=${{ matrix.target }}
@@ -63,10 +70,11 @@ jobs:
env:
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
- name: Upload file
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: gopher64-${{ matrix.arch }}
path: output/gopher64-${{ matrix.arch }}
archive: false
build-windows:
runs-on: ${{ matrix.os }}
@@ -78,18 +86,20 @@ jobs:
target: x86_64-pc-windows-msvc
arch: windows-x86_64
llvm: win64
#- os: windows-11-arm
# target: aarch64-pc-windows-msvc
# arch: windows-aarch64
# llvm: woa64
- os: windows-11-arm
target: aarch64-pc-windows-msvc
arch: windows-aarch64
llvm: woa64
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
rustflags: ""
cache: ${{ github.ref_type != 'tag' }}
- name: Set LLVM_VERSION
run: |
$llvm_version = rustc --version --verbose | Select-String "LLVM version" | ForEach-Object { $_.Line.Split(":")[1].Trim() }
@@ -98,7 +108,9 @@ jobs:
uses: KyleMayes/install-llvm-action@v2
with:
version: ${{ env.LLVM_VERSION }}
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{ env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-${{ matrix.llvm }}.exe
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-${{ matrix.llvm
}}.exe
- name: Build
run: |
cargo build --release -v --target=${{ matrix.target }}
@@ -110,13 +122,14 @@ jobs:
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
- name: Upload unsigned artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: gopher64-${{ matrix.arch }}
path: output/gopher64-${{ matrix.arch }}.exe
archive: ${{ github.ref_type == 'tag' && true || false }}
- name: SignPath
if: ${{ github.ref_type == 'tag' }}
uses: signpath/github-action-submit-signing-request@v2.0
uses: signpath/github-action-submit-signing-request@v2
with:
api-token: "${{ secrets.SIGNPATH_API_TOKEN }}"
organization-id: "${{ secrets.SIGNPATH_ORG_ID }}"
@@ -124,37 +137,87 @@ jobs:
artifact-configuration-slug: "gopher64-${{ matrix.arch }}"
signing-policy-slug: "release-signing"
github-artifact-id: "${{ steps.upload-unsigned-artifact.outputs.artifact-id }}"
wait-for-completion: false
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 3600
output-artifact-directory: output-signed
- name: Upload signed artifact
if: ${{ github.ref_type == 'tag' }}
uses: actions/upload-artifact@v7
with:
name: gopher64-${{ matrix.arch }}
path: output-signed/gopher64-${{ matrix.arch }}.exe
archive: false
overwrite: true
build-macos:
if: false
runs-on: macos-15
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-apple-darwin
arch: mac-x86_64
- target: aarch64-apple-darwin
arch: mac-aarch64
arch: macos-aarch64
llvm: ARM64
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
rustflags: ""
cache: ${{ github.ref_type != 'tag' }}
- name: Set LLVM_VERSION
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
-d':' -f2 | xargs)" >> $GITHUB_ENV
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v2
with:
version: ${{ env.LLVM_VERSION }}
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-macOS-${{
matrix.llvm }}.tar.xz
- name: Build
run: |
cargo build --release -v --target=${{ matrix.target }}
mkdir output
cp target/${{ matrix.target }}/release/gopher64 output/gopher64-${{ matrix.arch }}
cargo install cargo-bundle
brew install molten-vk
cargo build --release --no-default-features -v --target=${{ matrix.target }}
mv target/${{ matrix.target }}/release/gopher64 target/${{ matrix.target }}/release/gopher64-cli
cargo bundle --release --target=${{ matrix.target }} --format osx
cp target/${{ matrix.target }}/release/gopher64-cli target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/MacOS/gopher64-cli
install_name_tool -change /opt/homebrew/opt/freetype/lib/libfreetype.6.dylib \
@executable_path/../Frameworks/libfreetype.6.dylib target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/MacOS/gopher64
install_name_tool -change /opt/homebrew/opt/freetype/lib/libfreetype.6.dylib \
@executable_path/../Frameworks/libfreetype.6.dylib target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/MacOS/gopher64-cli
install_name_tool -change /opt/homebrew/opt/libpng/lib/libpng16.16.dylib \
@executable_path/../Frameworks/libpng16.16.dylib target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/Frameworks/libfreetype.6.dylib
env:
AR: ar
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
MACOSX_DEPLOYMENT_TARGET: "15.7"
- name: Sign Bundle Dev
if: ${{ github.ref_type != 'tag' }}
run: |
codesign -f --entitlements data/macos/entitlements_dev.plist --deep --timestamp --options runtime -s - target/${{ matrix.target }}/release/bundle/osx/Gopher64.app
- name: Import Codesign Certs
if: ${{ github.ref_type == 'tag' }}
uses: apple-actions/import-codesign-certs@v6
with:
p12-file-base64: ${{ secrets.APPLE_CERTIFICATES_FILE_BASE64 }}
p12-password: ${{ secrets.APPLE_CERTIFICATES_PASSWORD }}
- name: Sign Bundle Release
if: ${{ github.ref_type == 'tag' }}
run: |
codesign -f --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/Frameworks/libpng16.16.dylib
codesign -f --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/Frameworks/libfreetype.6.dylib
codesign -f --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/Frameworks/libMoltenVK.dylib
codesign -f --entitlements data/macos/entitlements_child.plist --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/MacOS/gopher64-cli
codesign -f --entitlements data/macos/entitlements_prod.plist --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/release/bundle/osx/Gopher64.app
zip -r gopher64-${{ matrix.arch }}.zip target/${{ matrix.target }}/release/bundle/osx/Gopher64.app
xcrun notarytool submit --wait --apple-id "${{ secrets.APPLE_ID }}" --team-id "${{ secrets.APPLE_DEVELOPER_ID }}" --password "${{ secrets.APPLE_APP_PASSWORD }}" gopher64-${{ matrix.arch }}.zip
xcrun stapler staple target/${{ matrix.target }}/release/bundle/osx/Gopher64.app
- name: Upload file
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: gopher64-${{ matrix.arch }}
path: output/gopher64-${{ matrix.arch }}
path: target/${{ matrix.target }}/release/bundle/osx
+15 -11
View File
@@ -2,9 +2,9 @@ name: Rust-lint
on:
push:
branches: ["main"]
branches: [ "main" ]
pull_request:
branches: ["main"]
branches: [ "main" ]
permissions:
contents: read
@@ -23,26 +23,30 @@ jobs:
with:
components: clippy, rustfmt
- name: Set LLVM_VERSION
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut -d':' -f2 | xargs)" >> $GITHUB_ENV
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
-d':' -f2 | xargs)" >> $GITHUB_ENV
- name: Install SDL 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 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 libfreetype-dev libfontconfig-dev
packages: build-essential git make pkg-config cmake ninja-build
gnome-desktop-testing libasound2-dev libpulse-dev libaudio-dev
libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-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 libthai-dev libpipewire-0.3-dev libwayland-dev
libdecor-0-dev liburing-dev libfreetype-dev libfontconfig-dev
version: lint-1.0
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v2
with:
version: ${{ env.LLVM_VERSION }}
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{ env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-Linux-X64.tar.xz
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-Linux-X64.tar.xz
- name: Rustfmt Check
uses: actions-rust-lang/rustfmt@v1
- name: Lint
run: |
cargo clippy -- -Dwarnings
cargo clippy --no-default-features -- -Dwarnings
+6 -2
View File
@@ -1,7 +1,11 @@
[submodule "parallel-rdp/parallel-rdp-standalone"]
path = parallel-rdp/parallel-rdp-standalone
url = https://github.com/gopher64/parallel-rdp-standalone.git
branch = gopher64
url = https://github.com/Themaister/parallel-rdp-standalone.git
branch = master
[submodule "src/compat/sse2neon"]
path = src/compat/sse2neon
url = https://github.com/DLTcollab/sse2neon.git
branch = master
[submodule "retroachievements/rcheevos"]
path = retroachievements/rcheevos
url = https://github.com/RetroAchievements/rcheevos.git
Generated
+1669 -1061
View File
File diff suppressed because it is too large Load Diff
+23 -14
View File
@@ -1,45 +1,46 @@
[package]
name = "gopher64"
version = "1.1.13"
version = "1.1.18"
edition = "2024"
rust-version = "1.93.0"
rust-version = "1.95.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["gui"]
gui = ["dep:rfd", "dep:slint", "dep:slint-build"]
[dependencies]
dirs = "6.0"
zip = "7.0"
governor = "0.10"
sevenz-rust2 = { version = "0.20", default-features = false }
zip = "8.0"
sevenz-rust2 = "0.20"
chrono = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-big-array = "0.5"
slint = { version = "1.12", default-features = false, features = ["compat-1-2", "std", "backend-winit", "renderer-skia-vulkan"] }
slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "unstable-winit-030", "backend-winit", "renderer-skia", "accessibility"], optional = true }
open = "5.3"
sha2 = "0.10"
sha2 = "0.11"
sdl3-sys = { version = "0.6", features = ["build-from-source-static"] }
sdl3-ttf-sys = { version = "0.6", features = ["build-from-source-static", "no-sdlttf-harfbuzz", "no-sdlttf-plutosvg"] }
rfd = "0.17"
tokio = {version = "1.46", features = ["rt-multi-thread", "macros", "fs"] }
rfd = {version = "0.17", optional = true }
tokio = {version = "1.46", features = ["rt-multi-thread", "macros", "fs", "process"] }
spin_sleep = "1.3"
encoding_rs = "0.8"
mimalloc = "0.1"
regex = "1.11"
tokio-tungstenite = "0.28"
tokio-tungstenite = "0.29"
futures = "0.3"
socket2 = { version = "0.6", features = ["all"] }
rand_chacha = "0.9"
rand = "0.10"
fatfs = "0.3"
semver = "1.0"
postcard = { version = "1.1", features = ["use-std"] }
clap = { version = "4.5", features = ["derive"] }
reqwest = { version = "0.13", default-features = false, features = ["http2","rustls","charset","json","query"] }
reqwest = { version = "0.13", features = ["json", "query"] }
[build-dependencies]
winresource = "0.1"
bindgen = "0.72"
slint-build = "1.12"
slint-build = { version = "1.12", optional = true }
cc = { version = "1.2", features = ["parallel"] }
[profile.release]
@@ -52,3 +53,11 @@ inherits = "release"
debug = true
strip = false
panic = "unwind"
[package.metadata.bundle]
name = "Gopher64"
identifier = "io.github.gopher64.gopher64"
category = "public.app-category.games"
icon = ["data/icon/gopher64_128x128.png","data/icon/gopher64_256x256.png","data/icon/gopher64_512x512.png"]
osx_frameworks = ["/opt/homebrew/opt/molten-vk/lib/libMoltenVK.dylib","/opt/homebrew/opt/freetype/lib/libfreetype.6.dylib","/opt/homebrew/opt/libpng/lib/libpng16.16.dylib"]
osx_minimum_system_version = "15.7"
+19 -5
View File
@@ -1,8 +1,20 @@
# gopher64
Gopher64 is a cross-platform N64 emulator. Some notable features:
* Netplay
* Homebrew support
* Upscaling
* CRT shader
* Emulate CPU overclocking
* Cheats
* Savestates
* RetroAchievements
## download
Windows: https://github.com/gopher64/gopher64/releases/latest/download/gopher64-windows-x86_64.exe
MacOS: https://github.com/gopher64/gopher64/releases/latest/download/gopher64-macos-aarch64.zip
Linux: https://flathub.org/apps/io.github.gopher64.gopher64
## wiki
@@ -11,7 +23,7 @@ https://github.com/gopher64/gopher64/wiki
## discord
https://discord.gg/9RGXq8W8JQ
[Discord invite link](https://discord.gg/9RGXq8W8JQ)
## controls
@@ -19,7 +31,7 @@ Keys are mapped according to [these defaults](https://github.com/gopher64/gopher
## netplay
Gopher64 supports netplay (online play with others) via cloud hosted servers. You can also run the server (https://github.com/gopher64/gopher64-netplay-server) yourself on a LAN.
Gopher64 supports netplay (online play with others) via cloud hosted servers. You can also run the [server](https://github.com/gopher64/gopher64-netplay-server) yourself on a LAN.
## portable mode
@@ -35,8 +47,8 @@ flatpak run --filesystem=host:ro io.github.gopher64.gopher64 /path/to/rom.z64
## building and usage
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
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`
5. `cargo build --release`
@@ -48,10 +60,12 @@ I am very open to contributions! Please contact me via a GitHub issue or Discord
## license
Gopher64 is licensed under the GPLv3 license. Many portions of gopher64 have been adapted from mupen64plus and/or ares. The license for mupen64plus can be found here: https://github.com/mupen64plus/mupen64plus-core/blob/master/LICENSES. The license for ares can be found here: https://github.com/ares-emulator/ares/blob/master/LICENSE.
Gopher64 is licensed under the GPLv3 license. Many portions of gopher64 have been adapted from mupen64plus and/or ares. The license for mupen64plus can be found [here](https://github.com/mupen64plus/mupen64plus-core/blob/master/LICENSES). The license for ares can be found [here](https://github.com/ares-emulator/ares/blob/master/LICENSE).
## privacy and code signing policy
Free code signing for the Windows release is provided by [SignPath.io](https://about.signpath.io), certificate by [SignPath Foundation](https://signpath.org).
During online netplay sessions, the server logs your IP address and basic session information (game title and session name) for operational purposes. No additional personal data is collected or stored.
If you enable the RetroAchievements feature, some data is sent to their systems. Please see their terms [here](https://retroachievements.org/terms).
+127 -17
View File
@@ -1,9 +1,13 @@
fn main() {
println!("cargo::rerun-if-changed=parallel-rdp");
println!("cargo::rerun-if-changed=retroachievements");
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();
#[cfg(feature = "gui")]
{
let slint_config = slint_build::CompilerConfiguration::new();
slint_build::compile_with_config("src/ui/gui/appwindow.slint", slint_config).unwrap();
}
let mut simd_build = cc::Build::new();
let mut volk_build = cc::Build::new();
@@ -14,7 +18,7 @@ fn main() {
let mut rdp_build = cc::Build::new();
rdp_build
.cpp(true)
.std("c++23")
.std("c++20")
.flag("-Wno-unused-parameter")
.flag("-Wno-missing-field-initializers")
.file("parallel-rdp/parallel-rdp-standalone/parallel-rdp/command_ring.cpp")
@@ -36,11 +40,9 @@ fn main() {
.file("parallel-rdp/parallel-rdp-standalone/vulkan/image.cpp")
.file("parallel-rdp/parallel-rdp-standalone/vulkan/indirect_layout.cpp")
.file("parallel-rdp/parallel-rdp-standalone/vulkan/memory_allocator.cpp")
.file("parallel-rdp/parallel-rdp-standalone/vulkan/pipeline_cache.cpp")
.file("parallel-rdp/parallel-rdp-standalone/vulkan/pipeline_event.cpp")
.file("parallel-rdp/parallel-rdp-standalone/vulkan/query_pool.cpp")
.file("parallel-rdp/parallel-rdp-standalone/vulkan/render_pass.cpp")
.file("parallel-rdp/parallel-rdp-standalone/vulkan/rtas.cpp")
.file("parallel-rdp/parallel-rdp-standalone/vulkan/sampler.cpp")
.file("parallel-rdp/parallel-rdp-standalone/vulkan/semaphore.cpp")
.file("parallel-rdp/parallel-rdp-standalone/vulkan/semaphore_manager.cpp")
@@ -55,7 +57,6 @@ fn main() {
.file("parallel-rdp/parallel-rdp-standalone/util/timeline_trace_file.cpp")
.file("parallel-rdp/parallel-rdp-standalone/util/environment.cpp")
.file("parallel-rdp/parallel-rdp-standalone/util/thread_name.cpp")
.file("parallel-rdp/parallel-rdp-standalone/util/slab_allocator.cpp")
.file("parallel-rdp/interface.cpp")
.file("parallel-rdp/wsi_platform.cpp")
.include("parallel-rdp/parallel-rdp-standalone/parallel-rdp")
@@ -64,19 +65,55 @@ fn main() {
.include("parallel-rdp/parallel-rdp-standalone/vulkan-headers/include")
.include("parallel-rdp/parallel-rdp-standalone/util")
.include(
std::path::PathBuf::from(std::env::var("DEP_SDL3_OUT_DIR").to_owned().unwrap())
.join("include"),
std::path::PathBuf::from(std::env::var("DEP_SDL3_OUT_DIR").unwrap()).join("include"),
)
.include(
std::path::PathBuf::from(std::env::var("DEP_SDL3_TTF_OUT_DIR").to_owned().unwrap())
std::path::PathBuf::from(std::env::var("DEP_SDL3_TTF_OUT_DIR").unwrap())
.join("include"),
);
let mut retroachievements_build = cc::Build::new();
retroachievements_build
.flag("-Wno-unused-parameter")
.include("retroachievements/rcheevos/include")
.flag("-DRC_CLIENT_SUPPORTS_HASH")
.file("retroachievements/rcheevos/src/rc_client.c")
.file("retroachievements/rcheevos/src/rc_compat.c")
.file("retroachievements/rcheevos/src/rc_util.c")
.file("retroachievements/rcheevos/src/rcheevos/alloc.c")
.file("retroachievements/rcheevos/src/rcheevos/condition.c")
.file("retroachievements/rcheevos/src/rcheevos/condset.c")
.file("retroachievements/rcheevos/src/rcheevos/consoleinfo.c")
.file("retroachievements/rcheevos/src/rcheevos/format.c")
.file("retroachievements/rcheevos/src/rcheevos/lboard.c")
.file("retroachievements/rcheevos/src/rcheevos/memref.c")
.file("retroachievements/rcheevos/src/rcheevos/operand.c")
.file("retroachievements/rcheevos/src/rcheevos/richpresence.c")
.file("retroachievements/rcheevos/src/rcheevos/runtime.c")
.file("retroachievements/rcheevos/src/rcheevos/runtime_progress.c")
.file("retroachievements/rcheevos/src/rcheevos/trigger.c")
.file("retroachievements/rcheevos/src/rcheevos/value.c")
.file("retroachievements/rcheevos/src/rapi/rc_api_common.c")
.file("retroachievements/rcheevos/src/rapi/rc_api_info.c")
.file("retroachievements/rcheevos/src/rapi/rc_api_runtime.c")
.file("retroachievements/rcheevos/src/rapi/rc_api_user.c")
.file("retroachievements/rcheevos/src/rhash/aes.c")
.file("retroachievements/rcheevos/src/rhash/cdreader.c")
.file("retroachievements/rcheevos/src/rhash/md5.c")
.file("retroachievements/rcheevos/src/rhash/hash.c")
.file("retroachievements/rcheevos/src/rhash/hash_disc.c")
.file("retroachievements/rcheevos/src/rhash/hash_encrypted.c")
.file("retroachievements/rcheevos/src/rhash/hash_rom.c")
.file("retroachievements/rcheevos/src/rhash/hash_zip.c")
.file("retroachievements/retroachievements.c");
let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let opt_flag = if arch == "x86_64" {
"-march=x86-64-v3"
} else if arch == "aarch64" {
} else if arch == "aarch64" && os == "macos" {
"-march=armv8.4-a"
} else if arch == "aarch64" && os != "macos" {
"-march=armv8.2-a"
} else {
panic!("unknown arch")
@@ -85,6 +122,7 @@ fn main() {
volk_build.flag(opt_flag);
rdp_build.flag(opt_flag);
simd_build.flag(opt_flag);
retroachievements_build.flag(opt_flag);
if os == "windows" {
volk_build.flag("-DVK_USE_PLATFORM_WIN32_KHR");
@@ -94,17 +132,69 @@ fn main() {
.set_icon("data/icon/icon.ico")
.compile()
.unwrap();
} else if os == "macos" {
println!("cargo:rustc-link-search=native=/opt/homebrew/opt/freetype/lib");
println!("cargo:rustc-link-lib=dylib=freetype");
let output = std::process::Command::new("clang")
.args(["--print-runtime-dir"])
.output()
.unwrap();
let runtime_dir = String::from_utf8_lossy(&output.stdout).trim().to_string();
println!("cargo:rustc-link-search=native={}", runtime_dir);
println!("cargo:rustc-link-lib=static=clang_rt.osx");
}
if cfg!(not(feature = "gui")) && os == "linux" {
println!("cargo:rustc-link-lib=dylib=freetype");
}
volk_build.flag("-flto=thin");
rdp_build.flag("-flto=thin");
simd_build.flag("-flto=thin");
retroachievements_build.flag("-flto=thin");
volk_build.compile("volk");
rdp_build.compile("parallel-rdp");
retroachievements_build.compile("retroachievements");
let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
let mut retroachievements_builder = bindgen::Builder::default()
.header("retroachievements/retroachievements.h")
.allowlist_function("ra_init_client")
.allowlist_function("ra_welcome")
.allowlist_function("ra_shutdown_client")
.allowlist_function("ra_get_hardcore")
.allowlist_function("ra_load_game")
.allowlist_function("ra_unload_game")
.allowlist_function("ra_set_dmem")
.allowlist_function("ra_do_frame")
.allowlist_function("ra_do_idle")
.allowlist_function("ra_http_callback")
.allowlist_function("ra_login_user")
.allowlist_function("ra_login_token_user")
.allowlist_function("ra_state_size")
.allowlist_function("ra_save_state")
.allowlist_function("ra_load_state");
if cfg!(feature = "gui") {
retroachievements_builder = retroachievements_builder
.allowlist_function("ra_logout_user")
.allowlist_function("ra_get_username")
.allowlist_function("ra_get_token");
}
let retroachievements_bindings = retroachievements_builder
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
retroachievements_bindings
.write_to_file(out_path.join("retroachievements_bindings.rs"))
.expect("Couldn't write bindings!");
let parallel_bindings = bindgen::Builder::default()
.header("parallel-rdp/interface.hpp")
.allowlist_function("rdp_init")
@@ -120,6 +210,7 @@ fn main() {
.allowlist_function("rdp_state_size")
.allowlist_function("rdp_save_state")
.allowlist_function("rdp_load_state")
.allowlist_function("rdp_set_fps")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
@@ -186,16 +277,35 @@ fn main() {
.compile("simd");
}
let git_output = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.unwrap();
let mut git_output = std::process::Command::new("git");
git_output.args(["describe", "--dirty"]);
let git_hash = String::from_utf8(git_output.stdout).unwrap();
println!("cargo:rustc-env=GIT_HASH={git_hash}");
let git_describe = if let Ok(git_output) = git_output.output()
&& git_output.status.success()
{
String::from_utf8(git_output.stdout).unwrap()
} else if let Ok(git_output) = git_output.args(["--always"]).output()
&& git_output.status.success()
{
format!(
"v{}-{}",
env!("CARGO_PKG_VERSION"),
String::from_utf8(git_output.stdout).unwrap()
)
} else {
panic!("Failed to get git describe");
};
println!("cargo:rustc-env=GIT_DESCRIBE={git_describe}");
println!("cargo:rerun-if-env-changed=NETPLAY_ID");
let netplay_id = std::env::var("NETPLAY_ID").unwrap_or("gopher64".to_string());
println!("cargo:rustc-env=NETPLAY_ID={netplay_id}");
println!("cargo:rustc-env=N64_STACK_SIZE={}", 8 * 1024 * 1024);
println!("cargo:rerun-if-env-changed=RA_HARDCORE");
println!("cargo:rustc-check-cfg=cfg(ra_hardcore_enabled)");
if let Ok(ra_hardcore) = std::env::var("RA_HARDCORE")
&& ra_hardcore.to_lowercase() == "true"
{
println!("cargo:rustc-cfg=ra_hardcore_enabled");
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.inherit</key>
<true/>
</dict>
</plist>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.device.usb</key>
<true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>
+818 -701
View File
File diff suppressed because it is too large Load Diff
+59 -43
View File
@@ -4,53 +4,69 @@
#include <cstdint>
#include <stddef.h>
extern "C"
{
extern "C" {
#endif
typedef struct
{
uint8_t *RDRAM;
uint8_t *DMEM;
uint32_t RDRAM_SIZE;
uint32_t *DPC_CURRENT_REG;
uint32_t *DPC_START_REG;
uint32_t *DPC_END_REG;
uint32_t *DPC_STATUS_REG;
bool PAL;
bool widescreen;
bool fullscreen;
bool integer_scaling;
uint32_t upscale;
bool crt;
} GFX_INFO;
typedef struct {
uint8_t *RDRAM;
uint8_t *DMEM;
uint32_t RDRAM_SIZE;
uint32_t *DPC_CURRENT_REG;
uint32_t *DPC_START_REG;
uint32_t *DPC_END_REG;
uint32_t *DPC_STATUS_REG;
bool PAL;
bool widescreen;
bool fullscreen;
bool vsync;
bool integer_scaling;
uint32_t upscale;
bool crt;
} GFX_INFO;
typedef struct
{
bool emu_running;
bool save_state;
bool load_state;
bool enable_speedlimiter;
bool lower_volume;
bool raise_volume;
bool paused;
bool frame_advance;
uint32_t save_state_slot;
} CALL_BACK;
typedef struct {
bool emu_running;
bool save_state;
bool load_state;
bool reset_game;
bool enable_speedlimiter;
bool lower_volume;
bool raise_volume;
bool paused;
bool frame_advance;
uint32_t save_state_slot;
} CALL_BACK;
void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font, size_t font_size);
void rdp_close();
void rdp_set_vi_register(uint32_t reg, uint32_t value);
void rdp_update_screen();
void rdp_render_frame();
CALL_BACK rdp_check_callback();
uint64_t rdp_process_commands();
void rdp_onscreen_message(const char *_message);
void rdp_new_processor(GFX_INFO _gfx_info);
void rdp_check_framebuffers(uint32_t address, uint32_t length);
size_t rdp_state_size();
void rdp_save_state(uint8_t *state);
void rdp_load_state(const uint8_t *state);
typedef enum {
MESSAGE_VERY_SHORT = 500,
MESSAGE_SHORT = 3000,
MESSAGE_LONG = 6000,
} MESSAGE_LENGTH;
void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
size_t font_size, uint32_t save_state_slot);
void rdp_close();
void rdp_set_vi_register(uint32_t reg, uint32_t value);
void rdp_update_screen();
void rdp_render_frame();
CALL_BACK rdp_check_callback();
uint64_t rdp_process_commands();
void rdp_onscreen_message(const char *message, MESSAGE_LENGTH milliseconds);
void rdp_new_processor(GFX_INFO _gfx_info);
void rdp_check_framebuffers(uint32_t address, uint32_t length);
size_t rdp_state_size();
void rdp_save_state(uint8_t *state);
void rdp_load_state(const uint8_t *state);
void rdp_set_fps(uint32_t fps, uint32_t vis);
void achievement_challenge_indicator_add(const char *achievement_title);
void achievement_challenge_indicator_remove(const char *achievement_title);
void achievement_progress_add(const char *achievement_title,
const char *progress);
void achievement_progress_remove();
void leaderboard_tracker_add(uint32_t id, const char *title,
const char *display);
void leaderboard_tracker_remove(uint32_t id);
#ifdef __cplusplus
}
+36 -54
View File
@@ -1,73 +1,55 @@
#include "wsi_platform.hpp"
#include <SDL3/SDL_vulkan.h>
VkSurfaceKHR SDL_WSIPlatform::create_surface(VkInstance instance, VkPhysicalDevice gpu)
{
VkSurfaceKHR surface = nullptr;
bool result = SDL_Vulkan_CreateSurface(window, instance, NULL, &surface);
if (result != true)
{
printf("Error creating surface\n");
}
return surface;
VkSurfaceKHR SDL_WSIPlatform::create_surface(VkInstance instance,
VkPhysicalDevice gpu) {
VkSurfaceKHR surface = nullptr;
bool result = SDL_Vulkan_CreateSurface(window, instance, NULL, &surface);
if (result != true) {
printf("Error creating surface\n");
}
return surface;
}
void SDL_WSIPlatform::destroy_surface(VkInstance instance, VkSurfaceKHR surface)
{
SDL_Vulkan_DestroySurface(instance, surface, NULL);
void SDL_WSIPlatform::destroy_surface(VkInstance instance,
VkSurfaceKHR surface) {
SDL_Vulkan_DestroySurface(instance, surface, NULL);
}
std::vector<const char *> SDL_WSIPlatform::get_instance_extensions()
{
std::vector<const char *> SDL_WSIPlatform::get_instance_extensions() {
unsigned int extensionCount = 0;
char const *const *extensions = SDL_Vulkan_GetInstanceExtensions(&extensionCount);
if (extensions == NULL)
{
printf("Error getting instance extensions\n");
}
unsigned int extensionCount = 0;
char const *const *extensions =
SDL_Vulkan_GetInstanceExtensions(&extensionCount);
if (extensions == NULL) {
printf("Error getting instance extensions\n");
}
std::vector<const char *> extensionNames;
for (unsigned int i = 0; i < extensionCount; ++i)
{
extensionNames.push_back(extensions[i]);
}
return extensionNames;
std::vector<const char *> extensionNames;
for (unsigned int i = 0; i < extensionCount; ++i) {
extensionNames.push_back(extensions[i]);
}
return extensionNames;
}
uint32_t SDL_WSIPlatform::get_surface_width()
{
int w, h;
SDL_GetWindowSize(window, &w, &h);
return w;
uint32_t SDL_WSIPlatform::get_surface_width() {
int w, h;
SDL_GetWindowSize(window, &w, &h);
return w;
}
uint32_t SDL_WSIPlatform::get_surface_height()
{
int w, h;
SDL_GetWindowSize(window, &w, &h);
return h;
uint32_t SDL_WSIPlatform::get_surface_height() {
int w, h;
SDL_GetWindowSize(window, &w, &h);
return h;
}
bool SDL_WSIPlatform::alive(Vulkan::WSI &wsi)
{
return true;
}
bool SDL_WSIPlatform::alive(Vulkan::WSI &wsi) { return true; }
void SDL_WSIPlatform::poll_input()
{
}
void SDL_WSIPlatform::poll_input() {}
void SDL_WSIPlatform::poll_input_async(Granite::InputTrackerHandler *handler)
{
}
void SDL_WSIPlatform::poll_input_async(Granite::InputTrackerHandler *handler) {}
void SDL_WSIPlatform::set_window(SDL_Window *_window)
{
window = _window;
}
void SDL_WSIPlatform::set_window(SDL_Window *_window) { window = _window; }
void SDL_WSIPlatform::do_resize()
{
resize = true;
}
void SDL_WSIPlatform::do_resize() { resize = true; }
+13 -13
View File
@@ -3,20 +3,20 @@
#include "wsi.hpp"
#include <SDL3/SDL.h>
class SDL_WSIPlatform : public Vulkan::WSIPlatform
{
class SDL_WSIPlatform : public Vulkan::WSIPlatform {
public:
VkSurfaceKHR create_surface(VkInstance instance, VkPhysicalDevice gpu) override;
void destroy_surface(VkInstance instance, VkSurfaceKHR surface) override;
std::vector<const char *> get_instance_extensions() override;
uint32_t get_surface_width() override;
uint32_t get_surface_height() override;
bool alive(Vulkan::WSI &wsi) override;
void poll_input() override;
void poll_input_async(Granite::InputTrackerHandler *handler) override;
void set_window(SDL_Window *_window);
void do_resize();
VkSurfaceKHR create_surface(VkInstance instance,
VkPhysicalDevice gpu) override;
void destroy_surface(VkInstance instance, VkSurfaceKHR surface) override;
std::vector<const char *> get_instance_extensions() override;
uint32_t get_surface_width() override;
uint32_t get_surface_height() override;
bool alive(Vulkan::WSI &wsi) override;
void poll_input() override;
void poll_input_async(Granite::InputTrackerHandler *handler) override;
void set_window(SDL_Window *_window);
void do_resize();
private:
SDL_Window *window;
SDL_Window *window;
};
+399
View File
@@ -0,0 +1,399 @@
#include "retroachievements.h"
#include "../parallel-rdp/interface.hpp"
#include <rc_client.h>
#include <rc_consoles.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void rust_server_call(const char *url, const char *post_data,
const char *content_type,
rc_client_server_callback_t callback,
void *callback_data);
void store_retroachievements_credentials(const char *username,
const char *token, void *userdata);
void notify_load_game(void *userdata);
static rc_client_t *g_client = NULL;
static const uint8_t *g_dmem = NULL;
static size_t g_dmem_size = 0;
static bool g_challenge = false;
static bool g_leaderboard = false;
static rc_client_leaderboard_list_t *g_leaderboard_list = NULL;
static uint32_t read_memory(uint32_t address, uint8_t *buffer,
uint32_t num_bytes, rc_client_t *client) {
if (address + num_bytes > g_dmem_size)
memset(buffer, 0, num_bytes);
else
memcpy(buffer, &g_dmem[address], num_bytes);
return num_bytes;
}
static void server_call(const rc_api_request_t *request,
rc_client_server_callback_t callback,
void *callback_data, rc_client_t *client) {
rust_server_call(request->url, request->post_data, request->content_type,
callback, callback_data);
}
void ra_http_callback(const char *content, size_t content_size, int status_code,
void *callback, void *callback_data) {
// Prepare a data object to pass the HTTP response to the callback
rc_api_server_response_t server_response;
memset(&server_response, 0, sizeof(server_response));
server_response.body = content;
server_response.body_length = content_size;
server_response.http_status_code = status_code;
// handle non-http errors (socket timeout, no internet available, etc)
if (status_code == 0) {
// Let rc_client know the error was not catastrophic and could be retried.
// It may decide to retry or just immediately pass the error to the
// callback. To prevent possible retries, use
// RC_API_SERVER_RESPONSE_CLIENT_ERROR.
server_response.http_status_code =
RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR;
}
((rc_client_server_callback_t)callback)(&server_response, callback_data);
}
static void log_message(const char *message, const rc_client_t *client) {
printf("RetroAchievements: %s\n", message);
}
static void login_callback(int result, const char *error_message,
rc_client_t *client, void *userdata) {
// If not successful, just report the error and bail.
if (result != RC_OK) {
if (client && !rc_client_get_userdata(client))
rc_client_set_userdata(client, strdup(error_message));
store_retroachievements_credentials(NULL, NULL, userdata);
return;
}
// Login was successful. Capture the token for future logins so we don't have
// to store the password anywhere.
const rc_client_user_t *user = rc_client_get_user_info(client);
store_retroachievements_credentials(user->username, user->token, userdata);
}
const char *ra_get_username() {
const rc_client_user_t *user = rc_client_get_user_info(g_client);
return user ? user->username : NULL;
}
const char *ra_get_token() {
const rc_client_user_t *user = rc_client_get_user_info(g_client);
return user ? user->token : NULL;
}
void ra_logout_user() { rc_client_logout(g_client); }
void ra_login_user(const char *username, const char *password, void *userdata) {
// This will generate an HTTP payload and call the server_call chain above.
// Eventually, login_callback will be called to let us know if the login was
// successful.
rc_client_begin_login_with_password(g_client, username, password,
login_callback, userdata);
}
void ra_login_token_user(const char *username, const char *token,
void *userdata) {
// This is exactly the same functionality as
// rc_client_begin_login_with_password, but uses the token captured from the
// first login instead of a password. Note that it uses the same callback.
rc_client_begin_login_with_token(g_client, username, token, login_callback,
userdata);
}
static void load_game_callback(int result, const char *error_message,
rc_client_t *client, void *userdata) {
if (result != RC_OK) {
if (client && !rc_client_get_userdata(client))
rc_client_set_userdata(client, strdup(error_message));
rc_client_set_hardcore_enabled(client, false);
notify_load_game(userdata);
return;
}
if (!rc_client_is_processing_required(client)) {
rc_client_set_hardcore_enabled(client, false);
}
g_leaderboard_list = rc_client_create_leaderboard_list(
client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE);
notify_load_game(userdata);
}
void ra_welcome() {
if (!g_client)
return;
char buffer[512];
char *error_message = rc_client_get_userdata(g_client);
if (error_message) {
snprintf(buffer, sizeof(buffer), "RA Error: %s", error_message);
rdp_onscreen_message(buffer, MESSAGE_LONG);
free(error_message);
rc_client_set_userdata(g_client, NULL);
return;
}
const rc_client_game_t *game = rc_client_get_game_info(g_client);
rc_client_user_game_summary_t summary;
rc_client_get_user_game_summary(g_client, &summary);
int message_length = snprintf(
buffer, sizeof(buffer), "%s\nMode: %s\n", game->title,
rc_client_get_hardcore_enabled(g_client) ? "Hardcore" : "Softcore");
if (summary.num_core_achievements != 0) {
snprintf(buffer + message_length, sizeof(buffer) - message_length,
"%u/%u achievements unlocked", summary.num_unlocked_achievements,
summary.num_core_achievements);
} else {
snprintf(buffer + message_length, sizeof(buffer) - message_length,
"Game has no achievements");
}
rdp_onscreen_message(buffer, MESSAGE_LONG);
}
void ra_load_game(const uint8_t *rom, size_t rom_size, void *userdata) {
rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_NINTENDO_64, NULL,
rom, rom_size, load_game_callback,
userdata);
}
void ra_unload_game() {
if (g_leaderboard_list) {
rc_client_destroy_leaderboard_list(g_leaderboard_list);
g_leaderboard_list = NULL;
}
rc_client_unload_game(g_client);
}
void ra_set_dmem(const uint8_t *dmem, size_t dmem_size) {
g_dmem = dmem;
g_dmem_size = dmem_size;
}
static void leaderboard_submitted(const rc_client_leaderboard_t *leaderboard) {
char buffer[512];
snprintf(buffer, sizeof(buffer), "Leaderboard submitted: %s - %s",
leaderboard->title, leaderboard->tracker_value);
rdp_onscreen_message(buffer, MESSAGE_SHORT);
}
static void achievement_triggered(const rc_client_achievement_t *achievement) {
char buffer[512];
snprintf(buffer, sizeof(buffer), "Unlocked: %s", achievement->title);
rdp_onscreen_message(buffer, MESSAGE_SHORT);
}
static void game_completed(rc_client_t *client) {
char buffer[512];
const rc_client_game_t *game = rc_client_get_game_info(client);
snprintf(buffer, sizeof(buffer), "%s: %s",
rc_client_get_hardcore_enabled(client) ? "Mastered" : "Completed",
game->title);
rdp_onscreen_message(buffer, MESSAGE_SHORT);
}
static void subset_completed(const rc_client_subset_t *subset,
rc_client_t *client) {
char buffer[512];
snprintf(buffer, sizeof(buffer), "Subset %s: %s",
rc_client_get_hardcore_enabled(client) ? "mastered" : "completed",
subset->title);
rdp_onscreen_message(buffer, MESSAGE_SHORT);
}
static void server_error(const rc_client_server_error_t *server_error) {
char buffer[512];
snprintf(buffer, sizeof(buffer), "RA server error: %s",
server_error->error_message);
rdp_onscreen_message(buffer, MESSAGE_SHORT);
}
static const char *get_leaderboard_title(const char *display) {
if (g_leaderboard_list == NULL)
return NULL;
for (uint32_t i = 0; i < g_leaderboard_list->num_buckets; i++) {
for (uint32_t j = 0; j < g_leaderboard_list->buckets[i].num_leaderboards;
j++) {
// this looks like a mistake, but it is intentional.
// Leaderboard trackers don't contain the title,
// but we take advantage of the fact that tracker_value and display point
// to the same char* to find a matching leaderboard.
if (g_leaderboard_list->buckets[i].leaderboards[j]->tracker_value ==
display) {
return g_leaderboard_list->buckets[i].leaderboards[j]->title;
}
}
}
return NULL;
}
static void event_handler(const rc_client_event_t *event, rc_client_t *client) {
switch (event->type) {
case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED:
achievement_triggered(event->achievement);
break;
case RC_CLIENT_EVENT_LEADERBOARD_STARTED:
break;
case RC_CLIENT_EVENT_LEADERBOARD_FAILED:
break;
case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED:
if (g_leaderboard)
leaderboard_submitted(event->leaderboard);
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW:
if (g_challenge)
achievement_challenge_indicator_add(event->achievement->title);
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE:
if (g_challenge)
achievement_challenge_indicator_remove(event->achievement->title);
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW:
achievement_progress_add(event->achievement->title,
event->achievement->measured_progress);
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE:
achievement_progress_remove();
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE:
achievement_progress_add(event->achievement->title,
event->achievement->measured_progress);
break;
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW:
if (g_leaderboard)
leaderboard_tracker_add(
event->leaderboard_tracker->id,
get_leaderboard_title(event->leaderboard_tracker->display),
event->leaderboard_tracker->display);
break;
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE:
if (g_leaderboard)
leaderboard_tracker_remove(event->leaderboard_tracker->id);
break;
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE:
if (g_leaderboard)
leaderboard_tracker_add(
event->leaderboard_tracker->id,
get_leaderboard_title(event->leaderboard_tracker->display),
event->leaderboard_tracker->display);
break;
case RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD:
break;
case RC_CLIENT_EVENT_GAME_COMPLETED:
game_completed(client);
break;
case RC_CLIENT_EVENT_SUBSET_COMPLETED:
subset_completed(event->subset, client);
break;
case RC_CLIENT_EVENT_SERVER_ERROR:
server_error(event->server_error);
break;
default:
printf("RetroAchievements: Unhandled event %d\n", event->type);
break;
}
}
void ra_init_client(bool hardcore, bool challenge, bool leaderboard) {
// Create the client instance (using a global variable simplifies this
// example)
g_client = rc_client_create(read_memory, server_call);
g_dmem = NULL;
g_dmem_size = 0;
// Provide a logging function to simplify debugging
rc_client_enable_logging(g_client, RC_CLIENT_LOG_LEVEL_WARN, log_message);
rc_client_set_event_handler(g_client, event_handler);
rc_client_set_hardcore_enabled(g_client, hardcore);
g_challenge = challenge;
g_leaderboard = leaderboard;
}
bool ra_get_hardcore() { return rc_client_get_hardcore_enabled(g_client); }
void ra_shutdown_client() {
if (g_client) {
// Release resources associated to the client instance
rc_client_destroy(g_client);
g_client = NULL;
}
}
void ra_do_frame() { rc_client_do_frame(g_client); }
void ra_do_idle() { rc_client_idle(g_client); }
size_t ra_state_size() { return rc_client_progress_size(g_client); }
void ra_save_state(uint8_t *state, size_t state_size) {
if (!rc_client_is_game_loaded(g_client))
return;
if (rc_client_serialize_progress_sized(g_client, state, state_size) !=
RC_OK) {
printf("RetroAchievements: Failed to serialize progress\n");
}
}
void ra_load_state(const uint8_t *state, size_t state_size) {
if (!rc_client_is_game_loaded(g_client))
return;
if (rc_client_deserialize_progress_sized(g_client, state, state_size) !=
RC_OK) {
printf("RetroAchievements: Failed to deserialize progress\n");
}
}
void ra_display_inprogress_achievements(void *userdata) {
if (!rc_client_is_game_loaded(g_client))
return;
rc_client_achievement_list_t *list = rc_client_create_achievement_list(
g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE,
RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE);
char buffer[1024] = {0};
size_t buffer_length = 0;
for (uint32_t i = 0; i < list->num_buckets; i++) {
if (list->buckets[i].bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED) {
for (uint32_t j = 0; j < list->buckets[i].num_achievements; j++) {
const rc_client_achievement_t *achievement =
list->buckets[i].achievements[j];
if (achievement->measured_percent > 0.0f) {
if (buffer_length < sizeof(buffer) - 1) {
buffer_length += snprintf(
buffer + buffer_length, sizeof(buffer) - buffer_length,
"%s: %s\n", achievement->title, achievement->measured_progress);
}
}
}
}
}
if (buffer_length > 0) {
rdp_onscreen_message(buffer, MESSAGE_SHORT);
}
rc_client_destroy_achievement_list(list);
}
+32
View File
@@ -0,0 +1,32 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
void ra_init_client(bool hardcore, bool challenge, bool leaderboard);
void ra_shutdown_client();
void ra_welcome();
bool ra_get_hardcore();
void ra_load_game(const uint8_t *rom, size_t rom_size, void *userdata);
void ra_unload_game();
void ra_set_dmem(const uint8_t *dmem, size_t dmem_size);
void ra_do_frame();
void ra_do_idle();
void ra_http_callback(const char *content, size_t content_size, int status_code,
void *callback, void *callback_data);
void ra_logout_user();
void ra_login_user(const char *username, const char *password, void *userdata);
void ra_login_token_user(const char *username, const char *token,
void *userdata);
const char *ra_get_username();
const char *ra_get_token();
size_t ra_state_size();
void ra_save_state(uint8_t *state, size_t state_size);
void ra_load_state(const uint8_t *state, size_t state_size);
#ifdef __cplusplus
extern "C" {
#endif
void ra_display_inprogress_achievements(void *userdata);
#ifdef __cplusplus
}
#endif
+1 -1
View File
@@ -1,2 +1,2 @@
[toolchain]
channel = "1.93.0"
channel = "1.95.0"
+15 -7
View File
@@ -15,16 +15,24 @@ pub struct Cheats {
pub enabled: bool,
}
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct CheatData {
pub note: String,
pub data: Vec<String>,
pub options: Option<std::collections::BTreeMap<String, String>>,
}
pub type Cheat = std::collections::BTreeMap<String, std::collections::BTreeMap<String, CheatData>>;
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 cheats = serde_json::from_slice::<Cheat>(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() {
@@ -71,7 +79,7 @@ pub fn init(
if !device.cheats.cheats.is_empty() {
device.cheats.enabled = true;
ui::video::onscreen_message(&device.ui, "Cheats enabled");
ui::video::onscreen_message("Cheats enabled", ui::video::MESSAGE_LENGTH_MESSAGE_SHORT);
}
}
+33 -17
View File
@@ -10,9 +10,9 @@ include!(concat!(env!("OUT_DIR"), "/simd_bindings.rs"));
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
use rand_chacha::rand_core::{RngCore, SeedableRng};
use rand::{Rng, SeedableRng};
use crate::{cheats, netplay, ui};
use crate::{cheats, netplay, retroachievements, ui};
use std::{collections::HashMap, fs, io::Read};
pub mod ai;
@@ -44,24 +44,30 @@ pub mod tlb;
pub mod unmapped;
pub mod vi;
pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, game_settings: ui::gui::GameSettings) {
device.ui.video.fullscreen = game_settings.fullscreen;
pub fn run_game(device: &mut Device, rom_contents: &[u8], game_settings: ui::GameSettings) {
device.cpu.overclock = game_settings.overclock;
if game_settings.disable_expansion_pak {
device.rdram.size = 0x400000;
}
if let Some(slot) = game_settings.load_savestate_slot {
device.ui.storage.save_state_slot = slot;
device.load_state = true;
}
init_rng(device);
cart::rom::init(device, rom_contents); // cart needs to come before rdram
// rdram pointer is shared with parallel-rdp
// rdram pointer is shared with parallel-rdp and retroachievements
rdram::init(device);
ui::video::init(device);
ui::audio::init(&mut device.ui, device.ai.freq);
ui::input::init(&mut device.ui);
// must be after video init
retroachievements::welcome();
mi::init(device);
pif::init(device);
if device.ui.config.input.emulate_vru && device.netplay.is_none() {
@@ -77,18 +83,22 @@ pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, game_settings: ui::g
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);
if !retroachievements::get_hardcore() {
cheats::init(device, game_settings.cheats);
}
cpu::run(device);
retroachievements::unload_game();
ui::storage::write_saves(device);
ui::input::close(&mut device.ui);
ui::audio::close(&mut device.ui);
ui::video::close(&device.ui);
}
fn set_rng() -> rand_chacha::ChaCha8Rng {
rand_chacha::ChaCha8Rng::from_os_rng()
fn set_rng() -> rand::rngs::Xoshiro256PlusPlus {
rand::rngs::Xoshiro256PlusPlus::try_from_rng(&mut rand::rngs::SysRng).unwrap()
}
fn init_rng(device: &mut Device) {
@@ -100,7 +110,7 @@ fn init_rng(device: &mut Device) {
rng_seed = netplay::receive_rng(netplay);
}
}
device.rng = rand_chacha::ChaCha8Rng::seed_from_u64(rng_seed);
device.rng = rand::rngs::Xoshiro256PlusPlus::seed_from_u64(rng_seed);
}
fn swap_rom(contents: Vec<u8>) -> Option<Vec<u8>> {
@@ -131,7 +141,11 @@ fn swap_rom(contents: Vec<u8>) -> Option<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") {
if file_path
.extension()
.unwrap_or_default()
.eq_ignore_ascii_case("zip")
{
let zip_file = fs::File::open(file_path).unwrap();
let mut archive = zip::ZipArchive::new(zip_file).unwrap();
for i in 0..archive.len() {
@@ -148,7 +162,11 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
break;
}
}
} else if file_path.extension().unwrap().eq_ignore_ascii_case("7z") {
} else if file_path
.extension()
.unwrap_or_default()
.eq_ignore_ascii_case("7z")
{
let mut archive =
sevenz_rust2::ArchiveReader::open(file_path, sevenz_rust2::Password::empty()).unwrap();
@@ -173,7 +191,8 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
)
.expect("ok");
} else {
contents = fs::read(file_path).expect("Should have been able to read the file");
contents = fs::read(file_path)
.unwrap_or_else(|_| panic!("Could not read ROM file: {}", file_path.display()));
}
if contents.is_empty() {
@@ -206,7 +225,7 @@ pub struct Device {
pub si: si::Si,
pub ri: ri::Ri,
#[serde(skip, default = "set_rng")]
pub rng: rand_chacha::ChaCha8Rng,
pub rng: rand::rngs::Xoshiro256PlusPlus,
pub vru: controller::vru::Vru,
pub transferpaks: [controller::transferpak::TransferPak; 4],
pub cheats: cheats::Cheats,
@@ -237,7 +256,6 @@ impl Device {
reg_write_masks: [0; cop0::COP0_REGS_COUNT as usize],
reg_latch: 0,
is_event: false,
pending_compare_interrupt: false,
instrs: [cop0::reserved; 32],
instrs2: [cop0::reserved; 32],
tlb_lut_w: vec![
@@ -472,10 +490,8 @@ impl Device {
delay: 0,
count_per_scanline: 0,
enable_speed_limiter: true,
limiter: None,
next_pace_deadline: None,
vi_counter: 0,
last_origin: 0,
internal_frame_counter: 0,
min_wait_time: std::time::Duration::from_secs(1),
frame_time: 0.0,
limit_freq: 2,
+7 -3
View File
@@ -133,7 +133,7 @@ pub fn dma_write(
device::pi::calculate_cycles(device, 1, length)
}
pub fn init(device: &mut device::Device, rom_file: Vec<u8>) {
pub fn init(device: &mut device::Device, rom_file: &[u8]) {
let now: chrono::DateTime<chrono::Local> = chrono::Local::now();
device.cart.rtc_timestamp = now.naive_local().and_utc().timestamp();
if let Some(netplay) = &mut device.netplay {
@@ -146,7 +146,7 @@ pub fn init(device: &mut device::Device, rom_file: Vec<u8>) {
device.cart.sc64.cfg[device::cart::sc64::SC64_BOOTLOADER_SWITCH as usize] = 1;
device.cart.rom = rom_file;
device.cart.rom = rom_file.to_vec();
device.cart.pal = is_system_pal(device.cart.rom[0x3E]);
set_cic(device);
@@ -200,5 +200,9 @@ fn set_cic(device: &mut device::Device) {
pub fn calculate_hash(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
format!("{:X}", hasher.finalize())
hasher
.finalize()
.iter()
.map(|b| format!("{:02X}", b))
.collect()
}
+12 -3
View File
@@ -190,21 +190,30 @@ pub fn pak_switch_event(device: &mut device::Device) {
write: device::controller::mempak::write,
pak_type: new_pak_type,
});
ui::video::onscreen_message(&device.ui, "MemPak selected");
ui::video::onscreen_message(
"MemPak selected",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
} else if new_pak_type == PakType::RumblePak {
channel.pak_handler = Some(device::controller::PakHandler {
read: device::controller::rumble::read,
write: device::controller::rumble::write,
pak_type: new_pak_type,
});
ui::video::onscreen_message(&device.ui, "RumblePak selected");
ui::video::onscreen_message(
"RumblePak selected",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
} else if new_pak_type == PakType::TransferPak {
channel.pak_handler = Some(device::controller::PakHandler {
read: device::controller::transferpak::read,
write: device::controller::transferpak::write,
pak_type: new_pak_type,
});
ui::video::onscreen_message(&device.ui, "TransferPak selected");
ui::video::onscreen_message(
"TransferPak selected",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
}
channel.change_pak = PakType::None;
}
+7 -11
View File
@@ -110,7 +110,7 @@ fn write_mbc3(
if address < 0x2000 {
cart.ram_enabled = value & 0xf == 0xa;
} else if address < 0x4000 {
let bank = value & 0x7f;
let bank = value & if cart.rom.len() > 2097152 { 0xff } else { 0x7f };
cart.rom_bank = bank as u32;
if cart.rom_bank == 0 {
cart.rom_bank = 1;
@@ -182,16 +182,12 @@ fn read_mbc3(
return;
}
if cart.ram_bank < 0x8 {
if cart.ram_bank > 3 {
for i in 0..size {
pif_ram[data + i] = 0;
}
} else {
let banked_address = address - 0xA000 + (cart.ram_bank * 0x2000);
pif_ram[data..data + size].copy_from_slice(
&cart.ram[banked_address as usize..banked_address as usize + size],
);
}
let banked_address = address - 0xA000 + (cart.ram_bank * 0x2000);
pif_ram[data..data + size].copy_from_slice(
cart.ram
.get(banked_address as usize..banked_address as usize + size)
.unwrap_or(&vec![0; size]),
);
} else {
let latch = match cart.ram_bank {
0x8 => cart.latch_second,
+7 -6
View File
@@ -1,4 +1,6 @@
use crate::{device, ui};
use crate::device;
#[cfg(feature = "gui")]
use crate::ui;
use std::collections::HashMap;
const JCMD_VRU_READ: u8 = 0x09;
@@ -179,11 +181,10 @@ pub fn process(device: &mut device::Device, channel: usize) {
device.pif.ram[device.pif.channels[channel].rx_buf.unwrap()] = 0;
}
JCMD_VRU_READ => {
let index = if let Some(weak) = &device.ui.weak {
ui::vru::prompt_for_match(&device.vru.words, weak, device.vi.frame_time)
} else {
0x7FFF
};
#[cfg(feature = "gui")]
let index = ui::vru::prompt_for_match(&device.vru.words, device.vi.frame_time);
#[cfg(not(feature = "gui"))]
let index = 0x7FFF;
let num_results = if index == 0x7FFF { 0 } else { 1 };
let data: HashMap<usize, u16> = HashMap::from([
(0, 0x8000),
+13 -9
View File
@@ -30,13 +30,15 @@ pub const COP0_XCONTEXT_REG: u32 = 20;
//const COP0_CACHEERR_REG: u32 = 27;
pub const COP0_TAGLO_REG: u32 = 28;
//const COP0_TAGHI_REG: u32 = 29;
const COP0_ERROREPC_REG: u32 = 30;
pub const COP0_ERROREPC_REG: u32 = 30;
//const COP0_UNUSED_31: u32 = 31;
pub const COP0_REGS_COUNT: u32 = 32;
pub const COP0_STATUS_IE: u64 = 1 << 0;
pub const COP0_STATUS_EXL: u64 = 1 << 1;
pub const COP0_STATUS_ERL: u64 = 1 << 2;
pub const COP0_STATUS_SR: u64 = 1 << 20;
pub const COP0_STATUS_TS: u64 = 1 << 21;
pub const COP0_STATUS_BEV: u64 = 1 << 22;
pub const COP0_STATUS_FR: u64 = 1 << 26;
pub const COP0_STATUS_CU1: u64 = 1 << 29;
@@ -55,6 +57,7 @@ pub const COP0_CAUSE_EXCCODE_CPU: u64 = 11 << 2;
pub const COP0_CAUSE_EXCCODE_TR: u64 = 13 << 2;
pub const COP0_CAUSE_EXCCODE_FPE: u64 = 15 << 2;
pub const COP0_CAUSE_IP2: u64 = 1 << 10;
pub const COP0_CAUSE_IP4: u64 = 1 << 12;
pub const COP0_CAUSE_IP7: u64 = 1 << 15;
pub const COP0_CAUSE_BD: u64 = 1 << 31;
@@ -62,7 +65,7 @@ pub const COP0_CAUSE_CE1: u64 = 1 << 28;
pub const COP0_CAUSE_CE2: u64 = 1 << 29;
pub const COP0_CAUSE_IP_MASK: u64 = 0b00000000000000001111111100000000;
//pub const COP0_CAUSE_EXCCODE_MASK: u64 = 0x1F << 2;
pub const COP0_CAUSE_EXCCODE_MASK: u64 = 0x1F << 2;
pub const COP0_CONTEXT_BADVPN2_MASK: u64 = 0b00000000011111111111111111110000;
pub const COP0_XCONTEXT_BADVPN2_MASK: u64 = 0b01111111111111111111111111110000;
pub const COP0_XCONTEXT_REGION_MASK: u64 = 0b110000000000000000000000000000000;
@@ -101,7 +104,6 @@ pub struct Cop0 {
pub tlb_lut_w: Vec<device::tlb::TlbLut>,
pub tlb_entries: [device::tlb::TlbEntry; 32],
pub is_event: bool,
pub pending_compare_interrupt: bool,
}
fn mfc0(device: &mut device::Device, opcode: u32) {
@@ -220,12 +222,11 @@ fn set_control_registers(device: &mut device::Device, index: u32, mut data: u64)
(compare_event_diff as u64) << 1,
);
device.cpu.cop0.regs[COP0_CAUSE_REG as usize] &= !COP0_CAUSE_IP7;
device.cpu.cop0.pending_compare_interrupt = false;
}
COP0_STATUS_REG => {
if data & COP0_STATUS_FR != device.cpu.cop0.regs[index as usize] & COP0_STATUS_FR {
device::cop1::set_fgr_registers(device, data)
}
COP0_STATUS_REG
if data & COP0_STATUS_FR != device.cpu.cop0.regs[index as usize] & COP0_STATUS_FR =>
{
device::cop1::set_fgr_registers(device, data)
}
_ => {}
}
@@ -238,7 +239,10 @@ fn set_control_registers(device: &mut device::Device, index: u32, mut data: u64)
}
pub fn compare_event(device: &mut device::Device) {
device.cpu.cop0.pending_compare_interrupt = true;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] |= device::cop0::COP0_CAUSE_IP7;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
!device::cop0::COP0_CAUSE_EXCCODE_MASK;
device::events::create_event_at(
device,
device::events::EVENT_TYPE_COMPARE,
+1 -1
View File
@@ -228,8 +228,8 @@ pub fn init(device: &mut device::Device) {
93750000
} else {
ui::video::onscreen_message(
&device.ui,
"Overclocking enabled, setting clock rate to 125 MHz",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
125000000
};
+3 -1
View File
@@ -14,7 +14,8 @@ pub const EVENT_TYPE_COMPARE: usize = 9;
pub const EVENT_TYPE_VRU: usize = 10;
pub const EVENT_TYPE_PAK: usize = 11;
pub const EVENT_TYPE_SAVE: usize = 12;
pub const EVENT_TYPE_COUNT: usize = 13;
pub const EVENT_TYPE_NMI: usize = 13;
pub const EVENT_TYPE_COUNT: usize = 14;
#[derive(PartialEq, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub struct Event {
@@ -63,6 +64,7 @@ fn get_event_handler(name: usize) -> fn(&mut device::Device) {
EVENT_TYPE_VRU => device::controller::vru::vru_talking_event,
EVENT_TYPE_PAK => device::controller::pak_switch_event,
EVENT_TYPE_SAVE => ui::storage::save_event,
EVENT_TYPE_NMI => device::exceptions::reset_event,
_ => dummy_event,
}
}
+22 -3
View File
@@ -1,4 +1,5 @@
use crate::device;
use crate::ui;
pub fn check_pending_interrupts(device: &mut device::Device) {
if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize]
@@ -15,9 +16,9 @@ pub fn check_pending_interrupts(device: &mut device::Device) {
& device.mi.regs[device::mi::MI_INTR_MASK_REG as usize]
!= 0
{
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] = device::cop0::COP0_CAUSE_IP2;
} else if device.cpu.cop0.pending_compare_interrupt {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] = device::cop0::COP0_CAUSE_IP7;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] |= device::cop0::COP0_CAUSE_IP2;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
!device::cop0::COP0_CAUSE_EXCCODE_MASK;
}
if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize]
@@ -150,6 +151,24 @@ pub fn tlb_miss_exception(
exception_general(device, vector_offset)
}
pub fn reset_event(device: &mut device::Device) {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &= !device::cop0::COP0_CAUSE_IP4;
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] |= device::cop0::COP0_STATUS_ERL
| device::cop0::COP0_STATUS_SR
| device::cop0::COP0_STATUS_BEV;
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] &= !device::cop0::COP0_STATUS_TS;
device.cpu.cop0.regs[device::cop0::COP0_ERROREPC_REG as usize] = device.cpu.pc;
device.cpu.pc = 0xBFC00000;
device.cpu.branch_state.state = device::cpu::State::Step;
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] = 0;
device::pif::reset_pif(device, true);
ui::video::onscreen_message("Game reset", ui::video::MESSAGE_LENGTH_MESSAGE_SHORT);
}
fn exception_general(device: &mut device::Device, vector_offset: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_EXL
== 0
+2 -2
View File
@@ -1,6 +1,6 @@
use crate::device;
use crate::ui;
use rand_chacha::rand_core::RngCore;
use rand::Rng;
const PI_DRAM_ADDR_REG: u32 = 0;
const PI_CART_ADDR_REG: u32 = 1;
@@ -169,7 +169,7 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
}
}
fn randomize_interrupt_time(rng: &mut rand_chacha::ChaCha8Rng) -> u64 {
fn randomize_interrupt_time(rng: &mut rand::rngs::Xoshiro256PlusPlus) -> u64 {
rng.next_u64() % 0x10
}
+12 -3
View File
@@ -249,14 +249,23 @@ fn get_default_handler(device: &device::Device) -> device::controller::PakHandle
}
}
pub fn init(device: &mut device::Device) {
pub fn reset_pif(device: &mut device::Device, is_nmi_reset: bool) {
if device.cart.pal {
device.pif.rom = rom::PAL_PIF_ROM;
} else {
device.pif.rom = rom::NTSC_PIF_ROM;
}
device.pif.ram[0x26] = device.cart.cic_seed;
device.pif.ram[0x27] = device.cart.cic_seed;
let reset_type = u32::from(is_nmi_reset);
let rom_type = 0u32;
let s7 = 0u32;
let cic = u32::from(device.cart.cic_seed);
let word = (rom_type << 19) | (s7 << 18) | (reset_type << 17) | (cic << 8) | 0x3f;
device.pif.ram[0x24..0x28].copy_from_slice(&word.to_be_bytes());
}
pub fn init(device: &mut device::Device) {
reset_pif(device, false);
let default_handler = get_default_handler(device);
let tpak_handler = device::controller::PakHandler {
+3
View File
@@ -1,4 +1,5 @@
use crate::device;
use crate::retroachievements;
use crate::ui;
use std::alloc::{Layout, alloc_zeroed};
@@ -151,6 +152,8 @@ pub fn init(device: &mut device::Device) {
device.rdram.mem =
unsafe { Vec::from_raw_parts(ptr, device.rdram.size as usize, device.rdram.size as usize) };
retroachievements::set_dmem(device.rdram.mem.as_ptr(), device.rdram.size as usize);
// hack, skip RDRAM initialization
device
.rdram
+2 -2
View File
@@ -844,7 +844,7 @@ pub fn vrcpl(device: &mut device::Device, opcode: u32) {
let vte = vte(device, vt(opcode), ve(opcode) as usize);
let input = if device.rsp.cpu.divdp {
((device.rsp.cpu.divin as i32) << 16)
| get_vpr16(device.rsp.cpu.vpr[vt(opcode) as usize], ve(opcode) as u8) as u16 as i32
| get_vpr16(device.rsp.cpu.vpr[vt(opcode) as usize], ve(opcode) as u8) as i32
} else {
get_vpr16(device.rsp.cpu.vpr[vt(opcode) as usize], ve(opcode) as u8) as i16 as i32
};
@@ -901,7 +901,7 @@ pub fn vrsql(device: &mut device::Device, opcode: u32) {
let vte = vte(device, vt(opcode), ve(opcode) as usize);
let input = if device.rsp.cpu.divdp {
((device.rsp.cpu.divin as i32) << 16)
| get_vpr16(device.rsp.cpu.vpr[vt(opcode) as usize], ve(opcode) as u8) as u16 as i32
| get_vpr16(device.rsp.cpu.vpr[vt(opcode) as usize], ve(opcode) as u8) as i32
} else {
get_vpr16(device.rsp.cpu.vpr[vt(opcode) as usize], ve(opcode) as u8) as i16 as i32
};
+2 -3
View File
@@ -1,7 +1,6 @@
use rand_chacha::rand_core::RngCore;
use crate::device;
use crate::ui;
use rand::Rng;
const SI_DRAM_ADDR_REG: u32 = 0;
const SI_PIF_ADDR_RD64B_REG: u32 = 1;
@@ -39,7 +38,7 @@ pub fn read_regs(
device.si.regs[((address & 0xFFFF) >> 2) as usize]
}
fn randomize_interrupt_time(rng: &mut rand_chacha::ChaCha8Rng) -> u64 {
fn randomize_interrupt_time(rng: &mut rand::rngs::Xoshiro256PlusPlus) -> u64 {
rng.next_u64() % 0x100
}
+58 -50
View File
@@ -1,5 +1,4 @@
use crate::{cheats, device, netplay, ui};
use governor::clock::Clock;
use crate::{cheats, device, netplay, retroachievements, ui};
const VI_STATUS_REG: u32 = 0;
const VI_ORIGIN_REG: u32 = 1;
@@ -24,12 +23,10 @@ pub struct Vi {
pub delay: u64,
pub field: u32,
#[serde(skip)]
pub limiter: Option<governor::DefaultDirectRateLimiter>,
pub next_pace_deadline: Option<std::time::Instant>,
pub count_per_scanline: u64,
pub enable_speed_limiter: bool,
pub vi_counter: u64,
pub last_origin: u32,
pub internal_frame_counter: u64,
pub min_wait_time: std::time::Duration,
pub frame_time: f64,
pub limit_freq: u64,
@@ -50,17 +47,11 @@ pub fn set_expected_refresh_rate(device: &mut device::Device) {
device.vi.count_per_scanline =
device.vi.delay / (device.vi.regs[VI_V_SYNC_REG as usize] + 1) as u64;
create_limiter(device);
reset_pace_deadline(device);
}
fn create_limiter(device: &mut device::Device) {
let quota = governor::Quota::with_period(std::time::Duration::from_secs_f64(
device.vi.frame_time * device.vi.limit_freq as f64,
))
.unwrap();
device.vi.limiter = Some(governor::RateLimiter::direct(quota));
let _ = device.vi.limiter.as_ref().unwrap().check();
//println!("new limit freq: {}", device.vi.limit_freq);
fn reset_pace_deadline(device: &mut device::Device) {
device.vi.next_pace_deadline = None;
}
fn set_vertical_interrupt(device: &mut device::Device) {
@@ -70,12 +61,10 @@ fn set_vertical_interrupt(device: &mut device::Device) {
}
fn set_current_line(device: &mut device::Device) {
let next_vi = device::events::get_event(device, device::events::EVENT_TYPE_VI);
if let Some(next_vi) = next_vi {
device.vi.regs[VI_CURRENT_REG as usize] = ((device.vi.delay
- (next_vi.count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize]))
/ device.vi.count_per_scanline)
as u32;
if let Some(next_vi) = device::events::get_event(device, device::events::EVENT_TYPE_VI) {
device.vi.regs[VI_CURRENT_REG as usize] = ((device.vi.delay.saturating_sub(
next_vi.count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize],
)) / device.vi.count_per_scanline) as u32;
// wrap around VI_CURRENT_REG if needed
if device.vi.regs[VI_CURRENT_REG as usize] >= device.vi.regs[VI_V_SYNC_REG as usize] {
@@ -119,10 +108,10 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
}
}
VI_ORIGIN_REG => {
let current_origin = device.vi.regs[reg as usize];
device::memory::masked_write_32(&mut device.vi.regs[reg as usize], value, mask);
if device.vi.regs[reg as usize] != device.vi.last_origin {
device.vi.last_origin = device.vi.regs[reg as usize];
device.vi.internal_frame_counter += 1;
if current_origin != device.vi.regs[reg as usize] {
let _ = device.ui.video.fps_tx.try_send(true);
}
}
_ => {
@@ -138,6 +127,9 @@ pub fn vertical_interrupt_event(device: &mut device::Device) {
}
ui::video::render_frame();
let _ = device.ui.video.vis_tx.try_send(true);
retroachievements::do_frame();
let (mut speed_limiter_toggled, paused) = ui::video::check_callback(device);
@@ -157,19 +149,18 @@ pub fn vertical_interrupt_event(device: &mut device::Device) {
device.vi.vi_counter += 1;
if device.netplay.is_none() && paused {
ui::video::pause_loop(device.vi.frame_time);
if retroachievements::get_hardcore() {
ui::video::onscreen_message(
"Cannot pause in RA hardcore mode",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
} else {
ui::video::pause_loop(device.vi.frame_time);
}
}
unsafe { sdl3_sys::events::SDL_PumpEvents() };
/*
let vis = if device.cart.pal { 50 } else { 60 };
if device.vi.vi_counter % vis == 0 {
println!("FPS: {}", device.vi.internal_frame_counter);
device.vi.internal_frame_counter = 0;
}
*/
/* toggle vi field if in interlaced mode */
device.vi.field ^= (device.vi.regs[VI_STATUS_REG as usize] >> 6) & 0x1;
@@ -190,34 +181,51 @@ pub fn init(device: &mut device::Device) {
}
}
fn speed_limiter(device: &mut device::Device, speed_limiter_toggled: bool) {
if let Err(outcome) = device.vi.limiter.as_ref().unwrap().check() {
let dur = outcome.wait_time_from(governor::clock::DefaultClock::default().now());
spin_sleep::sleep(dur);
if dur < device.vi.min_wait_time {
device.vi.min_wait_time = dur;
}
fn speed_limiter(device: &mut device::Device, mut speed_limiter_toggled: bool) {
let interval =
std::time::Duration::from_secs_f64(device.vi.frame_time * device.vi.limit_freq as f64);
let _ = device.vi.limiter.as_ref().unwrap().check();
} else {
device.vi.min_wait_time = std::time::Duration::from_secs(0);
let now = std::time::Instant::now();
match device.vi.next_pace_deadline {
None => {
device.vi.next_pace_deadline = Some(now + interval);
speed_limiter_toggled = true;
}
Some(deadline) => {
if now < deadline {
let dur = deadline - now;
spin_sleep::sleep(dur);
if dur < device.vi.min_wait_time {
device.vi.min_wait_time = dur;
}
} else {
//println!("did not sleep");
device.vi.min_wait_time = std::time::Duration::from_secs(0);
}
let mut next = deadline + interval;
let t = std::time::Instant::now();
while next <= t {
next += interval;
}
device.vi.next_pace_deadline = Some(next);
}
}
if std::time::Instant::now()
.duration_since(device.vi.limit_freq_check)
.as_secs_f64()
> 1.0
if std::time::Instant::now().duration_since(device.vi.limit_freq_check)
> std::time::Duration::from_secs(1)
{
if !speed_limiter_toggled {
if device.vi.min_wait_time.as_secs_f64() == 0.0 && device.vi.limit_freq < MAX_LIMIT_FREQ
if device.vi.min_wait_time == std::time::Duration::from_secs(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
reset_pace_deadline(device);
} else if device.vi.min_wait_time
> std::time::Duration::from_secs_f64(device.vi.frame_time)
&& device.vi.limit_freq > 1
{
device.vi.limit_freq -= 1;
create_limiter(device);
reset_pace_deadline(device);
}
}
+178 -48
View File
@@ -6,86 +6,119 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
mod cheats;
mod device;
mod netplay;
mod retroachievements;
mod savestates;
mod ui;
use clap::Parser;
use std::io::Error;
use ui::gui;
/// N64 emulator
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
#[command(author, version=env!("GIT_DESCRIBE"), about, long_about = None, arg_required_else_help = if cfg!(feature = "gui") { false } else { true })]
struct Args {
game: Option<String>,
#[arg(short, long)]
fullscreen: bool,
#[arg(long)]
overclock: Option<bool>,
#[arg(long)]
disable_expansion_pak: Option<bool>,
#[arg(long, value_name = "CHEATS_FILE", hide = true)]
cheats: Option<String>,
#[arg(long, value_name = "NETPLAY_PEER_ADDR", hide = true)]
netplay_peer_addr: Option<String>,
#[arg(long, value_name = "NETPLAY_PLAYER_NUMBER", hide = true)]
netplay_player_number: Option<u8>,
#[arg(
short,
long,
value_name = "PROFILE_NAME",
help = "Create a new input profile (keyboard/gamepad mappings)"
)]
configure_input_profile: Option<String>,
#[arg(
short,
long,
help = "Use DirectInput when configuring a new input profile"
)]
#[arg(long, help = "Use DirectInput when configuring a new input profile")]
use_dinput: bool,
#[arg(
short,
long,
value_name = "DEADZONE_PERCENTAGE",
help = "Used along with --configure-input-profile to set the deadzone for analog sticks"
)]
deadzone: Option<i32>,
#[arg(
short,
long,
value_name = "PROFILE_NAME",
help = "Must also specify --port. Used to bind a previously created profile to a port"
)]
bind_input_profile: Option<String>,
#[arg(
short,
long,
help = "Lists connected controllers which can be used in --assign-controller"
)]
list_controllers: bool,
#[arg(
short,
long,
value_name = "CONTROLLER_NUMBER",
help = "Must also specify --port. Used to assign a controller listed in --list-controllers to a port"
)]
assign_controller: Option<i32>,
#[arg(
short,
long,
value_name = "PORT",
help = "Valid values: 1-4. To be used alongside --bind-input-profile and --assign-controller"
)]
port: Option<usize>,
#[arg(
short = 'z',
long,
help = "Clear all input profile bindings and controller assignments"
)]
clear_input_bindings: bool,
#[arg(
long,
value_name = "SLOT",
help = "Load savestate from slot 0-9 when starting the game"
)]
load_state: Option<u32>,
#[arg(
long = "ra-username",
value_name = "USERNAME",
help = "Username for RetroAchievements"
)]
ra_username: Option<String>,
#[arg(
long = "ra-token",
value_name = "TOKEN",
help = "Token for RetroAchievements",
hide = true
)]
ra_token: Option<String>,
#[arg(
long = "ra-password",
value_name = "PASSWORD",
help = "Password for RetroAchievements"
)]
ra_password: Option<String>,
#[arg(
long = "ra-hardcore",
help = "Enable Hardcore mode for RetroAchievements"
)]
ra_hardcore: bool,
#[arg(
long = "ra-challenge",
help = "Enable Challenge Indicators for RetroAchievements"
)]
ra_challenge: bool,
#[arg(
long = "ra-leaderboard",
help = "Enable Leaderboard Trackers for RetroAchievements"
)]
ra_leaderboard: bool,
}
fn main() -> std::io::Result<()> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.worker_threads(4)
.thread_name("n64")
.thread_stack_size(env!("N64_STACK_SIZE").parse().unwrap())
.build()
.unwrap();
#[tokio::main(worker_threads = 4)]
async fn main() -> std::io::Result<()> {
let dirs = ui::get_dirs();
std::fs::create_dir_all(dirs.config_dir)?;
std::fs::create_dir_all(dirs.cache_dir)?;
std::fs::create_dir_all(dirs.data_dir.join("saves"))?;
std::fs::create_dir_all(dirs.data_dir.join("states"))?;
let args = Args::parse();
@@ -98,28 +131,122 @@ fn main() -> std::io::Result<()> {
)));
};
let handle = runtime.spawn(async 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;
if let Some(slot) = args.load_state
&& slot > 9
{
return Err(Error::other("Savestate slot must be between 0 and 9"));
}
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,
},
);
});
runtime.block_on(handle).unwrap()
let mut device = device::Device::new();
device.ui.config.recent_roms.retain(|x| *x != game);
device.ui.config.recent_roms.insert(0, game);
device.ui.config.recent_roms.truncate(5);
if args.fullscreen {
device.ui.video.fullscreen = true;
} else {
device.ui.video.fullscreen = device.ui.config.video.fullscreen;
}
let overclock = args
.overclock
.unwrap_or(device.ui.config.emulation.overclock);
let disable_expansion_pak = args
.disable_expansion_pak
.unwrap_or(device.ui.config.emulation.disable_expansion_pak);
let cheats = if let Some(cheats_file) = args.cheats
&& let Ok(data) = std::fs::read(cheats_file)
&& let Ok(cheats) = serde_json::from_slice(&data)
{
cheats
} else {
let game_crc = ui::storage::get_game_crc(&rom_contents);
ui::config::Cheats::new()
.cheats
.get(&game_crc)
.cloned()
.unwrap_or_default()
};
let mut shutdown_tx = None;
if let Some(peer_addr) = args.netplay_peer_addr
&& let Some(player_number) = args.netplay_player_number
{
device.netplay = Some(netplay::init(peer_addr.parse().unwrap(), player_number));
} else {
for i in 0..4 {
if device.ui.config.input.transfer_pak[i]
&& !device.ui.config.input.gb_rom_path[i].is_empty()
&& !device.ui.config.input.gb_ram_path[i].is_empty()
{
device.transferpaks[i].cart.rom =
std::fs::read(&device.ui.config.input.gb_rom_path[i]).unwrap();
device.transferpaks[i].cart.ram =
std::fs::read(&device.ui.config.input.gb_ram_path[i]).unwrap();
}
}
if device.ui.config.emulation.usb {
(shutdown_tx, device.ui.usb) = ui::usb::init();
}
if let Some(username) = args.ra_username {
retroachievements::init_client(
if cfg!(ra_hardcore_enabled) {
args.ra_hardcore
} else {
false
},
args.ra_challenge,
args.ra_leaderboard,
);
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
if let Some(password) = args.ra_password {
retroachievements::login_user(username, password, tx);
} else if let Some(token) = args.ra_token {
retroachievements::login_token_user(username, token, tx);
} else {
tx.send(false).unwrap();
}
rx.await.unwrap();
}
}
retroachievements::load_game(&rom_contents, rom_contents.len()).await;
device::run_game(
&mut device,
&rom_contents,
ui::GameSettings {
overclock,
disable_expansion_pak,
cheats,
load_savestate_slot: args.load_state,
},
);
retroachievements::shutdown_client();
if device.netplay.is_some() {
netplay::close(&mut device);
} else {
for i in 0..4 {
if device.ui.config.input.transfer_pak[i]
&& !device.ui.config.input.gb_ram_path[i].is_empty()
{
tokio::fs::write(
&device.ui.config.input.gb_ram_path[i],
&device.transferpaks[i].cart.ram,
)
.await
.unwrap();
}
}
}
if let Some(shutdown_tx) = &shutdown_tx {
ui::usb::close(shutdown_tx);
}
} else if std::env::args().count() > 1 {
let mut ui = ui::Ui::new();
@@ -161,10 +288,13 @@ fn main() -> std::io::Result<()> {
ui::input::bind_input_profile(&mut ui, profile, port);
}
} else {
runtime.block_on(async {
gui::app_window();
});
#[cfg(feature = "gui")]
{
retroachievements::init_client(false, false, false);
ui::gui::app_window();
retroachievements::shutdown_client();
}
}
runtime.shutdown_timeout(std::time::Duration::from_secs(1));
Ok(())
}
+15 -7
View File
@@ -85,7 +85,7 @@ pub fn send_save(netplay: &mut Netplay, save_type: &str, save_data: &[u8], size:
request.extend_from_slice(&(size as u32).to_be_bytes());
if size > 0 {
request.extend(save_data.to_owned());
request.extend_from_slice(save_data);
}
netplay.tcp_stream.write_all(&request).unwrap();
}
@@ -135,7 +135,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, &device.ui); // we execute process_incoming before request_input so that we send an accurate buffer count
process_incoming(netplay); // 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);
@@ -146,7 +146,10 @@ 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::video::onscreen_message(&device.ui, "Lost connection to netplay server");
ui::video::onscreen_message(
"Lost connection to netplay server",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
input = Some(InputEvent {
input: 0,
plugin: 0,
@@ -177,7 +180,7 @@ fn request_input(netplay: &Netplay, channel: usize) {
netplay.udp_socket.send(&request).unwrap();
}
fn process_incoming(netplay: &mut Netplay, ui: &ui::Ui) {
fn process_incoming(netplay: &mut Netplay) {
let mut buf: [u8; 1024] = [0; 1024];
while let Ok(_incoming) = netplay.udp_socket.recv(&mut buf) {
match buf[0] {
@@ -191,13 +194,18 @@ fn process_incoming(netplay: &mut Netplay, ui: &ui::Ui) {
}
if current_status != netplay.status {
if ((current_status & 0x1) ^ (netplay.status & 0x1)) != 0 {
ui::video::onscreen_message(ui, "Netplay desync detected");
ui::video::onscreen_message(ui, "Netplay desync detected"); // queue this message twice to ensure they see it
ui::video::onscreen_message(
"Netplay desync detected",
ui::video::MESSAGE_LENGTH_MESSAGE_LONG,
);
}
for dis in 1..5 {
if ((current_status & (0x1 << dis)) ^ (netplay.status & (0x1 << dis))) != 0
{
ui::video::onscreen_message(ui, &format!("Player {dis} disconnected"));
ui::video::onscreen_message(
&format!("Player {dis} disconnected"),
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
}
}
netplay.status = current_status;
+250
View File
@@ -0,0 +1,250 @@
include!(concat!(env!("OUT_DIR"), "/retroachievements_bindings.rs"));
use crate::ui;
static WEB_CLIENT: std::sync::LazyLock<reqwest::Client> = std::sync::LazyLock::new(|| {
reqwest::Client::builder()
.user_agent(format!(
"{}/{}",
env!("CARGO_PKG_NAME"),
env!("GIT_DESCRIBE")
))
.build()
.unwrap()
});
#[derive(serde::Serialize, serde::Deserialize)]
pub struct RAConfig {
pub username: String,
pub token: String,
pub enabled: bool,
pub hardcore: bool,
pub challenge: bool,
pub leaderboard: bool,
}
#[unsafe(no_mangle)]
pub extern "C" fn notify_load_game(ctx: *mut std::ffi::c_void) {
let tx = unsafe { Box::from_raw(ctx as *mut tokio::sync::oneshot::Sender<bool>) };
tx.send(true).unwrap();
}
#[unsafe(no_mangle)]
pub extern "C" fn store_retroachievements_credentials(
c_username: *const std::ffi::c_char,
c_token: *const std::ffi::c_char,
ctx: *mut std::ffi::c_void,
) {
let tx = unsafe { Box::from_raw(ctx as *mut tokio::sync::oneshot::Sender<bool>) };
if c_username.is_null() || c_token.is_null() {
tx.send(false).unwrap();
return;
}
let file_path = ui::get_dirs().config_dir.join("retroachievements.json");
let raconfig = if let Ok(ra_config) = std::fs::read(&file_path)
&& let Ok(result) = serde_json::from_slice::<RAConfig>(ra_config.as_ref())
{
RAConfig {
username: unsafe { std::ffi::CStr::from_ptr(c_username).to_str().unwrap() }.to_string(),
token: unsafe { std::ffi::CStr::from_ptr(c_token).to_str().unwrap() }.to_string(),
enabled: result.enabled,
hardcore: result.hardcore,
challenge: result.challenge,
leaderboard: result.leaderboard,
}
} else {
RAConfig {
username: unsafe { std::ffi::CStr::from_ptr(c_username).to_str().unwrap() }.to_string(),
token: unsafe { std::ffi::CStr::from_ptr(c_token).to_str().unwrap() }.to_string(),
enabled: false,
hardcore: false,
challenge: false,
leaderboard: false,
}
};
let f = std::fs::File::create(&file_path).unwrap();
serde_json::to_writer_pretty(f, &raconfig).unwrap();
tx.send(true).unwrap();
}
#[unsafe(no_mangle)]
pub extern "C" fn rust_server_call(
c_url: *const std::ffi::c_char,
c_post_data: *const std::ffi::c_char,
c_content_type: *const std::ffi::c_char,
c_callback: *mut std::ffi::c_void,
c_callback_data: *mut std::ffi::c_void,
) {
let url = unsafe { std::ffi::CStr::from_ptr(c_url).to_str().unwrap() }.to_string();
let task = if !c_post_data.is_null() {
let post_data =
unsafe { std::ffi::CStr::from_ptr(c_post_data).to_str().unwrap() }.to_string();
let content_type =
unsafe { std::ffi::CStr::from_ptr(c_content_type).to_str().unwrap() }.to_string();
WEB_CLIENT
.post(url)
.body(post_data)
.header(reqwest::header::CONTENT_TYPE, content_type)
.send()
} else {
WEB_CLIENT.get(url).send()
};
let callback = c_callback.addr();
let callback_data = c_callback_data.addr();
tokio::spawn(async move {
let response = task.await;
match response {
Ok(response) => {
let status = response.status().as_u16() as i32;
match response.text().await {
Ok(text) => {
let c_text = std::ffi::CString::new(text).unwrap();
unsafe {
ra_http_callback(
c_text.as_ptr(),
c_text.count_bytes(),
status,
callback as *mut std::ffi::c_void,
callback_data as *mut std::ffi::c_void,
)
};
}
Err(error) => {
let c_error = std::ffi::CString::new(error.to_string()).unwrap();
unsafe {
ra_http_callback(
c_error.as_ptr(),
c_error.count_bytes(),
status,
callback as *mut std::ffi::c_void,
callback_data as *mut std::ffi::c_void,
)
};
}
}
}
Err(error) => {
let c_error = std::ffi::CString::new(error.to_string()).unwrap();
unsafe {
ra_http_callback(
c_error.as_ptr(),
c_error.count_bytes(),
0,
callback as *mut std::ffi::c_void,
callback_data as *mut std::ffi::c_void,
)
};
}
}
});
}
pub async fn load_game(rom: &[u8], rom_size: usize) {
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
unsafe {
let tx_ptr = Box::into_raw(Box::new(tx)) as *mut std::ffi::c_void;
ra_load_game(rom.as_ptr(), rom_size, tx_ptr);
};
rx.await.unwrap();
}
pub fn unload_game() {
unsafe { ra_unload_game() };
}
pub fn welcome() {
unsafe { ra_welcome() };
}
pub fn set_dmem(dmem: *const u8, dmem_size: usize) {
unsafe { ra_set_dmem(dmem, dmem_size) };
}
pub fn do_frame() {
unsafe { ra_do_frame() };
}
pub fn do_idle() {
unsafe { ra_do_idle() };
}
pub fn init_client(hardcore: bool, challenge: bool, leaderboard: bool) {
unsafe { ra_init_client(hardcore, challenge, leaderboard) };
}
pub fn shutdown_client() {
unsafe { ra_shutdown_client() };
}
pub fn get_hardcore() -> bool {
unsafe { ra_get_hardcore() }
}
pub fn login_user(username: String, password: String, tx: tokio::sync::oneshot::Sender<bool>) {
unsafe {
let tx_ptr = Box::into_raw(Box::new(tx)) as *mut std::ffi::c_void;
let c_username = std::ffi::CString::new(username).unwrap();
let c_password = std::ffi::CString::new(password).unwrap();
ra_login_user(c_username.as_ptr(), c_password.as_ptr(), tx_ptr)
};
}
#[cfg(feature = "gui")]
pub fn logout_user() {
unsafe { ra_logout_user() };
}
pub fn login_token_user(username: String, token: String, tx: tokio::sync::oneshot::Sender<bool>) {
unsafe {
let tx_ptr = Box::into_raw(Box::new(tx)) as *mut std::ffi::c_void;
let c_username = std::ffi::CString::new(username).unwrap();
let c_token = std::ffi::CString::new(token).unwrap();
ra_login_token_user(c_username.as_ptr(), c_token.as_ptr(), tx_ptr)
};
}
#[cfg(feature = "gui")]
pub fn get_username() -> Option<String> {
let c_username = unsafe { ra_get_username() };
if c_username.is_null() {
None
} else {
Some(
unsafe { std::ffi::CStr::from_ptr(c_username) }
.to_str()
.unwrap()
.to_string(),
)
}
}
#[cfg(feature = "gui")]
pub fn get_token() -> Option<String> {
let c_token = unsafe { ra_get_token() };
if c_token.is_null() {
None
} else {
Some(
unsafe { std::ffi::CStr::from_ptr(c_token) }
.to_str()
.unwrap()
.to_string(),
)
}
}
pub fn state_size() -> usize {
unsafe { ra_state_size() }
}
pub fn save_state(state: *mut u8, state_size: usize) {
unsafe { ra_save_state(state, state_size) };
}
pub fn load_state(state: *const u8, state_size: usize) {
unsafe { ra_load_state(state, state_size) };
}
+44 -20
View File
@@ -1,4 +1,4 @@
use crate::{device, ui};
use crate::{cheats, device, retroachievements, ui};
#[cfg(target_arch = "aarch64")]
use device::__m128i;
use serde::de::{Deserialize, Deserializer, SeqAccess, Visitor};
@@ -20,7 +20,7 @@ impl<'de, const N: usize> Visitor<'de> for M128iArrayVisitor<N> {
A: SeqAccess<'de>,
{
let mut arr: [__m128i; N] = [device::zero_m128i(); N];
for (index, item) in arr.iter_mut().enumerate().take(N) {
for (index, item) in arr.iter_mut().enumerate() {
match seq.next_element::<u128>()? {
Some(value) => *item = unsafe { std::mem::transmute::<u128, __m128i>(value) },
None => return Err(serde::de::Error::invalid_length(index, &self)),
@@ -73,6 +73,9 @@ pub fn create_savestate(device: &device::Device) {
let mut rdp_state: Vec<u8> = vec![0; ui::video::state_size()];
ui::video::save_state(rdp_state.as_mut_ptr());
let mut ra_state: Vec<u8> = vec![0; retroachievements::state_size()];
retroachievements::save_state(ra_state.as_mut_ptr(), ra_state.len());
let data: &[(&[u8], &str)] = &[
(&postcard::to_stdvec(device).unwrap(), "device"),
(
@@ -80,28 +83,36 @@ pub fn create_savestate(device: &device::Device) {
"saves",
),
(&rdp_state, "rdp_state"),
(&ra_state, "ra_state"),
];
let save_path = device.ui.storage.paths.savestate_file_path.clone();
let compressed_file = ui::storage::compress_file(data);
std::fs::write(
device.ui.storage.paths.savestate_file_path.clone(),
compressed_file,
)
.unwrap();
tokio::spawn(async move {
tokio::fs::write(save_path, compressed_file).await.unwrap();
});
ui::video::onscreen_message(
&device.ui,
&format!(
"Savestate created in slot {}",
device.ui.storage.save_state_slot
),
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
}
pub fn load_savestate(device: &mut device::Device) {
if retroachievements::get_hardcore() {
ui::video::onscreen_message(
"Cannot load savestate in RA hardcore mode",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
return;
}
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");
let ra_state = ui::storage::decompress_file(savestate, "ra_state");
if let Ok(state) = postcard::from_bytes::<device::Device>(&device_bytes) {
device.ui.storage.saves = postcard::from_bytes(&save_bytes).unwrap();
@@ -163,28 +174,22 @@ pub fn load_savestate(device: &mut device::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
{
if let Some(handler) = device.pif.channels[i].pak_handler {
if handler.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
{
} else if handler.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
{
} else if handler.pak_type == device::controller::PakType::TransferPak {
let tpak_handler = device::controller::PakHandler {
read: device::controller::transferpak::read,
write: device::controller::transferpak::write,
@@ -198,22 +203,41 @@ pub fn load_savestate(device: &mut device::Device) {
ui::audio::close_game_audio(&mut device.ui);
ui::audio::init_game_audio(&mut device.ui, device.ai.freq);
ui::video::load_state(device, rdp_state.as_ptr());
if !ra_state.is_empty() {
retroachievements::load_state(ra_state.as_ptr(), ra_state.len());
} else {
retroachievements::load_state(std::ptr::null(), 0);
}
if device.cheats.enabled {
cheats::execute_cheats(device, device.cheats.cheats.clone());
}
ui::video::onscreen_message(
&device.ui,
&format!(
"Savestate loaded from slot {}",
device.ui.storage.save_state_slot
),
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
} else {
ui::video::onscreen_message(
&device.ui,
&format!(
"Failed to load savestate from slot {}",
device.ui.storage.save_state_slot
),
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
}
} else {
ui::video::onscreen_message(
&format!(
"Could not find savestate in slot {}",
device.ui.storage.save_state_slot
),
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
}
}
+35 -13
View File
@@ -1,17 +1,24 @@
pub mod audio;
#[cfg(feature = "gui")]
pub mod cheats;
pub mod config;
#[cfg(feature = "gui")]
pub mod gui;
pub mod input;
#[cfg(feature = "gui")]
pub mod netplay;
#[cfg(feature = "gui")]
pub mod retroachievements;
pub mod storage;
pub mod usb;
pub mod video;
#[cfg(feature = "gui")]
pub mod vru;
pub struct Dirs {
pub config_dir: std::path::PathBuf,
pub data_dir: std::path::PathBuf,
pub cache_dir: std::path::PathBuf,
}
pub struct Audio {
@@ -35,6 +42,10 @@ pub struct Storage {
pub struct Video {
pub window: *mut sdl3_sys::video::SDL_Window,
pub fullscreen: bool,
pub fps_tx: tokio::sync::mpsc::Sender<bool>,
pub fps_rx: Option<tokio::sync::mpsc::Receiver<bool>>,
pub vis_tx: tokio::sync::mpsc::Sender<bool>,
pub vis_rx: Option<tokio::sync::mpsc::Receiver<bool>>,
}
pub struct Usb {
@@ -42,6 +53,14 @@ pub struct Usb {
pub cart_rx: Option<tokio::sync::broadcast::Receiver<usb::UsbData>>,
}
#[derive(Clone)]
pub struct GameSettings {
pub overclock: bool,
pub disable_expansion_pak: bool,
pub cheats: std::collections::HashMap<String, Option<String>>,
pub load_savestate_slot: Option<u32>,
}
pub struct Ui {
pub dirs: Dirs,
pub config: config::Config,
@@ -53,7 +72,6 @@ pub struct Ui {
pub storage: Storage,
pub video: Video,
pub usb: Usb,
pub weak: Option<slint::Weak<gui::AppWindow>>,
}
impl Drop for Ui {
@@ -94,19 +112,18 @@ pub fn get_dirs() -> Dirs {
let exe_path = std::env::current_exe().unwrap();
let portable_dir = exe_path.parent();
let portable = portable_dir.unwrap().join("portable.txt").exists();
let config_dir;
let data_dir;
if portable {
config_dir = portable_dir.unwrap().join("portable_data").join("config");
data_dir = portable_dir.unwrap().join("portable_data").join("data");
Dirs {
config_dir: portable_dir.unwrap().join("portable_data").join("config"),
data_dir: portable_dir.unwrap().join("portable_data").join("data"),
cache_dir: portable_dir.unwrap().join("portable_data").join("cache"),
}
} else {
config_dir = dirs::config_dir().unwrap().join("gopher64");
data_dir = dirs::data_dir().unwrap().join("gopher64");
};
Dirs {
config_dir,
data_dir,
Dirs {
config_dir: dirs::config_dir().unwrap().join("gopher64"),
data_dir: dirs::data_dir().unwrap().join("gopher64"),
cache_dir: dirs::cache_dir().unwrap().join("gopher64"),
}
}
}
@@ -114,6 +131,8 @@ impl Ui {
fn construct_ui(joysticks: Vec<sdl3_sys::everything::SDL_JoystickID>, with_sdl: bool) -> Ui {
let dirs = get_dirs();
let (fps_tx, fps_rx) = tokio::sync::mpsc::channel(1000);
let (vis_tx, vis_rx) = tokio::sync::mpsc::channel(1000);
Ui {
input: Input {
controllers: [
@@ -191,12 +210,15 @@ impl Ui {
video: Video {
window: std::ptr::null_mut(),
fullscreen: false,
fps_tx,
fps_rx: Some(fps_rx),
vis_tx,
vis_rx: Some(vis_rx),
},
usb: Usb {
usb_tx: None,
cart_rx: None,
},
weak: None,
dirs,
with_sdl,
}
+8 -2
View File
@@ -53,7 +53,10 @@ pub fn lower_audio_volume(ui: &mut ui::Ui) {
}
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain);
}
ui::video::onscreen_message(ui, &format!("Audio volume: {:.0}%", ui.audio.gain * 100.0));
ui::video::onscreen_message(
&format!("Audio volume: {:.0}%", ui.audio.gain * 100.0),
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
);
}
pub fn raise_audio_volume(ui: &mut ui::Ui) {
@@ -64,7 +67,10 @@ pub fn raise_audio_volume(ui: &mut ui::Ui) {
}
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain);
}
ui::video::onscreen_message(ui, &format!("Audio volume: {:.0}%", ui.audio.gain * 100.0));
ui::video::onscreen_message(
&format!("Audio volume: {:.0}%", ui.audio.gain * 100.0),
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
);
}
fn adjust_audio_frequency(device: &device::Device, frequency: f32) {
+7 -32
View File
@@ -1,18 +1,10 @@
use crate::cheats;
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 |rom_dir| {
@@ -38,7 +30,7 @@ pub fn cheats_window(app: &AppWindow) {
handle.set_cheat_game_name(game_name.into());
})
.unwrap();
let cheats: Cheats =
let cheats: cheats::Cheat =
serde_json::from_slice(include_bytes!("../../data/cheats.json")).unwrap();
if let Some(cheat) = cheats.get(&game_crc) {
let cheat = cheat.clone();
@@ -77,16 +69,7 @@ pub fn cheats_window(app: &AppWindow) {
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));
handle.set_cheats(slint::ModelRc::from(std::rc::Rc::new(cheats_vec)));
})
.unwrap();
} else {
@@ -137,19 +120,11 @@ pub fn cheats_window(app: &AppWindow) {
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));
handle.set_cheats(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::default(),
)));
if clear_name {
handle.set_cheat_game_name("".into());
handle.set_cheat_game_name(String::new().into());
}
})
.unwrap();
+8
View File
@@ -39,6 +39,8 @@ pub struct Input {
pub controller_assignment: [Option<String>; 4],
pub controller_enabled: [bool; 4],
pub transfer_pak: [bool; 4],
pub gb_rom_path: [String; 4],
pub gb_ram_path: [String; 4],
pub emulate_vru: bool,
}
@@ -48,6 +50,7 @@ pub struct Video {
pub integer_scaling: bool,
pub fullscreen: bool,
pub widescreen: bool,
pub vsync: bool,
pub crt: bool,
}
@@ -70,6 +73,7 @@ pub struct Config {
pub video: Video,
pub emulation: Emulation,
pub rom_dir: std::path::PathBuf,
pub recent_roms: Vec<String>,
}
impl Drop for Cheats {
@@ -140,6 +144,8 @@ impl Config {
input_profiles,
controller_enabled: [true, false, false, false],
transfer_pak: [false, false, false, false],
gb_rom_path: [String::new(), String::new(), String::new(), String::new()],
gb_ram_path: [String::new(), String::new(), String::new(), String::new()],
emulate_vru: false,
},
video: Video {
@@ -147,6 +153,7 @@ impl Config {
integer_scaling: false,
fullscreen: false,
widescreen: false,
vsync: true,
crt: false,
},
emulation: Emulation {
@@ -155,6 +162,7 @@ impl Config {
usb: false,
},
rom_dir: std::path::PathBuf::new(),
recent_roms: Vec::new(),
}
}
}
+335 -173
View File
@@ -1,7 +1,7 @@
use crate::device;
use crate::netplay;
use crate::retroachievements;
use crate::ui;
use slint::Model;
use slint::winit_030::WinitWindowAccessor;
slint::include_modules!();
@@ -19,17 +19,12 @@ pub struct NetplayDevice {
pub player_number: u8,
}
pub struct GbPaths {
pub rom: [Option<std::path::PathBuf>; 4],
pub ram: [Option<std::path::PathBuf>; 4],
}
#[derive(Clone)]
pub struct GameSettings {
pub fullscreen: bool,
pub overclock: bool,
pub disable_expansion_pak: bool,
pub cheats: std::collections::HashMap<String, Option<String>>,
pub struct RASettings {
pub enabled: bool,
pub hardcore: bool,
pub challenge: bool,
pub leaderboard: bool,
}
fn check_latest_version(weak: slint::Weak<AppWindow>) {
@@ -59,23 +54,109 @@ fn check_latest_version(weak: slint::Weak<AppWindow>) {
});
}
fn local_game_window(app: &AppWindow, controller_paths: &[Option<String>], usb: ui::Usb) {
let dirs = ui::get_dirs();
fn run_with_path(
weak: slint::Weak<AppWindow>,
path: std::path::PathBuf,
controller_paths: &[Option<String>],
) {
let controller_paths = controller_paths.to_owned();
let weak2 = weak.clone();
weak.upgrade_in_event_loop(move |handle| {
if handle.get_game_running() {
return;
}
save_settings(&handle, &controller_paths);
run_rom(
path,
ui::GameSettings {
overclock: handle.get_overclock_n64_cpu(),
disable_expansion_pak: handle.get_disable_expansion_pak(),
cheats: std::collections::HashMap::new(), // will be filled in later
load_savestate_slot: None,
},
None,
RASettings {
enabled: handle.get_ra_enabled(),
hardcore: handle.get_ra_hardcore(),
challenge: handle.get_ra_challenge(),
leaderboard: handle.get_ra_leaderboard(),
},
weak2,
);
})
.unwrap();
}
fn file_dropped(app: &AppWindow, controller_paths: &[Option<String>]) {
let weak = app.as_weak();
let controller_paths = controller_paths.to_owned();
app.window()
.on_winit_window_event(move |_winit_window, event| {
if let slint::winit_030::winit::event::WindowEvent::DroppedFile(path) = event {
run_with_path(weak.clone(), path.to_path_buf(), &controller_paths);
}
slint::winit_030::EventResult::Propagate
});
}
fn local_game_window(
app: &AppWindow,
config: &ui::config::Config,
controller_paths: &[Option<String>],
) {
let dirs = ui::get_dirs();
app.set_recent_roms(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(
config
.recent_roms
.iter()
.filter(|x| {
if let Ok(exists) = std::fs::exists(x)
&& exists
{
true
} else {
false
}
})
.map(|x| {
(
x.into(),
std::path::Path::new(x)
.file_name()
.unwrap()
.to_str()
.unwrap()
.into(),
)
})
.collect::<Vec<(slint::SharedString, slint::SharedString)>>(),
),
)));
let weak = app.as_weak();
let owned_controller_paths = controller_paths.to_owned();
app.on_open_rom_button_clicked(move || {
let controller_paths = controller_paths.clone();
let mut usb_tx = None;
if let Some(usb_tx_inner) = usb.usb_tx.as_ref() {
usb_tx = Some(usb_tx_inner.clone());
}
let mut cart_rx = None;
if let Some(cart_rx_inner) = usb.cart_rx.as_ref() {
cart_rx = Some(cart_rx_inner.resubscribe());
}
let controller_paths = owned_controller_paths.clone();
weak.upgrade_in_event_loop(move |handle| {
save_settings(&handle, &controller_paths);
open_rom(&handle, ui::Usb { usb_tx, cart_rx })
open_rom(&handle)
})
.unwrap();
});
let weak = app.as_weak();
let owned_controller_paths = controller_paths.to_owned();
app.on_recent_rom_button_clicked(move |rom| {
let controller_paths = owned_controller_paths.clone();
weak.upgrade_in_event_loop(move |handle| {
run_with_path(
handle.as_weak(),
std::path::PathBuf::from(rom.to_string()),
&controller_paths,
);
})
.unwrap();
});
@@ -84,6 +165,7 @@ fn local_game_window(app: &AppWindow, controller_paths: &[Option<String>], usb:
app.on_saves_folder_button_clicked(move || {
open::that_detached(saves_path.clone()).unwrap();
});
file_dropped(app, controller_paths);
}
fn input_profiles(config: &ui::config::Config) -> Vec<String> {
@@ -98,6 +180,7 @@ fn settings_window(app: &AppWindow, config: &ui::config::Config) {
app.set_integer_scaling(config.video.integer_scaling);
app.set_fullscreen(config.video.fullscreen);
app.set_widescreen(config.video.widescreen);
app.set_vsync(config.video.vsync);
app.set_apply_crt_shader(config.video.crt);
app.set_overclock_n64_cpu(config.emulation.overclock);
app.set_disable_expansion_pak(config.emulation.disable_expansion_pak);
@@ -134,17 +217,17 @@ fn update_input_profiles(weak: &slint::Weak<AppWindow>, config: &ui::config::Con
profile_bindings.push(position.unwrap() as i32);
}
let input_profiles = slint::VecModel::default();
for profile in profiles {
input_profiles.push(profile.into());
}
let input_profiles_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
std::rc::Rc::new(input_profiles);
handle.set_input_profiles(slint::ModelRc::from(input_profiles_model));
handle.set_input_profiles(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(
profiles
.iter()
.map(|x| x.into())
.collect::<Vec<slint::SharedString>>(),
),
)));
let input_profile_binding_model: std::rc::Rc<slint::VecModel<i32>> =
std::rc::Rc::new(profile_bindings);
handle.set_selected_profile_binding(slint::ModelRc::from(input_profile_binding_model));
handle
.set_selected_profile_binding(slint::ModelRc::from(std::rc::Rc::new(profile_bindings)));
// this is a workaround to make the input profile combobox update
handle.set_blank_profiles(true);
@@ -162,53 +245,65 @@ fn update_input_profiles(weak: &slint::Weak<AppWindow>, config: &ui::config::Con
fn controller_window(
app: &AppWindow,
config: &ui::config::Config,
controller_names: &Vec<String>,
controller_names: &[String],
controller_paths: &[Option<String>],
) {
let controller_enabled_model: std::rc::Rc<slint::VecModel<bool>> = std::rc::Rc::new(
slint::VecModel::from(config.input.controller_enabled.to_vec()),
);
app.set_emulate_vru(config.input.emulate_vru);
app.set_controller_enabled(slint::ModelRc::from(controller_enabled_model));
app.set_controller_enabled(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(config.input.controller_enabled.to_vec()),
)));
let transferpak_enabled_model: std::rc::Rc<slint::VecModel<bool>> =
std::rc::Rc::new(slint::VecModel::from(config.input.transfer_pak.to_vec()));
app.set_transferpak(slint::ModelRc::from(transferpak_enabled_model));
app.set_transferpak(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(config.input.transfer_pak.to_vec()),
)));
app.set_gb_rom_paths(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(
config
.input
.gb_rom_path
.iter()
.map(|x| x.into())
.collect::<Vec<slint::SharedString>>(),
),
)));
app.set_gb_ram_paths(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(
config
.input
.gb_ram_path
.iter()
.map(|x| x.into())
.collect::<Vec<slint::SharedString>>(),
),
)));
update_input_profiles(&app.as_weak(), config);
let controllers = slint::VecModel::default();
for controller in controller_names {
controllers.push(controller.into());
}
let controller_names_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
std::rc::Rc::new(controllers);
app.set_controller_names(slint::ModelRc::from(controller_names_model));
app.set_controller_names(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(
controller_names
.iter()
.map(|x| x.into())
.collect::<Vec<slint::SharedString>>(),
),
)));
let controller_changed = slint::VecModel::default();
let selected_controllers = slint::VecModel::default();
for selected in config.input.controller_assignment.iter() {
let mut found = false;
for (i, path) in controller_paths.iter().enumerate() {
if selected == path {
selected_controllers.push(i as i32);
found = true;
continue;
}
}
if !found {
selected_controllers.push(0);
}
let selected_index = controller_paths
.iter()
.position(|path| selected == path)
.unwrap_or(0) as i32;
selected_controllers.push(selected_index);
controller_changed.push(false);
}
let selected_controllers_model: std::rc::Rc<slint::VecModel<i32>> =
std::rc::Rc::new(selected_controllers);
app.set_selected_controller(slint::ModelRc::from(selected_controllers_model));
app.set_selected_controller(slint::ModelRc::from(std::rc::Rc::new(selected_controllers)));
let controller_changed_model: std::rc::Rc<slint::VecModel<bool>> =
std::rc::Rc::new(controller_changed);
app.set_controller_changed(slint::ModelRc::from(controller_changed_model));
app.set_controller_changed(slint::ModelRc::from(std::rc::Rc::new(controller_changed)));
let weak_app = app.as_weak();
app.on_input_profile_button_clicked(move || {
@@ -221,25 +316,87 @@ fn controller_window(
weak_dialog
.upgrade_in_event_loop(move |handle| {
handle.hide().unwrap();
let profile_name = handle.get_profile_name().into();
let profile_name = handle.get_profile_name();
let dinput = handle.get_dinput();
let deadzone = handle.get_deadzone();
tokio::spawn(async move {
let mut game_ui = ui::Ui::new();
ui::input::configure_input_profile(
&mut game_ui,
profile_name,
dinput,
deadzone,
);
update_input_profiles(&weak_app, &game_ui.config);
let cli_path = std::env::current_exe()
.unwrap()
.parent()
.unwrap()
.join(format!("{}-cli", env!("CARGO_PKG_NAME")));
let cmd_path = if cfg!(target_os = "macos") && cli_path.exists() {
cli_path
} else {
std::env::current_exe().unwrap()
};
let mut command = tokio::process::Command::new(cmd_path);
command.args([
"--configure-input-profile",
&profile_name,
"--deadzone",
&deadzone.to_string(),
]);
if dinput {
command.arg("--use-dinput");
}
if !command.status().await.unwrap().success() {
eprintln!("Failed to configure input profile");
}
let config = ui::config::Config::new();
update_input_profiles(&weak_app, &config);
});
})
.unwrap();
});
dialog.show().unwrap();
});
let weak_app2 = app.as_weak();
app.on_transferpak_toggled(move |player, enabled| {
if enabled {
let select_gb_rom = rfd::AsyncFileDialog::new()
.set_title(format!("GB ROM P{}", player + 1))
.add_filter("GB ROM files", &["gb", "gbc", "GB", "GBC"])
.pick_file();
let select_gb_ram = rfd::AsyncFileDialog::new()
.set_title(format!("GB RAM P{}", player + 1))
.add_filter("GB RAM files", &["sav", "ram", "srm", "SAV", "RAM", "SRM"])
.pick_file();
let weak_app3 = weak_app2.clone();
tokio::spawn(async move {
if let (Some(gb_rom), Some(gb_ram)) = (select_gb_rom.await, select_gb_ram.await) {
weak_app3
.upgrade_in_event_loop(move |handle| {
let rom_paths = handle.get_gb_rom_paths();
let ram_paths = handle.get_gb_ram_paths();
rom_paths.set_row_data(
player as usize,
gb_rom.path().to_str().unwrap().into(),
);
ram_paths.set_row_data(
player as usize,
gb_ram.path().to_str().unwrap().into(),
);
handle.set_gb_rom_paths(rom_paths);
handle.set_gb_ram_paths(ram_paths);
})
.unwrap();
} else {
weak_app3
.upgrade_in_event_loop(move |handle| {
let rom_paths = handle.get_gb_rom_paths();
let ram_paths = handle.get_gb_ram_paths();
rom_paths.set_row_data(player as usize, String::new().into());
ram_paths.set_row_data(player as usize, String::new().into());
handle.set_gb_rom_paths(rom_paths);
handle.set_gb_ram_paths(ram_paths);
})
.unwrap();
}
});
}
});
}
pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
@@ -247,6 +404,7 @@ pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
config.video.integer_scaling = app.get_integer_scaling();
config.video.fullscreen = app.get_fullscreen();
config.video.widescreen = app.get_widescreen();
config.video.vsync = app.get_vsync();
config.video.crt = app.get_apply_crt_shader();
config.emulation.overclock = app.get_overclock_n64_cpu();
config.emulation.disable_expansion_pak = app.get_disable_expansion_pak();
@@ -260,6 +418,8 @@ pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
}
for (i, transferpak_enabled) in app.get_transferpak().iter().enumerate() {
config.input.transfer_pak[i] = transferpak_enabled;
config.input.gb_rom_path[i] = app.get_gb_rom_paths().row_data(i).unwrap().to_string();
config.input.gb_ram_path[i] = app.get_gb_ram_paths().row_data(i).unwrap().to_string();
}
for (i, input_profile_binding) in app.get_selected_profile_binding().iter().enumerate() {
config.input.input_profile_binding[i] = app
@@ -296,18 +456,16 @@ fn about_window(app: &AppWindow) {
app.on_newversion_button_clicked(move || {
open::that_detached("https://github.com/gopher64/gopher64/releases/latest").unwrap();
});
app.set_version(format!("Version: {}", env!("CARGO_PKG_VERSION")).into());
check_latest_version(app.as_weak());
app.set_version(format!("Version: {}", env!("GIT_DESCRIBE")).into());
if std::env::var("FLATPAK_ID").is_err() {
check_latest_version(app.as_weak());
}
}
pub fn app_window() {
let app = AppWindow::new().unwrap();
about_window(&app);
let mut usb = ui::Usb {
usb_tx: None,
cart_rx: None,
};
let mut shutdown_tx = None;
ui::retroachievements::ra_window(&app);
let mut controller_paths;
{
let game_ui = ui::Ui::new();
@@ -317,76 +475,108 @@ pub fn app_window() {
controller_paths.insert(0, None);
settings_window(&app, &game_ui.config);
controller_window(&app, &game_ui.config, &controller_names, &controller_paths);
if game_ui.config.emulation.usb {
(shutdown_tx, usb) = ui::usb::init(app.as_weak());
}
local_game_window(&app, &game_ui.config, &controller_paths);
}
local_game_window(&app, &controller_paths, usb);
ui::netplay::netplay_window(&app, &controller_paths);
ui::cheats::cheats_window(&app);
app.run().unwrap();
if let Some(shutdown_tx) = &shutdown_tx {
ui::usb::close(shutdown_tx);
}
save_settings(&app, &controller_paths);
}
pub fn run_rom(
gb_paths: GbPaths,
file_path: std::path::PathBuf,
mut game_settings: GameSettings,
game_settings: ui::GameSettings,
netplay: Option<NetplayDevice>,
usb: ui::Usb,
ra_settings: RASettings,
weak: slint::Weak<AppWindow>,
) {
tokio::spawn(async move {
weak.upgrade_in_event_loop(move |handle| handle.set_game_running(true))
.unwrap();
let mut device = device::Device::new();
device.ui.config.rom_dir = file_path.parent().unwrap().to_path_buf();
for i in 0..4 {
if gb_paths.rom[i].is_some() && gb_paths.ram[i].is_some() {
device.transferpaks[i].cart.rom =
std::fs::read(gb_paths.rom[i].as_ref().unwrap()).unwrap();
device.transferpaks[i].cart.ram =
std::fs::read(gb_paths.ram[i].as_ref().unwrap()).unwrap();
}
}
device.ui.usb.usb_tx = usb.usb_tx;
device.ui.usb.cart_rx = usb.cart_rx;
device.ui.weak = Some(weak.clone());
if let Some(rom_contents) = device::get_rom_contents(file_path.as_path()) {
if let Some(netplay_device) = netplay {
device.netplay = Some(netplay::init(
netplay_device.peer_addr,
netplay_device.player_number,
));
} else {
game_settings.cheats = {
let game_crc = ui::storage::get_game_crc(&rom_contents);
let cheats = ui::config::Cheats::new();
cheats.cheats.get(&game_crc).cloned().unwrap_or_default()
};
}
device::run_game(&mut device, rom_contents, game_settings);
if device.netplay.is_some() {
netplay::close(&mut device);
}
let cli_path = std::env::current_exe()
.unwrap()
.parent()
.unwrap()
.join(format!("{}-cli", env!("CARGO_PKG_NAME")));
let cmd_path = if cfg!(target_os = "macos") && cli_path.exists() {
cli_path
} else {
println!("Could not read rom file");
std::env::current_exe().unwrap()
};
let mut command = tokio::process::Command::new(cmd_path);
command.args([
"--overclock",
&game_settings.overclock.to_string(),
"--disable-expansion-pak",
&game_settings.disable_expansion_pak.to_string(),
]);
let cheats_path = ui::get_dirs().cache_dir.join("cheats.json");
if let Some(netplay_device) = netplay {
let f = std::fs::File::create(cheats_path.to_str().unwrap()).unwrap();
serde_json::to_writer_pretty(f, &game_settings.cheats).unwrap();
command.args([
"--netplay-peer-addr",
&netplay_device.peer_addr.to_string(),
"--netplay-player-number",
&netplay_device.player_number.to_string(),
"--cheats",
cheats_path.to_str().unwrap(),
]);
}
if ra_settings.enabled {
command.args([
"--ra-username",
&retroachievements::get_username().unwrap_or("unknown".into()),
"--ra-token",
&retroachievements::get_token().unwrap_or("unknown".into()),
]);
if ra_settings.hardcore {
command.args(["--ra-hardcore"]);
}
if ra_settings.challenge {
command.args(["--ra-challenge"]);
}
if ra_settings.leaderboard {
command.args(["--ra-leaderboard"]);
}
}
let rom_dir = device.ui.config.rom_dir.clone();
let success = command
.arg(file_path.to_str().unwrap())
.status()
.await
.unwrap()
.success();
if !success {
eprintln!("Failed to run game");
}
let _ = std::fs::remove_file(cheats_path.to_str().unwrap());
weak.upgrade_in_event_loop(move |handle| {
if let Some(rom_dir_str) = rom_dir.to_str() {
handle.set_rom_dir(rom_dir_str.into());
if let Some(rom_dir) = file_path.parent().unwrap().to_str() {
handle.set_rom_dir(rom_dir.into());
}
if success {
let recent_roms = slint::VecModel::default();
recent_roms.push((
file_path.to_str().unwrap().into(),
file_path.file_name().unwrap().to_str().unwrap().into(),
));
for rom in handle.get_recent_roms().iter() {
if rom.0 != file_path.to_str().unwrap()
&& recent_roms.row_count() < 5
&& let Ok(exists) = std::fs::exists(&rom.0)
&& exists
{
recent_roms.push(rom);
}
}
handle.set_recent_roms(slint::ModelRc::from(std::rc::Rc::new(recent_roms)));
}
handle.set_game_running(false);
})
@@ -394,7 +584,7 @@ pub fn run_rom(
});
}
fn open_rom(app: &AppWindow, usb: ui::Usb) {
fn open_rom(app: &AppWindow) {
let rom_dir = app.get_rom_dir();
let select_rom = if !rom_dir.is_empty()
&& let Ok(exists) = std::fs::exists(&rom_dir)
@@ -407,60 +597,32 @@ fn open_rom(app: &AppWindow, usb: ui::Usb) {
.set_title("Select ROM")
.add_filter("ROM files", &N64_EXTENSIONS)
.pick_file();
let mut select_gb_rom = [None, None, None, None];
let mut select_gb_ram = [None, None, None, None];
for (i, transfer_pak_enabled) in app.get_transferpak().iter().enumerate() {
if transfer_pak_enabled {
select_gb_rom[i] = Some(
rfd::AsyncFileDialog::new()
.set_title(format!("GB ROM P{}", i + 1))
.add_filter("GB ROM files", &["gb", "gbc", "GB", "GBC"])
.pick_file(),
);
select_gb_ram[i] = Some(
rfd::AsyncFileDialog::new()
.set_title(format!("GB RAM P{}", i + 1))
.add_filter("GB RAM files", &["sav", "ram", "srm", "SAV", "RAM", "SRM"])
.pick_file(),
);
}
}
let fullscreen = app.get_fullscreen();
let overclock = app.get_overclock_n64_cpu();
let disable_expansion_pak = app.get_disable_expansion_pak();
let ra_enabled = app.get_ra_enabled();
let ra_hardcore = app.get_ra_hardcore();
let ra_challenge = app.get_ra_challenge();
let ra_leaderboard = app.get_ra_leaderboard();
let weak = app.as_weak();
tokio::spawn(async move {
if let Some(file) = select_rom.await {
let mut gb_rom_path = [None, None, None, None];
let mut gb_ram_path = [None, None, None, None];
for i in 0..4 {
if let (Some(gb_rom), Some(gb_ram)) =
(select_gb_rom[i].as_mut(), select_gb_ram[i].as_mut())
&& let (Some(gb_rom), Some(gb_ram)) = (gb_rom.await, gb_ram.await)
{
gb_rom_path[i] = Some(gb_rom.path().to_path_buf());
gb_ram_path[i] = Some(gb_ram.path().to_path_buf());
}
}
run_rom(
GbPaths {
rom: gb_rom_path,
ram: gb_ram_path,
},
file.path().to_path_buf(),
GameSettings {
fullscreen,
ui::GameSettings {
overclock,
disable_expansion_pak,
cheats: std::collections::HashMap::new(), // will be filled in later
load_savestate_slot: None,
},
None,
usb,
RASettings {
enabled: ra_enabled,
hardcore: ra_hardcore,
challenge: ra_challenge,
leaderboard: ra_leaderboard,
},
weak,
);
}
+45 -3
View File
@@ -2,9 +2,10 @@
// SPDX-License-Identifier: MIT
import { StandardButton } from "std-widgets.slint";
import { LocalGame, ControllerConfig, Netplay, Cheats, Settings, About } from "pages.slint";
import { LocalGame, ControllerConfig, Netplay, Cheats, RetroAchievements, Settings, About } from "pages.slint";
import { SideBar } from "side_bar.slint";
import { SettingsData } from "settings_page.slint";
import { RAData } from "retroachievements_page.slint";
import { AboutData } from "about_page.slint";
import { ControllerData } from "controller_page.slint";
import { State } from "localgame_page.slint";
@@ -21,12 +22,14 @@ export component AppWindow inherits Window {
callback wiki_button_clicked;
callback discord_button_clicked;
callback newversion_button_clicked;
callback recent_rom_button_clicked(string);
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 transferpak_toggled(int,bool);
callback create_session_button_clicked;
callback join_session_button_clicked;
callback netplay_discord_button_clicked;
@@ -34,11 +37,26 @@ export component AppWindow inherits Window {
callback cheats_select_rom_clicked(string);
callback cheats_clear_clicked;
callback cheat_toggled(string, string, string, bool);
callback ra_button_clicked(string);
callback ra_games_clicked;
callback ra_show_profile_clicked;
callback ra_toggled(bool,bool,bool,bool);
in property version <=> AboutData.version;
in property has_update <=> AboutData.has_update;
in-out property ra_username <=> RAData.ra_username;
in-out property ra_password <=> RAData.ra_password;
in-out property ra_logging_in <=> RAData.ra_logging_in;
in-out property ra_show_profile <=> RAData.ra_show_profile;
in-out property ra_current_user_message <=> RAData.current_user_message;
in-out property ra_enabled <=> RAData.enabled;
in-out property ra_hardcore <=> RAData.hardcore;
in-out property ra_challenge <=> RAData.challenge;
in-out property ra_leaderboard <=> RAData.leaderboard;
in-out property ra_softcore_only <=> RAData.ra_softcore_only;
in-out property integer_scaling <=> SettingsData.integer_scaling;
in-out property fullscreen <=> SettingsData.fullscreen;
in-out property widescreen <=> SettingsData.widescreen;
in-out property vsync <=> SettingsData.vsync;
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;
@@ -49,11 +67,14 @@ export component AppWindow inherits Window {
in-out property controller_enabled <=> ControllerData.controller_enabled;
in-out property controller_changed <=> ControllerData.controller_changed;
in-out property transferpak <=> ControllerData.transferpak;
in-out property gb_rom_paths <=> ControllerData.gb_rom_paths;
in-out property gb_ram_paths <=> ControllerData.gb_ram_paths;
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 recent_roms <=> State.recent_roms;
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;
@@ -71,6 +92,7 @@ export component AppWindow inherits Window {
@tr("Menu" => "Controller Config"),
@tr("Menu" => "Netplay"),
@tr("Menu" => "Cheats"),
@tr("Menu" => "RetroAchievements"),
@tr("Menu" => "Settings"),
@tr("Menu" => "About")
];
@@ -86,11 +108,17 @@ export component AppWindow inherits Window {
newversion_button_clicked => {
newversion_button_clicked();
}
recent_rom_button_clicked(rom) => {
recent_rom_button_clicked(rom);
}
}
if(side-bar.current-item == 1): ControllerConfig {
input_profile_button_clicked => {
input_profile_button_clicked();
}
transferpak_toggled(player,enabled) => {
transferpak_toggled(player,enabled);
}
}
if(side-bar.current-item == 2): Netplay {
create_session_button_clicked => {
@@ -117,8 +145,22 @@ export component AppWindow inherits Window {
cheat_toggled(game_crc,cheat_name,option,enabled);
}
}
if(side-bar.current-item == 4): Settings { }
if(side-bar.current-item == 5): About {
if(side-bar.current-item == 4): RetroAchievements {
ra_button_clicked(password) => {
ra_button_clicked(password);
}
ra_toggled(enabled,hardcore,challenge,leaderboard) => {
ra_toggled(enabled,hardcore,challenge,leaderboard);
}
ra_games_clicked => {
ra_games_clicked();
}
ra_show_profile_clicked => {
ra_show_profile_clicked();
}
}
if(side-bar.current-item == 5): Settings { }
if(side-bar.current-item == 6): About {
wiki_button_clicked => {
wiki_button_clicked();
}
+3 -4
View File
@@ -1,7 +1,7 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { ListView, HorizontalBox, VerticalBox, Button, GroupBox, CheckBox } from "std-widgets.slint";
import { ListView, HorizontalBox, VerticalBox, Button, GroupBox, Switch, CheckBox } from "std-widgets.slint";
import { Page } from "page.slint";
import { SettingsData } from "settings_page.slint";
@@ -62,15 +62,14 @@ export component Cheats inherits Page {
wrap: word-wrap;
}
if data.options.length == 0: CheckBox {
text: @tr("Enabled");
if data.options.length == 0: Switch {
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 {
for item in data.options: CheckBox {
text: item.option;
enabled: !data.enabled || (data.enabled && item.enabled);
checked: item.enabled;
+8 -4
View File
@@ -1,7 +1,7 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { CheckBox, ComboBox, Button, HorizontalBox, VerticalBox, GroupBox } from "std-widgets.slint";
import { Switch, ComboBox, Button, HorizontalBox, VerticalBox, GroupBox } from "std-widgets.slint";
import { Page } from "page.slint";
import { State } from "localgame_page.slint";
@@ -9,6 +9,8 @@ export global ControllerData {
in-out property <bool> emulate_vru;
in-out property <[bool]> controller_enabled;
in-out property <[bool]> transferpak;
in-out property <[string]> gb_rom_paths;
in-out property <[string]> gb_ram_paths;
in-out property <[int]> selected_profile_binding: [-1, -1, -1, -1];
in-out property <[string]> input_profiles;
in-out property <[string]> controller_names;
@@ -19,6 +21,7 @@ export global ControllerData {
export component ControllerConfig inherits Page {
callback input_profile_button_clicked;
callback transferpak_toggled(int,bool);
title: @tr("Controller Configuration");
VerticalBox {
@@ -52,7 +55,7 @@ export component ControllerConfig inherits Page {
title: @tr("Enabled");
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: CheckBox {
for player in [0, 1, 2, 3]: Switch {
checked: ControllerData.controller_enabled[player];
toggled => {
ControllerData.controller_enabled[player] = self.checked;
@@ -65,7 +68,7 @@ export component ControllerConfig inherits Page {
title: @tr("Emulate VRU");
VerticalLayout {
alignment: space-between;
for valid in [false, false, false, true]: CheckBox {
for valid in [false, false, false, true]: Switch {
enabled: valid;
checked: valid && ControllerData.emulate_vru;
toggled => {
@@ -79,10 +82,11 @@ export component ControllerConfig inherits Page {
title: @tr("Transfer Pak");
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: CheckBox {
for player in [0, 1, 2, 3]: Switch {
checked: ControllerData.transferpak[player];
toggled => {
ControllerData.transferpak[player] = self.checked;
transferpak_toggled(player,self.checked);
}
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { StandardButton, HorizontalBox, VerticalBox, Button, LineEdit, CheckBox, Slider } from "std-widgets.slint";
import { StandardButton, HorizontalBox, VerticalBox, Button, LineEdit, Switch, Slider } from "std-widgets.slint";
export global InputProfileData {
in-out property <bool> dinput;
@@ -47,7 +47,7 @@ export component InputProfileDialog inherits Window {
HorizontalBox {
alignment: center;
CheckBox {
Switch {
text: @tr("Use DirectInput");
checked: InputProfileData.dinput;
toggled => {
+20 -1
View File
@@ -1,18 +1,20 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Button, HorizontalBox, VerticalBox } from "std-widgets.slint";
import { Button, HorizontalBox, VerticalBox, GroupBox } 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;
in-out property <[{a:string, b:string}]> recent_roms;
}
export component LocalGame inherits Page {
callback open_rom_button_clicked;
callback saves_folder_button_clicked;
callback newversion_button_clicked;
callback recent_rom_button_clicked(string);
title: @tr("Local Game");
VerticalBox {
@@ -44,5 +46,22 @@ export component LocalGame inherits Page {
}
}
Rectangle { }
if State.recent_roms.length>0:HorizontalBox {
alignment: center;
GroupBox {
title: @tr("Recent ROMs");
VerticalLayout {
alignment: space-between;
for rom in State.recent_roms: Button {
text: rom.b;
enabled: !State.game_running;
clicked => {
recent_rom_button_clicked(rom.a);
}
}
}
}
}
}
}
+3 -3
View File
@@ -25,7 +25,7 @@ export component NetplayCreate inherits Window {
}
LineEdit {
placeholder_text: @tr("Enter session name");
placeholder-text: @tr("Enter session name");
edited(text) => {
NetplayData.session_name = text;
}
@@ -38,7 +38,7 @@ export component NetplayCreate inherits Window {
}
LineEdit {
placeholder_text: @tr("Enter session password");
placeholder-text: @tr("Enter session password");
edited(text) => {
NetplayData.password = text;
}
@@ -52,7 +52,7 @@ export component NetplayCreate inherits Window {
}
LineEdit {
placeholder_text: @tr("Enter player name");
placeholder-text: @tr("Enter player name");
edited(text) => {
NetplayData.player_name = text;
}
+2 -2
View File
@@ -27,11 +27,11 @@ export component NetplayJoin inherits Window {
HorizontalBox {
VerticalBox {
Text {
text: @tr("Player Name:");
text: @tr("Player Name (required):");
}
LineEdit {
placeholder_text: @tr("Enter player name");
placeholder-text: @tr("Enter player name");
edited(text) => {
NetplayData.player_name = text;
}
+1 -1
View File
@@ -73,7 +73,7 @@ export component NetplayWait inherits Window {
HorizontalBox {
chat_message := LineEdit {
placeholder_text: @tr("Enter chat message");
placeholder-text: @tr("Enter chat message");
key-released(event) => {
if (event.text == Key.Return && chat_message.text != "") {
send_chat_message(chat_message.text);
+1
View File
@@ -2,5 +2,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 { RetroAchievements } from "retroachievements_page.slint";
export { Settings } from "settings_page.slint";
export { About } from "about_page.slint";
+143
View File
@@ -0,0 +1,143 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { LineEdit, Switch, Button, HorizontalBox, VerticalBox } from "std-widgets.slint";
import { Page } from "page.slint";
export global RAData {
in-out property <bool> enabled;
in-out property <bool> hardcore;
in-out property <bool> challenge;
in-out property <bool> leaderboard;
in-out property <bool> ra_logging_in;
in-out property <bool> ra_show_profile;
in-out property <bool> ra_softcore_only;
in-out property <string> ra_username;
in-out property <string> ra_password;
in-out property <string> current_user_message;
}
export component RetroAchievements inherits Page {
callback ra_button_clicked(string);
callback ra_toggled(bool,bool,bool,bool);
callback ra_games_clicked;
callback ra_show_profile_clicked;
title: @tr("RetroAchievements");
VerticalBox {
alignment: start;
HorizontalBox {
alignment: start;
Switch {
text: @tr("Enabled");
checked: RAData.enabled;
toggled => {
RAData.enabled = self.checked;
if (!RAData.enabled) {
RAData.ra_username = "";
username_widget.text = "";
password_widget.text = "";
RAData.current_user_message = "Not currently logged in";
RAData.ra_show_profile = false;
}
ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge,RAData.leaderboard);
}
}
}
if !RAData.ra_softcore_only:HorizontalBox {
alignment: start;
Switch {
text: @tr("Hardcore mode");
checked: RAData.hardcore;
enabled: RAData.enabled;
toggled => {
RAData.hardcore = self.checked;
ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge,RAData.leaderboard);
}
}
}
HorizontalBox {
alignment: start;
Switch {
text: @tr("Challenge Indicators");
checked: RAData.challenge;
enabled: RAData.enabled;
toggled => {
RAData.challenge = self.checked;
ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge,RAData.leaderboard);
}
}
}
HorizontalBox {
alignment: start;
Switch {
text: @tr("Leaderboard Trackers");
checked: RAData.leaderboard;
enabled: RAData.enabled;
toggled => {
RAData.leaderboard = self.checked;
ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge,RAData.leaderboard);
}
}
}
HorizontalBox {
alignment: center;
Text {
text: RAData.current_user_message;
vertical-alignment: center;
}
if RAData.ra_show_profile:Button {
enabled: RAData.enabled;
text: @tr("View Profile");
clicked => {
ra_show_profile_clicked();
}
}
}
HorizontalBox {
alignment: center;
username_widget := LineEdit {
enabled: RAData.enabled;
placeholder-text: @tr("Username");
text: RAData.ra_username;
edited(text) => {
RAData.ra_username = text;
}
}
password_widget := LineEdit {
enabled: RAData.enabled;
input-type: password;
placeholder-text: @tr("Password");
edited(text) => {
RAData.ra_password = text;
}
}
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Login");
enabled: RAData.enabled && !RAData.ra_logging_in && RAData.ra_username != "" && RAData.ra_password != "";
clicked => {
RAData.ra_logging_in = true;
ra_button_clicked(RAData.ra_password);
RAData.ra_password = "";
password_widget.text = "";
}
}
}
Rectangle {
height: 20px;
}
HorizontalBox {
alignment: center;
Button {
text: @tr("View Supported Games");
clicked => {
ra_games_clicked();
}
}
}
}
}
+23 -11
View File
@@ -1,13 +1,14 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { ComboBox, HorizontalBox, VerticalBox, CheckBox } from "std-widgets.slint";
import { ComboBox, HorizontalBox, VerticalBox, Switch } 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> vsync;
in-out property <bool> apply_crt_shader;
in-out property <bool> overclock_n64_cpu;
in-out property <bool> disable_expansion_pak;
@@ -39,7 +40,7 @@ export component Settings inherits Page {
HorizontalBox {
alignment: start;
CheckBox {
Switch {
text: @tr("Integer Scaling");
checked: SettingsData.integer_scaling;
toggled => {
@@ -50,8 +51,8 @@ export component Settings inherits Page {
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Fullscreen (Esc closes game)");
Switch {
text: @tr("Fullscreen");
checked: SettingsData.fullscreen;
toggled => {
SettingsData.fullscreen = self.checked;
@@ -61,8 +62,8 @@ export component Settings inherits Page {
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Widescreen (strech)");
Switch {
text: @tr("Widescreen (stretch)");
checked: SettingsData.widescreen;
toggled => {
SettingsData.widescreen = self.checked;
@@ -72,7 +73,18 @@ export component Settings inherits Page {
HorizontalBox {
alignment: start;
CheckBox {
Switch {
text: @tr("VSync");
checked: SettingsData.vsync;
toggled => {
SettingsData.vsync = self.checked;
}
}
}
HorizontalBox {
alignment: start;
Switch {
text: @tr("Apply CRT shader");
checked: SettingsData.apply_crt_shader;
toggled => {
@@ -87,7 +99,7 @@ export component Settings inherits Page {
HorizontalBox {
alignment: start;
CheckBox {
Switch {
text: @tr("Overclock N64 CPU (may cause bugs)");
checked: SettingsData.overclock_n64_cpu;
toggled => {
@@ -98,7 +110,7 @@ export component Settings inherits Page {
HorizontalBox {
alignment: start;
CheckBox {
Switch {
text: @tr("Disable Expansion Pak");
checked: SettingsData.disable_expansion_pak;
toggled => {
@@ -109,8 +121,8 @@ export component Settings inherits Page {
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Emulate UNFLoader (reload emulator after changing)");
Switch {
text: @tr("Emulate UNFLoader");
checked: SettingsData.emulate_usb;
toggled => {
SettingsData.emulate_usb = self.checked;
+52 -35
View File
@@ -42,7 +42,7 @@ pub struct InputData {
}
fn bound_axis(x: &mut f64, y: &mut f64) {
let radius = f64::sqrt(70.0 * 70.0 + 70.0 * 70.0); // this is roughly the maxium diagonal distance of the controller
let radius = f64::sqrt(70.0 * 70.0 + 70.0 * 70.0); // this is roughly the maximum diagonal distance of the controller
// Calculate the distance from the origin (0, 0)
let distance = f64::sqrt((*x) * (*x) + (*y) * (*y));
@@ -73,6 +73,10 @@ fn apply_deadzone(x: &mut f64, y: &mut f64, deadzone: i32) {
*y = *y / distance * new_distance;
}
fn normalize_axis_position(axis_position: i16) -> f64 {
axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64
}
fn set_axis_from_joystick(
profile: &ui::config::InputProfile,
joystick: *mut sdl3_sys::joystick::SDL_Joystick,
@@ -84,7 +88,7 @@ fn set_axis_from_joystick(
sdl3_sys::joystick::SDL_GetJoystickAxis(joystick, profile.joystick_axis[AXIS_LEFT].id)
};
if axis_position as isize * profile.joystick_axis[AXIS_LEFT].axis as isize > 0 {
x = axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64;
x = normalize_axis_position(axis_position);
}
}
if profile.joystick_axis[AXIS_RIGHT].enabled {
@@ -92,7 +96,7 @@ fn set_axis_from_joystick(
sdl3_sys::joystick::SDL_GetJoystickAxis(joystick, profile.joystick_axis[AXIS_RIGHT].id)
};
if axis_position as isize * profile.joystick_axis[AXIS_RIGHT].axis as isize > 0 {
x = axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64;
x = normalize_axis_position(axis_position);
}
}
if profile.joystick_axis[AXIS_DOWN].enabled {
@@ -100,7 +104,7 @@ fn set_axis_from_joystick(
sdl3_sys::joystick::SDL_GetJoystickAxis(joystick, profile.joystick_axis[AXIS_DOWN].id)
};
if axis_position as isize * profile.joystick_axis[AXIS_DOWN].axis as isize > 0 {
y = (axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64).neg();
y = normalize_axis_position(axis_position).neg();
}
}
if profile.joystick_axis[AXIS_UP].enabled {
@@ -108,7 +112,7 @@ fn set_axis_from_joystick(
sdl3_sys::joystick::SDL_GetJoystickAxis(joystick, profile.joystick_axis[AXIS_UP].id)
};
if axis_position as isize * profile.joystick_axis[AXIS_UP].axis as isize > 0 {
y = (axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64).neg();
y = normalize_axis_position(axis_position).neg();
}
}
(x, y)
@@ -172,7 +176,7 @@ fn set_axis_from_controller(
)
};
if axis_position as isize * profile.controller_axis[AXIS_LEFT].axis as isize > 0 {
x = axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64;
x = normalize_axis_position(axis_position);
}
}
if profile.controller_axis[AXIS_RIGHT].enabled {
@@ -183,7 +187,7 @@ fn set_axis_from_controller(
)
};
if axis_position as isize * profile.controller_axis[AXIS_RIGHT].axis as isize > 0 {
x = axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64;
x = normalize_axis_position(axis_position);
}
}
if profile.controller_axis[AXIS_DOWN].enabled {
@@ -194,7 +198,7 @@ fn set_axis_from_controller(
)
};
if axis_position as isize * profile.controller_axis[AXIS_DOWN].axis as isize > 0 {
y = (axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64).neg();
y = normalize_axis_position(axis_position).neg();
}
}
if profile.controller_axis[AXIS_UP].enabled {
@@ -205,7 +209,7 @@ fn set_axis_from_controller(
)
};
if axis_position as isize * profile.controller_axis[AXIS_UP].axis as isize > 0 {
y = (axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64).neg();
y = normalize_axis_position(axis_position).neg();
}
}
(x, y)
@@ -380,6 +384,7 @@ pub fn get_controller_names(game_ui: &ui::Ui) -> Vec<String> {
controllers
}
#[cfg(feature = "gui")]
pub fn get_controller_paths(game_ui: &ui::Ui) -> Vec<Option<String>> {
let mut controller_paths: Vec<Option<String>> = vec![];
@@ -485,9 +490,13 @@ pub fn clear_bindings(ui: &mut ui::Ui) {
}
}
fn close_controllers(
fn close_input_profile_window(
open_joysticks: Vec<*mut sdl3_sys::joystick::SDL_Joystick>,
open_controllers: Vec<*mut sdl3_sys::gamepad::SDL_Gamepad>,
renderer: *mut sdl3_sys::render::SDL_Renderer,
text_engine: *mut sdl3_ttf_sys::ttf::TTF_TextEngine,
font: *mut sdl3_ttf_sys::ttf::TTF_Font,
window: *mut sdl3_sys::video::SDL_Window,
) {
for joystick in open_joysticks {
unsafe { sdl3_sys::joystick::SDL_CloseJoystick(joystick) }
@@ -495,6 +504,13 @@ fn close_controllers(
for controller in open_controllers {
unsafe { sdl3_sys::gamepad::SDL_CloseGamepad(controller) }
}
unsafe {
sdl3_ttf_sys::ttf::TTF_CloseFont(font);
sdl3_ttf_sys::ttf::TTF_DestroyRendererTextEngine(text_engine);
sdl3_sys::render::SDL_DestroyRenderer(renderer);
sdl3_sys::video::SDL_DestroyWindow(window);
}
}
pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool, deadzone: i32) {
@@ -532,7 +548,7 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool, d
if !unsafe {
sdl3_sys::render::SDL_CreateWindowAndRenderer(
title.as_ptr(),
640,
852,
480,
sdl3_sys::video::SDL_WindowFlags(0),
&mut window,
@@ -603,7 +619,7 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool, d
let text_engine = unsafe { sdl3_ttf_sys::ttf::TTF_CreateRendererTextEngine(renderer) };
let font = unsafe {
let font_bytes = include_bytes!("../../data/Roboto-Regular.ttf");
let font_bytes = include_bytes!("../../data/RobotoMono-Regular.ttf");
sdl3_ttf_sys::ttf::TTF_OpenFontIO(
sdl3_sys::everything::SDL_IOFromConstMem(
font_bytes.as_ptr() as *const std::ffi::c_void,
@@ -618,20 +634,26 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool, d
let mut event: sdl3_sys::events::SDL_Event = Default::default();
while unsafe { sdl3_sys::events::SDL_PollEvent(&mut event) } {} // clear events
ui::video::draw_text(
format!("Select binding for: {key}").as_str(),
renderer,
text_engine,
font,
);
let mut key_set = false;
while !key_set {
ui::video::draw_text(
format!("Select binding for: {key}").as_str(),
renderer,
text_engine,
font,
);
std::thread::sleep(std::time::Duration::from_millis(100));
while unsafe { sdl3_sys::events::SDL_PollEvent(&mut event) } {
let event_type = unsafe { event.r#type };
if event_type == u32::from(sdl3_sys::events::SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
close_controllers(open_joysticks, open_controllers);
close_input_profile_window(
open_joysticks,
open_controllers,
renderer,
text_engine,
font,
window,
);
return;
} else if event_type == u32::from(sdl3_sys::events::SDL_EVENT_KEY_DOWN) {
if unsafe {
@@ -711,23 +733,18 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool, d
}
}
}
ui::video::draw_text(
format!("Binding set for {key}").as_str(),
renderer,
text_engine,
font,
);
std::thread::sleep(std::time::Duration::from_millis(200));
ui::video::draw_text("Binding set, please wait...", renderer, text_engine, font);
std::thread::sleep(std::time::Duration::from_millis(500));
}
close_controllers(open_joysticks, open_controllers);
unsafe {
sdl3_ttf_sys::ttf::TTF_CloseFont(font);
sdl3_ttf_sys::ttf::TTF_DestroyRendererTextEngine(text_engine);
sdl3_sys::render::SDL_DestroyRenderer(renderer);
sdl3_sys::video::SDL_DestroyWindow(window);
}
close_input_profile_window(
open_joysticks,
open_controllers,
renderer,
text_engine,
font,
window,
);
let new_profile = ui::config::InputProfile {
keys: new_keys,
+101 -74
View File
@@ -1,8 +1,8 @@
use crate::device;
use crate::ui;
use crate::ui::gui::{
AppWindow, CustomNetplayServer, DispatcherDialog, ErrorDialog, GameSettings, GbPaths,
NetplayCreate, NetplayDevice, NetplayJoin, NetplayWait, run_rom, save_settings,
AppWindow, CustomNetplayServer, DispatcherDialog, ErrorDialog, NetplayCreate, NetplayDevice,
NetplayJoin, NetplayWait, RASettings, run_rom, save_settings,
};
use futures::{SinkExt, StreamExt};
use sha2::{Digest, Sha256};
@@ -121,10 +121,9 @@ fn populate_server_names<T: ComponentHandle + NetplayPages + 'static>(weak: slin
.unwrap();
broadcast_sock.set_broadcast(true).unwrap();
let data: [u8; 1] = [1];
broadcast_sock
let _ = broadcast_sock
.send_to(&data, (std::net::Ipv4Addr::BROADCAST, 45000))
.await
.unwrap();
.await;
let mut buffer = [0; 1024];
if let Ok(Ok(result)) = tokio::time::timeout(
std::time::Duration::from_millis(200),
@@ -160,12 +159,8 @@ fn populate_server_names<T: ComponentHandle + NetplayPages + 'static>(weak: slin
}
server_names.push("Custom".into());
handle.refresh_sessions();
let server_names_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
std::rc::Rc::new(server_names);
let server_urls_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
std::rc::Rc::new(server_urls);
handle.set_server_names(slint::ModelRc::from(server_names_model));
handle.set_server_urls(slint::ModelRc::from(server_urls_model));
handle.set_server_names(slint::ModelRc::from(std::rc::Rc::new(server_names)));
handle.set_server_urls(slint::ModelRc::from(std::rc::Rc::new(server_urls)));
})
.unwrap();
});
@@ -193,7 +188,7 @@ fn select_rom<T: ComponentHandle + NetplayPages + 'static>(
let mut game_name = ui::storage::get_game_name(&rom_contents);
let game_crc = ui::storage::get_game_crc(&rom_contents);
let cheats = ui::config::Cheats::new();
let mut parsed_cheats = "".to_string();
let mut parsed_cheats = String::new();
if let Some(game_cheats) = cheats.cheats.get(&game_crc)
&& !game_cheats.is_empty()
{
@@ -225,10 +220,10 @@ fn select_rom<T: ComponentHandle + NetplayPages + 'static>(
message_dialog.set_text("Could not read ROM".into());
message_dialog.show().unwrap();
handle.set_game_name("".into());
handle.set_game_hash("".into());
handle.set_game_cheats("".into());
handle.set_rom_path("".into());
handle.set_game_name(String::new().into());
handle.set_game_hash(String::new().into());
handle.set_game_cheats(String::new().into());
handle.set_rom_path(String::new().into());
})
.unwrap();
}
@@ -252,7 +247,8 @@ fn show_custom_url_dialog(weak: slint::Weak<NetplayCreate>, server_url: slint::S
pub fn setup_create_window(
create_window: &NetplayCreate,
game_settings: GameSettings,
game_settings: ui::GameSettings,
ra_settings: RASettings,
rom_dir: slint::SharedString,
weak_app: slint::Weak<AppWindow>,
) {
@@ -308,6 +304,7 @@ pub fn setup_create_window(
let game_settings = game_settings.clone();
let weak = weak.clone();
let weak_app = weak_app.clone();
let ra_settings = ra_settings.clone();
tokio::spawn(async move {
let response = task.await;
weak_dialog
@@ -339,6 +336,7 @@ pub fn setup_create_window(
game_cheats.to_string(),
password.to_string(),
game_settings,
ra_settings,
weak_app,
weak.clone(),
);
@@ -368,6 +366,7 @@ pub fn setup_create_window(
game_cheats.to_string(),
password.to_string(),
game_settings.clone(),
ra_settings.clone(),
weak_app.clone(),
weak.clone(),
);
@@ -517,7 +516,13 @@ fn update_sessions(weak: slint::Weak<NetplayJoin>) {
rooms: None,
message: None,
auth_time: Some(now_utc),
auth: Some(format!("{:x}", hasher.finalize())),
auth: Some(
hasher
.finalize()
.iter()
.map(|b| format!("{:02x}", b))
.collect(),
),
room: None,
};
write
@@ -574,26 +579,19 @@ fn update_sessions(weak: slint::Weak<NetplayJoin>) {
.upgrade_in_event_loop(move |handle| {
let sessions_vec = slint::VecModel::default();
for session in sessions.iter() {
let session_vec = slint::VecModel::from(session.to_vec());
let session_model: std::rc::Rc<
slint::VecModel<slint::StandardListViewItem>,
> = std::rc::Rc::new(session_vec);
sessions_vec.push(slint::ModelRc::from(session_model));
sessions_vec.push(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(session.to_vec()),
)));
}
let rooms_model: std::rc::Rc<
slint::VecModel<slint::ModelRc<slint::StandardListViewItem>>,
> = std::rc::Rc::new(sessions_vec);
handle.set_sessions(slint::ModelRc::from(rooms_model));
handle.set_sessions(slint::ModelRc::from(std::rc::Rc::new(sessions_vec)));
let room_urls_vec = slint::VecModel::from(room_urls.to_vec());
let room_urls_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
std::rc::Rc::new(room_urls_vec);
handle.set_room_urls(slint::ModelRc::from(room_urls_model));
handle.set_room_urls(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(room_urls.to_vec()),
)));
let room_ports_vec = slint::VecModel::from(room_ports.to_vec());
let room_ports_model: std::rc::Rc<slint::VecModel<i32>> =
std::rc::Rc::new(room_ports_vec);
handle.set_room_ports(slint::ModelRc::from(room_ports_model));
handle.set_room_ports(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(room_ports.to_vec()),
)));
handle.set_current_session(-1);
handle.set_pending_refresh(false);
@@ -615,7 +613,8 @@ fn create_session(
game_hash: String,
game_cheats: String,
password: String,
game_settings: GameSettings,
game_settings: ui::GameSettings,
ra_settings: RASettings,
weak_app: slint::Weak<AppWindow>,
weak: slint::Weak<NetplayCreate>,
) {
@@ -637,7 +636,7 @@ fn create_session(
let create_room = NetplayMessage {
message_type: "request_create_room".to_string(),
player_name: Some(player_name),
client_sha: Some(env!("GIT_HASH").to_string()),
client_sha: Some(env!("GIT_DESCRIBE").to_string()),
netplay_version: Some(NETPLAY_VERSION),
emulator: Some(EMU_NAME.to_string()),
accept: None,
@@ -645,7 +644,13 @@ fn create_session(
rooms: None,
auth_time: Some(now_utc),
player_names: None,
auth: Some(format!("{:x}", hasher.finalize())),
auth: Some(
hasher
.finalize()
.iter()
.map(|b| format!("{:02x}", b))
.collect(),
),
room: Some(NetplayRoom {
room_name: Some(session_name),
password: Some(password),
@@ -698,13 +703,13 @@ fn create_session(
handle.get_rom_path(),
message.player_name.as_ref().unwrap().into(),
session.port.unwrap(),
true,
GameSettings {
fullscreen: game_settings.fullscreen,
ui::GameSettings {
overclock: overclock.parse().unwrap(),
disable_expansion_pak: disable_expansion_pak.parse().unwrap(),
cheats: serde_json::from_str(cheats).unwrap(),
load_savestate_slot: None,
},
ra_settings,
handle.get_peer_addr(),
weak_app,
);
@@ -743,7 +748,7 @@ fn join_session(
game_hash: String,
password: String,
room_port: i32,
fullscreen: bool,
ra_settings: RASettings,
weak_app: slint::Weak<AppWindow>,
weak: slint::Weak<NetplayJoin>,
) {
@@ -754,7 +759,7 @@ fn join_session(
let join_room = NetplayMessage {
message_type: "request_join_room".to_string(),
player_name: Some(player_name),
client_sha: Some(env!("GIT_HASH").to_string()),
client_sha: Some(env!("GIT_DESCRIBE").to_string()),
netplay_version: Some(NETPLAY_VERSION),
emulator: Some(EMU_NAME.to_string()),
accept: None,
@@ -762,7 +767,13 @@ fn join_session(
rooms: None,
player_names: None,
auth_time: Some(now_utc),
auth: Some(format!("{:x}", hasher.finalize())),
auth: Some(
hasher
.finalize()
.iter()
.map(|b| format!("{:02x}", b))
.collect(),
),
room: Some(NetplayRoom {
room_name: None,
password: Some(password),
@@ -815,13 +826,13 @@ fn join_session(
handle.get_rom_path(),
message.player_name.as_ref().unwrap().into(),
session.port.unwrap(),
false,
GameSettings {
fullscreen,
ui::GameSettings {
overclock: overclock.parse().unwrap(),
disable_expansion_pak: disable_expansion_pak.parse().unwrap(),
cheats: serde_json::from_str(cheats).unwrap(),
load_savestate_slot: None,
},
ra_settings,
handle.get_peer_addr(),
weak_app,
);
@@ -861,8 +872,8 @@ fn setup_wait_window(
rom_path: slint::SharedString,
player_name: slint::SharedString,
port: i32,
can_start: bool,
game_settings: GameSettings,
game_settings: ui::GameSettings,
ra_settings: RASettings,
peer_addr: slint::SharedString,
weak_app: slint::Weak<AppWindow>,
) {
@@ -875,7 +886,6 @@ fn setup_wait_window(
wait.set_game_name(game_name);
wait.set_rom_path(rom_path);
wait.set_port(port);
wait.set_can_start(can_start);
wait.set_ping("Unknown".into());
let sender = netplay_write_sender.clone();
@@ -1032,17 +1042,23 @@ fn setup_wait_window(
.unwrap();
}
"reply_players" => {
let local_player = local_player.clone();
weak.upgrade_in_event_loop(move |handle| {
if let Some(player_names) = response.player_names {
let players_vec: slint::VecModel<slint::SharedString> =
slint::VecModel::default();
for player in player_names {
players_vec.push(player.into());
if local_player == player_names[0] {
handle.set_can_start(true);
} else {
handle.set_can_start(false);
}
let players_model: std::rc::Rc<
slint::VecModel<slint::SharedString>,
> = std::rc::Rc::new(players_vec);
handle.set_players(slint::ModelRc::from(players_model));
handle.set_players(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(
player_names
.iter()
.map(|x| x.into())
.collect::<Vec<slint::SharedString>>(),
),
)));
}
})
.unwrap();
@@ -1073,33 +1089,33 @@ fn setup_wait_window(
}
run_rom(
GbPaths {
rom: [None, None, None, None],
ram: [None, None, None, None],
},
handle.get_rom_path().as_str().into(),
GameSettings {
fullscreen: game_settings.fullscreen,
ui::GameSettings {
overclock: game_settings.overclock,
disable_expansion_pak: game_settings.disable_expansion_pak,
cheats: game_settings.cheats,
load_savestate_slot: None,
},
Some(NetplayDevice {
peer_addr: socket_addr,
player_number: player_number as u8,
}),
ui::Usb {
usb_tx: None,
cart_rx: None,
},
ra_settings,
weak_app,
);
})
.unwrap();
return;
} else {
let local_player = local_player.clone();
weak.upgrade_in_event_loop(move |handle| {
handle.set_can_start(can_start);
if let Some(p0) = handle.get_players().row_data(0) {
if p0 == local_player {
handle.set_can_start(true);
} else {
handle.set_can_start(false);
}
}
if let Some(message) = response.message {
show_netplay_error(message);
}
@@ -1126,8 +1142,8 @@ fn setup_wait_window(
pub fn setup_join_window(
join_window: &NetplayJoin,
fullscreen: bool,
rom_dir: slint::SharedString,
ra_settings: RASettings,
weak_app: slint::Weak<AppWindow>,
) {
let (netplay_read_sender, netplay_read_receiver): (
@@ -1175,7 +1191,7 @@ pub fn setup_join_window(
game_hash.to_string(),
password.to_string(),
room_port,
fullscreen,
ra_settings.clone(),
weak_app.clone(),
weak.clone(),
);
@@ -1198,11 +1214,17 @@ pub fn netplay_window(app: &AppWindow, controller_paths: &[Option<String>]) {
save_settings(&handle, &controller_paths);
setup_create_window(
&create_window,
GameSettings {
fullscreen: handle.get_fullscreen(),
ui::GameSettings {
overclock: handle.get_overclock_n64_cpu(),
disable_expansion_pak: handle.get_disable_expansion_pak(),
cheats: std::collections::HashMap::new(), // not used here
load_savestate_slot: None,
},
RASettings {
enabled: handle.get_ra_enabled(),
hardcore: handle.get_ra_hardcore(),
challenge: handle.get_ra_challenge(),
leaderboard: handle.get_ra_leaderboard(),
},
handle.get_rom_dir(),
weak_app,
@@ -1223,8 +1245,13 @@ pub fn netplay_window(app: &AppWindow, controller_paths: &[Option<String>]) {
save_settings(&handle, &controller_paths);
setup_join_window(
&join_window,
handle.get_fullscreen(),
handle.get_rom_dir(),
RASettings {
enabled: handle.get_ra_enabled(),
hardcore: handle.get_ra_hardcore(),
challenge: handle.get_ra_challenge(),
leaderboard: handle.get_ra_leaderboard(),
},
weak_app,
);
})
+120
View File
@@ -0,0 +1,120 @@
use crate::retroachievements;
use crate::ui;
use slint::ComponentHandle;
pub fn ra_window(app: &ui::gui::AppWindow) {
let mut token = String::new();
if let Ok(ra_config) = std::fs::read(ui::get_dirs().config_dir.join("retroachievements.json"))
&& let Ok(result) =
serde_json::from_slice::<retroachievements::RAConfig>(ra_config.as_ref())
{
app.set_ra_username(result.username.into());
app.set_ra_enabled(result.enabled);
app.set_ra_hardcore(result.hardcore);
app.set_ra_challenge(result.challenge);
app.set_ra_leaderboard(result.leaderboard);
token = result.token;
} else {
app.set_ra_hardcore(true);
}
if !cfg!(ra_hardcore_enabled) {
app.set_ra_softcore_only(true);
}
if app.get_ra_enabled() && !app.get_ra_username().is_empty() {
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
retroachievements::login_token_user(app.get_ra_username().to_string(), token, tx);
set_current_user_message(app, rx);
} else {
app.set_ra_current_user_message("Not currently logged in".into());
}
let weak_app2 = app.as_weak();
app.on_ra_button_clicked(move |password| {
weak_app2
.upgrade_in_event_loop(move |handle| {
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
retroachievements::login_user(
handle.get_ra_username().to_string(),
password.to_string(),
tx,
);
set_current_user_message(&handle, rx);
})
.unwrap();
});
app.on_ra_toggled(move |enabled, hardcore, challenge, leaderboard| {
let file_path = ui::get_dirs().config_dir.join("retroachievements.json");
let raconfig = if let Ok(ra_config) = std::fs::read(&file_path)
&& let Ok(result) =
serde_json::from_slice::<retroachievements::RAConfig>(ra_config.as_ref())
{
if !enabled {
retroachievements::logout_user();
retroachievements::RAConfig {
username: String::new(),
token: String::new(),
enabled,
hardcore,
challenge,
leaderboard,
}
} else {
retroachievements::RAConfig {
username: result.username,
token: result.token,
enabled,
hardcore,
challenge,
leaderboard,
}
}
} else {
retroachievements::RAConfig {
username: String::new(),
token: String::new(),
enabled,
hardcore,
challenge,
leaderboard,
}
};
let f = std::fs::File::create(&file_path).unwrap();
serde_json::to_writer_pretty(f, &raconfig).unwrap();
});
app.on_ra_games_clicked(move || {
open::that_detached("https://retroachievements.org/system/2-nintendo-64/games").unwrap();
});
app.on_ra_show_profile_clicked(move || {
open::that_detached(format!(
"https://retroachievements.org/user/{}",
retroachievements::get_username().unwrap_or_default()
))
.unwrap();
});
}
fn set_current_user_message(app: &ui::gui::AppWindow, rx: tokio::sync::oneshot::Receiver<bool>) {
app.set_ra_current_user_message("Logging in...".into());
let weak_app = app.as_weak();
tokio::spawn(async move {
rx.await.unwrap();
weak_app
.upgrade_in_event_loop(move |handle| {
if let Some(username) = retroachievements::get_username() {
handle.set_ra_current_user_message(format!("Logged in as {}", username).into());
handle.set_ra_show_profile(true);
} else {
handle.set_ra_current_user_message("Login failed".into());
handle.set_ra_show_profile(false);
}
handle.set_ra_logging_in(false);
})
.unwrap();
});
}
+13 -16
View File
@@ -135,13 +135,13 @@ pub fn get_game_crc(rom: &[u8]) -> String {
}
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 let Ok(header_value) = header_value {
let re = regex::Regex::new(r"[^a-zA-Z0-9_ -]").unwrap();
game_name = re.replace_all(header_value, "").trim().replace('\0', "");
re.replace_all(header_value, "").trim().replace('\0', "")
} else {
String::new()
}
game_name
}
pub fn init(ui: &mut ui::Ui, rom: &[u8]) {
@@ -163,49 +163,46 @@ pub fn init(ui: &mut ui::Ui, rom: &[u8]) {
ui.storage
.paths
.eep_file_path
.push(prefix.to_owned() + "-" + &ui.game_hash + ".eep");
.push(format!("{}-{}.eep", prefix, ui.game_hash));
ui.storage.paths.sra_file_path.clone_from(&saves_path);
ui.storage
.paths
.sra_file_path
.push(prefix.to_owned() + "-" + &ui.game_hash + ".sra");
.push(format!("{}-{}.sra", prefix, ui.game_hash));
ui.storage.paths.fla_file_path.clone_from(&saves_path);
ui.storage
.paths
.fla_file_path
.push(prefix.to_owned() + "-" + &ui.game_hash + ".fla");
.push(format!("{}-{}.fla", prefix, ui.game_hash));
ui.storage.paths.pak_file_path.clone_from(&saves_path);
ui.storage
.paths
.pak_file_path
.push(prefix.to_owned() + "-" + &ui.game_hash + ".mpk");
.push(format!("{}-{}.mpk", prefix, ui.game_hash));
ui.storage.paths.sdcard_file_path.clone_from(&saves_path);
ui.storage
.paths
.sdcard_file_path
.push(prefix.to_owned() + "-" + &ui.game_hash + ".img");
.push(format!("{}-{}.img", prefix, ui.game_hash));
ui.storage.paths.romsave_file_path.clone_from(&saves_path);
ui.storage
.paths
.romsave_file_path
.push(prefix.to_owned() + "-" + &ui.game_hash + ".romsave");
.push(format!("{}-{}.romsave", prefix, ui.game_hash));
ui.storage
.paths
.savestate_file_path
.clone_from(&states_path);
ui.storage.paths.savestate_file_path.push(
prefix.to_owned()
+ "-"
+ &ui.game_hash
+ ".state"
+ ui.storage.save_state_slot.to_string().as_str(),
);
ui.storage.paths.savestate_file_path.push(format!(
"{}-{}.state{}",
prefix, ui.game_hash, ui.storage.save_state_slot
));
}
pub fn load_saves(ui: &mut ui::Ui, netplay: &mut Option<netplay::Netplay>) {
+3 -45
View File
@@ -26,52 +26,12 @@ fn respond_to_handshake(usb_tx: &tokio::sync::broadcast::Sender<UsbData>, data:
}
}
fn upload_rom(
weak: slint::Weak<ui::gui::AppWindow>,
rom: Vec<u8>,
usb_tx: &tokio::sync::broadcast::Sender<UsbData>,
cart_rx: &tokio::sync::broadcast::Receiver<UsbData>,
) {
let weak_clone = weak.clone();
let usb_tx = usb_tx.clone();
let cart_rx = cart_rx.resubscribe();
weak.upgrade_in_event_loop(move |handle| {
let dir = std::env::temp_dir();
let rom_path = dir.join("rom_upload.bin");
std::fs::write(rom_path.clone(), rom).unwrap();
ui::gui::run_rom(
ui::gui::GbPaths {
rom: [None, None, None, None],
ram: [None, None, None, None],
},
rom_path,
ui::gui::GameSettings {
fullscreen: handle.get_fullscreen(),
overclock: handle.get_overclock_n64_cpu(),
disable_expansion_pak: handle.get_disable_expansion_pak(),
cheats: std::collections::HashMap::new(), // will be filled in later
},
None,
ui::Usb {
usb_tx: Some(usb_tx),
cart_rx: Some(cart_rx),
},
weak_clone,
);
})
.unwrap();
}
async fn handle_connection(
conn: tokio::net::TcpStream,
mut shutdown_rx: tokio::sync::watch::Receiver<()>,
mut usb_rx: tokio::sync::broadcast::Receiver<UsbData>,
usb_tx: tokio::sync::broadcast::Sender<UsbData>,
cart_tx: tokio::sync::broadcast::Sender<UsbData>,
cart_rx: tokio::sync::broadcast::Receiver<UsbData>,
weak: slint::Weak<ui::gui::AppWindow>,
) {
let (mut incoming, mut outgoing) = conn.into_split();
@@ -146,7 +106,7 @@ async fn handle_connection(
if usb_data.data_type == DATATYPE_TCPTEST {
respond_to_handshake(&usb_tx,usb_data.data);
} else if usb_data.data_type == DATATYPE_ROMUPLOAD {
upload_rom(weak.clone(), usb_data.data,&usb_tx,&cart_rx);
panic!("ROM upload not supported");
} else {
cart_tx.send(usb_data).unwrap();
}
@@ -167,9 +127,7 @@ async fn handle_connection(
}
}
pub fn init(
weak: slint::Weak<ui::gui::AppWindow>,
) -> (Option<tokio::sync::watch::Sender<()>>, ui::Usb) {
pub fn init() -> (Option<tokio::sync::watch::Sender<()>>, ui::Usb) {
let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(());
let (usb_tx, usb_rx): (
tokio::sync::broadcast::Sender<UsbData>,
@@ -191,7 +149,7 @@ pub fn init(
tokio::select! {
res = listener.accept() => {
if let Ok((c,_)) = res {
handle_connection(c,shutdown_rx.clone(),usb_rx.resubscribe(),usb_tx.clone(),cart_tx.clone(),cart_rx.resubscribe(),weak.clone()).await;
handle_connection(c,shutdown_rx.clone(),usb_rx.resubscribe(),usb_tx.clone(),cart_tx.clone()).await;
} else {
break;
}
+104 -53
View File
@@ -1,12 +1,43 @@
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
include!(concat!(env!("OUT_DIR"), "/parallel_bindings.rs"));
use crate::{device, ui};
use crate::{device, retroachievements, ui};
const PAL_WIDESCREEN_WIDTH: i32 = 512;
const PAL_STANDARD_WIDTH: i32 = 384;
const PAL_HEIGHT: i32 = 288;
const NTSC_WIDESCREEN_WIDTH: i32 = 426;
const NTSC_STANDARD_WIDTH: i32 = 320;
const NTSC_HEIGHT: i32 = 240;
fn build_gfx_info(device: &mut device::Device) -> GFX_INFO {
GFX_INFO {
RDRAM: device.rdram.mem.as_mut_ptr(),
DMEM: device.rsp.mem.as_mut_ptr(),
RDRAM_SIZE: device.rdram.size,
DPC_CURRENT_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_CURRENT_REG as usize],
DPC_START_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_START_REG as usize],
DPC_END_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_END_REG as usize],
DPC_STATUS_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_STATUS_REG as usize],
PAL: device.cart.pal,
widescreen: device.ui.config.video.widescreen,
fullscreen: device.ui.video.fullscreen,
vsync: if device.netplay.is_none() {
device.ui.config.video.vsync
} else {
false
},
integer_scaling: device.ui.config.video.integer_scaling,
upscale: device.ui.config.video.upscale,
crt: device.ui.config.video.crt,
}
}
pub fn init(device: &mut device::Device) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
ui::ttf_init();
let title = std::ffi::CString::new("gopher64").unwrap();
let window_title = std::ffi::CString::new("gopher64").unwrap();
let mut flags = sdl3_sys::video::SDL_WINDOW_VULKAN
| sdl3_sys::video::SDL_WINDOW_RESIZABLE
@@ -18,23 +49,28 @@ pub fn init(device: &mut device::Device) {
let window_width;
let window_height;
let scale = if device.ui.config.video.upscale > 1 {
device.ui.config.video.upscale as i32
} else {
2
};
if device.cart.pal {
window_width = if device.ui.config.video.widescreen {
1024
PAL_WIDESCREEN_WIDTH * scale
} else {
768
PAL_STANDARD_WIDTH * scale
};
window_height = 576;
window_height = PAL_HEIGHT * scale;
} else {
window_width = if device.ui.config.video.widescreen {
852
NTSC_WIDESCREEN_WIDTH * scale
} else {
640
NTSC_STANDARD_WIDTH * scale
};
window_height = 480;
window_height = NTSC_HEIGHT * scale;
}
device.ui.video.window = unsafe {
sdl3_sys::video::SDL_CreateWindow(title.as_ptr(), window_width, window_height, flags)
sdl3_sys::video::SDL_CreateWindow(window_title.as_ptr(), window_width, window_height, flags)
};
if device.ui.video.window.is_null() {
panic!("Could not create window: {}", unsafe {
@@ -52,37 +88,54 @@ pub fn init(device: &mut device::Device) {
}
unsafe {
sdl3_sys::everything::SDL_HideCursor();
let hint = std::ffi::CString::new("1").unwrap();
sdl3_sys::everything::SDL_SetHint(
sdl3_sys::everything::SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS,
std::ffi::CString::new("1").unwrap().as_ptr(),
hint.as_ptr(),
);
}
let gfx_info = GFX_INFO {
RDRAM: device.rdram.mem.as_mut_ptr(),
DMEM: device.rsp.mem.as_mut_ptr(),
RDRAM_SIZE: device.rdram.size,
DPC_CURRENT_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_CURRENT_REG as usize],
DPC_START_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_START_REG as usize],
DPC_END_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_END_REG as usize],
DPC_STATUS_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_STATUS_REG as usize],
PAL: device.cart.pal,
widescreen: device.ui.config.video.widescreen,
fullscreen: device.ui.video.fullscreen,
integer_scaling: device.ui.config.video.integer_scaling,
upscale: device.ui.config.video.upscale,
crt: device.ui.config.video.crt,
};
let gfx_info = build_gfx_info(device);
unsafe {
let font_bytes = include_bytes!("../../data/Roboto-Regular.ttf");
let font_bytes = include_bytes!("../../data/RobotoMono-Regular.ttf");
rdp_init(
device.ui.video.window as *mut std::ffi::c_void,
gfx_info,
font_bytes.as_ptr() as *const std::ffi::c_void,
font_bytes.len(),
device.ui.storage.save_state_slot,
)
}
fps_counter(&mut device.ui);
}
fn fps_counter(ui: &mut ui::Ui) {
let mut fps_rx = ui.video.fps_rx.take().unwrap();
let mut vis_rx = ui.video.vis_rx.take().unwrap();
tokio::spawn(async move {
loop {
let mut fps: u32 = 0;
let mut vis: u32 = 0;
loop {
match fps_rx.try_recv() {
Ok(_fps_update) => fps += 1,
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => return,
}
}
loop {
match vis_rx.try_recv() {
Ok(_vis_update) => vis += 1,
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => return,
}
}
unsafe { rdp_set_fps(fps, vis) };
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
});
}
pub fn close(ui: &ui::Ui) {
@@ -109,21 +162,7 @@ pub fn save_state(rdp_state: *mut u8) {
}
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(),
RDRAM_SIZE: device.rdram.size,
DPC_CURRENT_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_CURRENT_REG as usize],
DPC_START_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_START_REG as usize],
DPC_END_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_END_REG as usize],
DPC_STATUS_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_STATUS_REG as usize],
PAL: device.cart.pal,
widescreen: device.ui.config.video.widescreen,
fullscreen: device.ui.video.fullscreen,
integer_scaling: device.ui.config.video.integer_scaling,
upscale: device.ui.config.video.upscale,
crt: device.ui.config.video.crt,
};
let gfx_info = build_gfx_info(device);
unsafe {
rdp_new_processor(gfx_info);
rdp_load_state(rdp_state);
@@ -139,6 +178,7 @@ pub fn pause_loop(frame_time: f64) {
while paused && !frame_advance {
std::thread::sleep(std::time::Duration::from_secs_f64(frame_time));
unsafe { sdl3_sys::events::SDL_PumpEvents() };
retroachievements::do_idle();
let callback = unsafe { rdp_check_callback() };
paused = callback.paused;
frame_advance = callback.frame_advance;
@@ -155,6 +195,18 @@ pub fn check_callback(device: &mut device::Device) -> (bool, bool) {
} else if callback.load_state {
device.load_state = true;
}
if callback.reset_game {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] |=
device::cop0::COP0_CAUSE_IP4;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
!device::cop0::COP0_CAUSE_EXCCODE_MASK;
device::events::create_event(
device,
device::events::EVENT_TYPE_NMI,
device.cpu.clock_rate, // 1 second
);
}
if device.vi.enable_speed_limiter != callback.enable_speedlimiter {
speed_limiter_toggled = true;
device.vi.enable_speed_limiter = callback.enable_speedlimiter;
@@ -162,9 +214,9 @@ pub fn check_callback(device: &mut device::Device) -> (bool, bool) {
}
if device.ui.storage.save_state_slot != callback.save_state_slot {
ui::video::onscreen_message(
&device.ui,
&format!("Switching savestate slot to {}", callback.save_state_slot,),
onscreen_message(
&format!("Switching savestate slot to {}", callback.save_state_slot),
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
device.ui.storage.save_state_slot = callback.save_state_slot;
device
@@ -172,7 +224,7 @@ pub fn check_callback(device: &mut device::Device) -> (bool, bool) {
.storage
.paths
.savestate_file_path
.set_extension("state".to_owned() + callback.save_state_slot.to_string().as_str());
.set_extension(format!("state{}", callback.save_state_slot));
}
if callback.lower_volume {
ui::audio::lower_audio_volume(&mut device.ui);
@@ -196,8 +248,11 @@ pub fn check_framebuffers(address: u32, length: u32) {
unsafe { rdp_check_framebuffers(address, length) }
}
pub fn onscreen_message(_ui: &ui::Ui, message: &str) {
unsafe { rdp_onscreen_message(std::ffi::CString::new(message).unwrap().as_ptr()) };
pub fn onscreen_message(message: &str, milliseconds: MESSAGE_LENGTH) {
unsafe {
let c_message = std::ffi::CString::new(message).unwrap();
rdp_onscreen_message(c_message.as_ptr(), milliseconds)
};
}
pub fn draw_text(
@@ -210,12 +265,8 @@ pub fn draw_text(
let (mut w, mut h) = (0, 0);
sdl3_sys::render::SDL_GetRenderOutputSize(renderer, &mut w, &mut h);
let ttf_text = sdl3_ttf_sys::ttf::TTF_CreateText(
text_engine,
font,
std::ffi::CString::new(text).unwrap().as_ptr(),
0,
);
let c_text = std::ffi::CString::new(text).unwrap();
let ttf_text = sdl3_ttf_sys::ttf::TTF_CreateText(text_engine, font, c_text.as_ptr(), 0);
sdl3_sys::everything::SDL_RenderClear(renderer);
sdl3_ttf_sys::ttf::TTF_DrawRendererText(ttf_text, 20.0, h as f32 / 2.0);
+32 -39
View File
@@ -1,55 +1,48 @@
use crate::gui;
use crate::retroachievements;
use crate::ui::gui;
use slint::ComponentHandle;
pub fn prompt_for_match(
words: &[String],
weak_vru: &slint::Weak<gui::AppWindow>,
frame_time: f64,
) -> u16 {
pub fn prompt_for_match(words: &[String], frame_time: f64) -> u16 {
let mut dedup_words = words.to_owned();
dedup_words.sort();
dedup_words.dedup();
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
weak_vru
.upgrade_in_event_loop(move |_handle| {
let vru_dialog = gui::VruDialog::new().unwrap();
let vru_dialog_weak = vru_dialog.as_weak();
let vru_dialog = gui::VruDialog::new().unwrap();
let vru_dialog_weak = vru_dialog.as_weak();
let tx_clicked = tx.clone();
vru_dialog.on_vru_button_clicked(move |chosen_word| {
tx.try_send(chosen_word.to_string()).unwrap();
vru_dialog_weak.unwrap().window().hide().unwrap();
});
vru_dialog.on_vru_button_clicked(move |chosen_word| {
tx_clicked.try_send(chosen_word.to_string()).unwrap();
vru_dialog_weak.unwrap().window().hide().unwrap();
});
vru_dialog.set_words(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(
dedup_words
.iter()
.map(|x| x.into())
.collect::<Vec<slint::SharedString>>(),
),
)));
vru_dialog.window().on_close_requested(move || {
tx.try_send("".to_string()).unwrap();
slint::CloseRequestResponse::HideWindow
});
let timer = slint::Timer::default();
timer.start(
slint::TimerMode::Repeated,
std::time::Duration::from_secs_f64(frame_time),
move || {
unsafe { sdl3_sys::events::SDL_PumpEvents() }; // so the OS doesn't complain about the game window being frozen
retroachievements::do_idle();
},
);
let words_vec = slint::VecModel::default();
for word in dedup_words {
words_vec.push(word.into());
vru_dialog.run().unwrap();
if let Ok(result) = rx.try_recv() {
for (i, v) in words.iter().enumerate() {
if *v == result {
return i as u16;
}
let words_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
std::rc::Rc::new(words_vec);
vru_dialog.set_words(slint::ModelRc::from(words_model));
vru_dialog.show().unwrap();
})
.unwrap();
let mut result = rx.try_recv();
while result.is_err() {
result = rx.try_recv();
std::thread::sleep(std::time::Duration::from_secs_f64(frame_time));
unsafe { sdl3_sys::events::SDL_PumpEvents() }; // so the OS doesn't complain about the game window being frozen
}
for (i, v) in words.iter().enumerate() {
if *v == *result.as_ref().unwrap() {
return i as u16;
}
}
0x7FFF