Compare commits

...

283 Commits

Author SHA1 Message Date
Logan McNaughton f793eb06be bump to 1.1.20 (#849) 2026-05-07 06:49:49 +00:00
Logan McNaughton 61532f7b45 move sdl init (#848)
* move sdl init

* more

* more

* more

* more
2026-05-06 20:09:21 +00:00
Logan McNaughton a69a74bf37 cleanup some if statements (#846) 2026-05-06 11:57:04 +00:00
Logan McNaughton 6d4d5468f9 save rom dir (#845) 2026-05-06 11:17:28 +00:00
Logan McNaughton 7015d44da7 fix ghost image with SSAA (#844) 2026-05-06 11:05:48 +00:00
Logan McNaughton 172867527b add option for SSAA (#843) 2026-05-06 09:52:13 +00:00
Logan McNaughton 323e8cf1f0 move sdl quit calls (#842)
* move sdl quit calls

* more
2026-05-06 08:59:05 +00:00
Logan McNaughton 172d10d7cb update controllers when clicking on controller config (#841)
* update controllers when clicking on controller config

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-05-06 07:44:39 +00:00
Logan McNaughton 945d456f9c disable fps display on close (#839)
* disable fps display on close

* another check
2026-05-04 12:52:25 +00:00
Logan McNaughton a4f635265b guards around postcard (#838) 2026-05-04 11:52:46 +00:00
Logan McNaughton f45f951af8 fast forward controller hotkey (#837)
* fast forward controller hotkey

* more
2026-05-04 08:59:20 +00:00
Logan McNaughton 883852a828 more error messages (#836) 2026-05-04 08:36:50 +00:00
Logan McNaughton 90052257bb Use eprintln (#835)
* use eprintln

* more

* more
2026-05-04 07:49:38 +00:00
Logan McNaughton 1184d3d77a better error message (#834) 2026-05-03 14:22:50 +00:00
Logan McNaughton 12280bb35b more crash protection (#832)
* more crash protection

* more

* more
2026-05-03 14:02:17 +00:00
Logan McNaughton 17f3f2dc63 more crash protection (#833)
* more crash protection

* more
2026-05-03 13:59:59 +00:00
Logan McNaughton 3d6ca17a9a more crash protection (#831) 2026-05-03 13:11:08 +00:00
Logan McNaughton 01863a70a8 avoid some controller crashes (#830)
* avoid some controller crashes

* more

* more
2026-05-03 07:50:13 +00:00
Logan McNaughton 801cc1bc74 cargo update (#827) 2026-05-03 05:49:27 +00:00
Logan McNaughton 9ed8d835c0 add merge_group to workflow (#828) 2026-05-03 05:49:07 +00:00
Logan McNaughton b754891d45 ensure audio is playing (#826)
* ensure audio is playing

* more
2026-05-03 07:18:28 +02:00
Logan McNaughton 9cc6d48493 ensure file dialog order for transferpak (#824)
* ensure file dialog order for transferpak

* more
2026-05-02 22:13:59 +02:00
Logan McNaughton b865538d6e avoid panic with invalid cheat (#823)
* avoid panic with invalid cheat

* more

* more
2026-05-02 17:59:51 +02:00
Logan McNaughton abacaddf8a avoid crash on missing GB files (#822)
* avoid crash on missing GB files

* more

* more
2026-05-02 17:06:56 +02:00
Logan McNaughton b86c5daa2c add controller hotkeys (#821)
* add controller hotkeys

* more

* more

* more

* more

* more

* more
2026-05-02 09:04:42 +02:00
Logan McNaughton df8d7d9ff0 allow single controller mpk files (#820) 2026-05-01 21:32:08 +02:00
Logan McNaughton 8c7d3d4893 protect against null ptr dereference (#819)
* protect against null ptr dereference

* more
2026-05-01 20:44:14 +02:00
Logan McNaughton b88f041bd8 don't check for update inside itch app (#817) 2026-05-01 17:42:07 +02:00
Logan McNaughton e860b96bd8 Controller code cleanup (#816)
* controller hotkeys

* more

* more

* more

* more

* more
2026-04-28 08:37:59 +02:00
Logan McNaughton b2ef544513 handle controller reconnection (#815) 2026-04-27 16:38:20 +02:00
dependabot[bot] ca00ee1a42 Bump apple-actions/import-codesign-certs from 6 to 7 (#814)
Bumps [apple-actions/import-codesign-certs](https://github.com/apple-actions/import-codesign-certs) from 6 to 7.
- [Release notes](https://github.com/apple-actions/import-codesign-certs/releases)
- [Commits](https://github.com/apple-actions/import-codesign-certs/compare/v6...v7)

---
updated-dependencies:
- dependency-name: apple-actions/import-codesign-certs
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 05:59:33 +02:00
Logan McNaughton 511d8461c2 add note about modifying profile (#812) 2026-04-26 17:46:12 +02:00
Logan McNaughton fe42efadfa limit frequent audio adjustments (#810) 2026-04-25 17:05:54 +02:00
Logan McNaughton c1e73320b1 adjust audio buffer management (#809)
* adjust audio buffer management

* more

* more
2026-04-25 16:05:28 +02:00
Logan McNaughton 47dd6d61b0 update setup-butler (#808) 2026-04-25 11:20:56 +02:00
Logan McNaughton 212557610b use bash shell (#807) 2026-04-25 07:53:30 +02:00
Logan McNaughton 5e6a7731f4 update to 1.1.19 (#806)
* update to 1.1.19

* more
2026-04-25 07:22:58 +02:00
Logan McNaughton 65c63dfc5d Always set user agent (#805)
* Always set user agent

* more
2026-04-25 07:15:47 +02:00
Logan McNaughton ae4e7cb01f Update download links (#804) 2026-04-24 22:12:24 +02:00
Logan McNaughton a233c72dad add itch.io link (#803) 2026-04-24 10:54:47 +02:00
Logan McNaughton b267b83248 upload to itch.io (#802) 2026-04-24 10:23:28 +02:00
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
Logan McNaughton 0aab9f71e1 cargo update 2026-01-29 21:58:06 +01:00
Logan McNaughton 7f84eb2aff add missing gamepad button types 2026-01-26 21:44:03 +01:00
Logan McNaughton b460d55714 use SDL_HAPTIC_INFINITY 2026-01-25 13:35:59 +01:00
Logan McNaughton ceaf56c563 bump to 1.1.13 2026-01-22 21:00:26 +01:00
Logan McNaughton 5937922558 fix issue with emux_xdetect 2026-01-22 20:44:17 +01:00
Logan McNaughton e9e71a6e02 Update to rust 1.93.0 (#659) 2026-01-22 20:24:41 +01:00
Logan McNaughton 9775f103e0 flush saves to disk (#658)
* flush saves to disk

* more

* more

* more

* more

* more

* more

* more

* more
2026-01-22 20:09:13 +01:00
Logan McNaughton bf01898c6f remove extra function 2026-01-22 09:22:47 +01:00
Logan McNaughton ce3b12bba9 print message for audio volume 2026-01-21 19:01:50 +01:00
Logan McNaughton 56ff2b260c adjust audio ratios 2026-01-21 12:13:19 +01:00
Logan McNaughton 367c22877b add system for micro adjustments to audio pitch 2026-01-21 11:47:33 +01:00
Logan McNaughton befbacc5b3 remove un-needed var 2026-01-20 15:31:03 +01:00
Logan McNaughton e1a6663bb5 remove needless copy 2026-01-20 13:21:44 +01:00
Logan McNaughton c5185e60c6 tweak ai dma calculation 2026-01-20 13:19:14 +01:00
Logan McNaughton 1aa879d759 tweak audio queue formula 2026-01-20 13:05:32 +01:00
Logan McNaughton bfad71f09f Update parallel-rdp (#656)
* Update parallel-rdp

* update cargo
2026-01-19 13:32:06 +01:00
Logan McNaughton 431b787998 update rfd 2026-01-05 19:27:16 +01:00
Logan McNaughton d48e3dc106 update rfd 2026-01-05 09:54:42 +01:00
Logan McNaughton 8be1ac7740 update SDL crate (#648)
* update SDL crate

* more
2026-01-03 10:07:27 +01:00
Logan McNaughton 56d0b1b2e2 update sse2neon to v1.9.1 2026-01-02 14:41:00 +01:00
Logan McNaughton 7f63bf1fde Update sse2neon to v1.9.0 (#642) 2025-12-22 10:08:09 +01:00
Logan McNaughton 05be960358 cargo update 2025-12-21 16:23:23 +01:00
dependabot[bot] 38a5079acf Bump actions/upload-artifact from 5 to 6 (#641)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 06:36:17 +01:00
Logan McNaughton 144e31c7fc bump to 1.1.12 2025-12-11 22:08:08 +01:00
Logan McNaughton e7a21171f4 fix windows build (#636) 2025-12-11 21:48:28 +01:00
Logan McNaughton 413da0178e update parallel rdp 2025-12-11 17:57:59 +01:00
Logan McNaughton d8c6669da1 Update to Rust 1.92.0 (#634) 2025-12-11 17:46:50 +01:00
Logan McNaughton c6fcfddf8c some cleanup 2025-12-10 15:12:56 +01:00
Logan McNaughton 0aa5c3e63a cleanup variable 2025-12-10 12:35:22 +01:00
Logan McNaughton 6f8433138d clippy fixes (#632)
* clippy fixes

* fix netplay browser
2025-12-10 10:00:42 +01:00
Logan McNaughton ea5bbfb218 only update controller assignment after user input (#629) 2025-12-06 13:42:10 +01:00
Logan McNaughton 10af598223 cargo update 2025-11-26 16:48:39 +01:00
dependabot[bot] c08511adc5 Bump actions/checkout from 5 to 6 (#622)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 04:24:48 +01:00
Logan McNaughton f116617ac3 remove redundant SDL_PumpEvents 2025-11-22 14:31:08 +01:00
Logan McNaughton 1141095b65 some code cleanup 2025-11-22 14:14:22 +01:00
Logan McNaughton 162a6df56d move if statement for better performance 2025-11-22 13:40:26 +01:00
Logan McNaughton 72f47e5aff addressing hack to fix compatibility with latest libdragon (#620) 2025-11-22 10:04:31 +01:00
Logan McNaughton 6d40f5a12d cargo update 2025-11-21 10:44:52 +01:00
Logan McNaughton 64e6d75db1 Update Granite (#618)
* Update Granite

* add file
2025-11-21 10:44:09 +01:00
Logan McNaughton 20761faf8c rust 1.91.1 2025-11-11 12:43:54 +01:00
Adam Steinberg 79eb34d83d unsolicited main.rs refactor (#614)
* main.rs refactor

* remove anyhow

* forgive me clippy, for I have sinned
2025-11-08 21:34:35 +01:00
Logan McNaughton c4057c2f21 bump to 1.1.11 2025-11-05 15:29:56 +01:00
Logan McNaughton 581cc0b9aa cargo update 2025-11-05 15:16:57 +01:00
Logan McNaughton 6f6e21c718 tweak build options 2025-11-05 15:12:12 +01:00
Logan McNaughton 181774a1de pass netplay_id to workflow 2025-11-04 15:38:30 +01:00
Logan McNaughton 002fbbe8a4 set netplay id from environment (#606) 2025-11-04 08:27:32 +01:00
Logan McNaughton ab630cc8cb Use skia backend for gui (#605)
* Use skia backend for gui

* more

* use clang-cl

* more

* more
2025-11-03 23:07:10 +01:00
Logan McNaughton 06f9c56a79 bump to 1.1.10 2025-10-30 22:52:34 +01:00
Logan McNaughton c3f4b11519 update sdl3 2025-10-30 22:47:45 +01:00
Logan McNaughton 921a0d5362 Rust 1.91 (#599) 2025-10-30 20:14:39 +01:00
Logan McNaughton 24fc5b9638 update parallel-rdp 2025-10-30 13:59:20 +01:00
dependabot[bot] 1559856483 Bump signpath/github-action-submit-signing-request from 1.3 to 2.0 (#597)
Bumps [signpath/github-action-submit-signing-request](https://github.com/signpath/github-action-submit-signing-request) from 1.3 to 2.0.
- [Release notes](https://github.com/signpath/github-action-submit-signing-request/releases)
- [Commits](https://github.com/signpath/github-action-submit-signing-request/compare/v1.3...v2.0)

---
updated-dependencies:
- dependency-name: signpath/github-action-submit-signing-request
  dependency-version: '2.0'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 08:36:02 +01:00
dependabot[bot] 1deb91fc1e Bump actions/upload-artifact from 4 to 5 (#596)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 06:01:31 +01:00
Logan McNaughton 726a58f854 cargo update 2025-10-26 13:45:56 +01:00
Logan McNaughton fe0297690f pump events during vi (#593)
* pump events during vi

* add var

* more

* more
2025-10-26 13:42:25 +01:00
Logan McNaughton 847df61526 Add pause between key bindings (#594) 2025-10-26 13:33:40 +01:00
Logan McNaughton 6de8128f5b cargo update 2025-10-12 12:30:57 +02:00
Logan McNaughton 6c09176d12 add .srm for GB saves 2025-10-11 19:32:35 +02:00
Logan McNaughton 2050098735 fix out of bounds access (#590) 2025-10-06 09:02:50 +02:00
Logan McNaughton d3ecc37da6 fix compare interrupt issue (#588) 2025-10-01 10:05:03 +02:00
Logan McNaughton a0237e9f7d bump to 1.1.9 2025-09-28 10:00:37 +02:00
Logan McNaughton d6d608b674 Revert "more audio cleanup"
This reverts commit f3271cba63.
2025-09-28 09:59:02 +02:00
Logan McNaughton f254bec968 bump to 1.1.8 2025-09-27 21:33:21 +02:00
Logan McNaughton bd4aff2b2c cargo update 2025-09-27 21:32:27 +02:00
Logan McNaughton 0bf70a77ea correct load block size 2025-09-26 18:52:27 +02:00
Logan McNaughton e23e7f33c1 Re-add wgpu
I didn't see any evidence of crashing
2025-09-26 07:23:48 +02:00
Logan McNaughton 9d0516545e reorder items 2025-09-25 16:14:48 +02:00
Logan McNaughton e3023a902f further depth buffer tracking tweaks (#583) 2025-09-25 15:59:58 +02:00
Logan McNaughton 431c7c2a8d cargo update 2025-09-25 13:32:23 +02:00
Logan McNaughton cf99539f85 remove wgpu (#576) 2025-09-25 07:36:23 +02:00
Logan McNaughton c2493b829e watch z-buffer reads as well (#581) 2025-09-24 23:00:39 +02:00
Logan McNaughton 2f25de55d7 change depth buffer tracking (#580) 2025-09-24 20:06:39 +02:00
Logan McNaughton 116ffe9845 fix clippy warning 2025-09-22 09:15:41 +02:00
Logan McNaughton f3271cba63 more audio cleanup 2025-09-21 18:36:03 +02:00
Logan McNaughton 9037680cda cleanup audio code (#579) 2025-09-21 16:30:11 +02:00
Logan McNaughton fc41e26a6d expose 8x resolution (#578) 2025-09-21 13:36:13 +02:00
Logan McNaughton d1f28bb7a8 optimize rdp_check_framebuffers (#577) 2025-09-21 10:43:49 +02:00
Logan McNaughton 9db831fdef disable repeat mode on MI_CLR_INIT 2025-09-20 17:21:06 +02:00
Logan McNaughton 004b603e96 support basic mi repeat mode (#575)
* support basic mi repeat mode

* more

* more
2025-09-20 17:09:30 +02:00
Logan McNaughton 8661e2c9b1 always enable lto 2025-09-20 08:37:55 +02:00
Logan McNaughton c712f2a288 bump to 1.1.7 2025-09-19 12:57:05 +02:00
Logan McNaughton 366162e6fe cargo update 2025-09-19 12:56:07 +02:00
Logan McNaughton 2b93fb8aa1 re-add framebuffer tracking (#573) 2025-09-19 11:32:48 +02:00
Logan McNaughton a191701610 update to rust 1.90 (#574)
* update to rust 1.90

* more
2025-09-18 22:46:22 +02:00
Logan McNaughton 0605967c28 modify rsp sync points 2025-09-16 11:39:23 +02:00
Logan McNaughton d6e6a16814 reset device.rdp.last_status_value 2025-09-16 10:02:07 +02:00
Logan McNaughton e374825d5e add release-with-debug profile 2025-09-15 19:23:21 +02:00
Logan McNaughton f6d6746207 use addr_masked 2025-09-15 18:32:25 +02:00
Logan McNaughton 100238f198 optimize some reads/writes (#571) 2025-09-15 18:26:45 +02:00
Logan McNaughton daee53a626 Optimize VU instructions (#570) 2025-09-15 15:50:28 +02:00
Logan McNaughton 018d8ca3e3 fix parallel-rdp build 2025-09-15 12:28:54 +02:00
Logan McNaughton 24d26d864e update parallel-rdp 2025-09-15 12:21:26 +02:00
Logan McNaughton 7b8c549e8e remove rdp sync speed hack (#567) 2025-09-15 09:50:23 +02:00
Logan McNaughton f6c7e3d3c0 use wgpu for gui 2025-09-12 14:16:46 +02:00
Logan McNaughton 374b01dccf cargo update 2025-09-12 13:58:21 +02:00
Logan McNaughton 569b26a067 panic on attempted use of mi repeat mode 2025-09-10 13:16:57 +02:00
Logan McNaughton 380d196ae7 cargo update 2025-09-05 10:54:03 +02:00
Logan McNaughton 4fa1a8ad39 remove redundant get_rooms netplay call (#563)
* remove redundant get_rooms netplay call

* unwrap
2025-09-05 09:32:17 +02:00
Logan McNaughton 8acf3b9de8 bump to 1.1.6 2025-08-24 10:58:58 +02:00
Logan McNaughton b3297f82b2 cargo update 2025-08-24 10:58:01 +02:00
Logan McNaughton 2127705e7b set deadzon max to 99 2025-08-24 07:29:07 +02:00
Logan McNaughton cccb079d63 cleanup deadzone code (#558) 2025-08-24 00:05:03 +02:00
Logan McNaughton c52b680753 use saturating abs for deadzone (#557) 2025-08-23 22:52:04 +02:00
Logan McNaughton 8799650245 add deadzone option (#554)
* add deadzone option

* add deadzone calc

* more
2025-08-23 15:36:03 +02:00
Logan McNaughton 90be2130c1 move cheats back 2025-08-23 09:36:47 +02:00
Logan McNaughton 96af33c434 Split render frame and update screen, move pause loop (#556)
* split render and update screen functions

* move pause loop
2025-08-23 08:54:47 +02:00
Logan McNaughton 0a42152bcb move cheats 2025-08-22 22:42:09 +02:00
Logan McNaughton db335e24d2 render frame before speed limiter (#555) 2025-08-22 07:59:28 +02:00
Logan McNaughton 4857b86897 bump to 1.1.5 (#553) 2025-08-21 15:22:19 +02:00
82 changed files with 7837 additions and 4409 deletions
+10 -3
View File
@@ -1,16 +1,23 @@
[target.'cfg(target_os = "linux")']
[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:
+132 -32
View File
@@ -4,14 +4,16 @@ permissions:
on:
push:
branches: ["main"]
branches: [ "main" ]
tags:
- "v*"
pull_request:
branches: ["main"]
branches: [ "main" ]
merge_group:
env:
CARGO_TERM_COLOR: always
RA_HARDCORE: ${{ github.ref_type == 'tag' && 'true' || 'false' }}
jobs:
build-linux:
@@ -29,42 +31,60 @@ jobs:
arch: linux-aarch64
llvm: ARM64
steps:
- uses: actions/checkout@v5
- 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
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 }}
mkdir output
cp target/${{ matrix.target }}/release/gopher64 output/gopher64-${{ matrix.arch }}
env:
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
- name: Upload file
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: gopher64-${{ matrix.arch }}
path: output/gopher64-${{ matrix.arch }}
archive: false
- name: Setup butler
if: ${{ github.ref_type == 'tag' }}
uses: remarkablegames/setup-butler@v3
- name: Upload to itch.io
if: ${{ github.ref_type == 'tag' }}
run: |
butler push output/gopher64-${{ matrix.arch }} loganmc10/gopher64:${{ matrix.arch }} --userversion ${GITHUB_REF_NAME#v}
env:
BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }}
build-windows:
runs-on: ${{ matrix.os }}
@@ -76,18 +96,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@v5
- 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() }
@@ -96,21 +118,28 @@ 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 }}
mkdir output
cp target/${{ matrix.target }}/release/gopher64.exe output/gopher64-${{ matrix.arch }}.exe
env:
CC: clang-cl
CXX: clang-cl
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
- name: Upload unsigned artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v4
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@v1.3
uses: signpath/github-action-submit-signing-request@v2
with:
api-token: "${{ secrets.SIGNPATH_API_TOKEN }}"
organization-id: "${{ secrets.SIGNPATH_ORG_ID }}"
@@ -118,7 +147,27 @@ 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
- name: Setup butler
if: ${{ github.ref_type == 'tag' }}
uses: remarkablegames/setup-butler@v3
- name: Upload to itch.io
if: ${{ github.ref_type == 'tag' }}
shell: bash
run: |
butler push output-signed/gopher64-${{ matrix.arch }}.exe loganmc10/gopher64:${{ matrix.arch }} --userversion ${GITHUB_REF_NAME#v}
env:
BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }}
build-macos:
runs-on: macos-15
@@ -126,27 +175,78 @@ jobs:
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@v5
- 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.0"
- 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@v7
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@v4
uses: actions/upload-artifact@v7
with:
name: gopher64-${{ matrix.arch }}
path: output/gopher64-${{ matrix.arch }}
path: target/${{ matrix.target }}/release/bundle/osx
- name: Setup butler
if: ${{ github.ref_type == 'tag' }}
uses: remarkablegames/setup-butler@v3
- name: Upload to itch.io
if: ${{ github.ref_type == 'tag' }}
run: |
butler push target/${{ matrix.target }}/release/bundle/osx/Gopher64.app loganmc10/gopher64:${{ matrix.arch }} --userversion ${GITHUB_REF_NAME#v}
env:
BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }}
+17 -12
View File
@@ -2,9 +2,10 @@ name: Rust-lint
on:
push:
branches: ["main"]
branches: [ "main" ]
pull_request:
branches: ["main"]
branches: [ "main" ]
merge_group:
permissions:
contents: read
@@ -16,33 +17,37 @@ jobs:
lint-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions-rust-lang/setup-rust-toolchain@v1
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
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
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
checks: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: rustsec/audit-check@v2
+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
+2455 -1604
View File
File diff suppressed because it is too large Load Diff
+32 -16
View File
@@ -1,47 +1,63 @@
[package]
name = "gopher64"
version = "1.1.4"
version = "1.1.20"
edition = "2024"
rust-version = "1.89"
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 = "4.3"
governor = "0.10"
sevenz-rust2 = { version = "0.17", default-features = false }
zip = "8.0"
sevenz-rust2 = "0.21"
chrono = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-big-array = "0.5"
slint = { version = "1.12", default-features = false, features = ["compat-1-2", "std", "backend-winit", "renderer-femtovg"] }
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"
sdl3-sys = { version = "0.5", features = ["build-from-source-static"] }
sdl3-ttf-sys = { version = "0.2", features = ["build-static-vendored", "no-sdlttf-harfbuzz", "no-sdlttf-plutosvg"] }
rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "tokio"] }
tokio = {version = "1.46", features = ["rt-multi-thread", "macros"] }
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 = {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.27"
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.12", default-features = false, features = ["http2","rustls-tls","charset","json"] }
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]
panic = "abort"
lto = "thin"
strip = true
[profile.release-with-debug]
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.0"
+28 -7
View File
@@ -1,9 +1,28 @@
# 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
<a href="https://loganmc10.itch.io/gopher64"><img src="https://static.itch.io/images/badge.svg" width="200" ></a>
Linux: https://flathub.org/apps/io.github.gopher64.gopher64
Windows:
* Standalone executable: [gopher64-windows-x86_64.exe](https://github.com/gopher64/gopher64/releases/latest/download/gopher64-windows-x86_64.exe)
MacOS:
* App Bundle: [gopher64-macos-aarch64.zip](https://github.com/gopher64/gopher64/releases/latest/download/gopher64-macos-aarch64.zip)
* Homebrew: `brew install --cask gopher64`
Linux:
* Standalone executable: [gopher64-linux-x86_64](https://github.com/gopher64/gopher64/releases/latest/download/gopher64-linux-x86_64)
* Flatpak: `flatpak install flathub io.github.gopher64.gopher64`
## wiki
@@ -11,7 +30,7 @@ https://github.com/gopher64/gopher64/wiki
## discord
https://discord.gg/9RGXq8W8JQ
[Discord invite link](https://discord.gg/9RGXq8W8JQ)
## controls
@@ -19,7 +38,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 +54,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 +67,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).
+138 -21
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,7 +40,6 @@ 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")
@@ -62,20 +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 profile = std::env::var("PROFILE").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")
@@ -84,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");
@@ -93,31 +132,86 @@ 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");
}
if profile == "release" {
volk_build.flag("-flto=thin");
rdp_build.flag("-flto=thin");
simd_build.flag("-flto=thin");
}
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")
.allowlist_function("rdp_close")
.allowlist_function("rdp_set_vi_register")
.allowlist_function("rdp_update_screen")
.allowlist_function("rdp_render_frame")
.allowlist_function("rdp_process_commands")
.allowlist_function("rdp_onscreen_message")
.allowlist_function("rdp_check_callback")
.allowlist_function("rdp_new_processor")
.allowlist_function("rdp_check_framebuffers")
.allowlist_function("rdp_state_size")
.allowlist_function("rdp_save_state")
.allowlist_function("rdp_load_state")
.allowlist_function("rdp_set_fps")
.allowlist_function("get_joystick_event")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
@@ -162,6 +256,7 @@ fn main() {
.allowlist_function("_mm_cmplt_epi16")
.allowlist_function("_mm_subs_epu16")
.allowlist_function("_mm_set1_epi32")
.allowlist_function("_mm_set1_epi16")
.blocklist_type("__m128i")
.blocklist_type("int64x2_t")
.wrap_static_fns(true)
@@ -183,13 +278,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:rustc-env=N64_STACK_SIZE={}", 8 * 1024 * 1024);
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: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>
+926 -577
View File
File diff suppressed because it is too large Load Diff
+67 -41
View File
@@ -4,51 +4,77 @@
#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 ssaa;
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();
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);
size_t rdp_state_size();
void rdp_save_state(uint8_t *state);
void rdp_load_state(const uint8_t *state);
typedef struct {
uint32_t joystick_id;
bool connected;
} JoystickEvent;
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);
JoystickEvent get_joystick_event();
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.89.0"
channel = "1.95.0"
+30 -15
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() {
@@ -40,7 +48,8 @@ pub fn init(
{
option_value = Some(found_option_value.to_string());
} else {
panic!("Cheat option: {option} not found");
eprintln!("Cheat option: {option} not found");
continue;
}
}
@@ -57,21 +66,27 @@ pub fn init(
}
let mut split = result.split_whitespace();
let first_part = u32::from_str_radix(split.next().unwrap(), 16).unwrap();
decoded_cheat.push(DecodedCheat {
code_type: (first_part >> 24) as u8,
address: first_part & 0x00FFFFFF,
data: u16::from_str_radix(split.next().unwrap(), 16).unwrap(),
});
if let Ok(data) = u16::from_str_radix(split.next().unwrap(), 16) {
decoded_cheat.push(DecodedCheat {
code_type: (first_part >> 24) as u8,
address: first_part & 0x00FFFFFF,
data,
})
} else {
eprintln!("Could not parse data for: {}", cheat_setting.0);
};
}
if !decoded_cheat.is_empty() {
device.cheats.cheats.push(decoded_cheat);
}
device.cheats.cheats.push(decoded_cheat);
} else {
println!("Could not find cheat: {}", cheat_setting.0);
eprintln!("Could not find cheat: {}", cheat_setting.0);
}
}
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);
}
}
+37 -26
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::audio::init(device);
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() {
@@ -76,32 +82,36 @@ 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);
ui::storage::format_saves(device);
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) {
let mut rng_seed = set_rng().next_u64();
if device.netplay.is_some() {
let netplay = device.netplay.as_mut().unwrap();
if let Some(netplay) = &mut device.netplay {
if netplay.player_number == 0 {
netplay::send_rng(netplay, rng_seed);
} else {
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>> {
@@ -132,7 +142,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() {
@@ -149,7 +163,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();
@@ -174,7 +192,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() {
@@ -188,7 +207,7 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
pub struct Device {
#[serde(skip)]
pub netplay: Option<netplay::Netplay>,
#[serde(skip, default = "ui::Ui::default")]
#[serde(skip, default = "ui::Ui::new")]
pub ui: ui::Ui,
pub byte_swap: usize,
pub save_state: bool,
@@ -207,9 +226,8 @@ 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 vru_window: controller::vru::VruWindow,
pub transferpaks: [controller::transferpak::TransferPak; 4],
pub cheats: cheats::Cheats,
}
@@ -239,7 +257,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![
@@ -474,19 +491,13 @@ 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,
limit_freq_check: std::time::Instant::now(),
},
vru_window: controller::vru::VruWindow {
window_notifier: None,
word_receiver: None,
},
vru: controller::vru::Vru {
status: 0,
voice_state: 0,
+6 -5
View File
@@ -3,12 +3,12 @@ use crate::{device, ui};
const AI_DRAM_ADDR_REG: u32 = 0;
const AI_LEN_REG: u32 = 1;
// const AI_CONTROL_REG: u32 = 2;
const AI_STATUS_REG: u32 = 3;
pub const AI_STATUS_REG: u32 = 3;
const AI_DACRATE_REG: u32 = 4;
// const AI_BITRATE_REG: u32 = 5;
pub const AI_REGS_COUNT: u32 = 6;
const AI_STATUS_BUSY: u32 = 0x40000000;
pub const AI_STATUS_BUSY: u32 = 0x40000000;
const AI_STATUS_FULL: u32 = 0x80000000;
#[derive(serde::Serialize, serde::Deserialize)]
@@ -45,11 +45,10 @@ fn get_remaining_dma_length(device: &device::Device) -> u64 {
}
fn get_dma_duration(device: &device::Device) -> u64 {
let samples_per_sec = device.vi.clock / (1 + device.ai.regs[AI_DACRATE_REG as usize]) as u64;
let bytes_per_sample = 4; /* XXX: assume 16bit stereo - should depends on bitrate instead */
let length = (device.ai.regs[AI_LEN_REG as usize] & !7) as u64;
length * (device.cpu.clock_rate / (bytes_per_sample * samples_per_sec))
(length * device.cpu.clock_rate) / (bytes_per_sample * device.ai.freq)
}
fn do_dma(device: &mut device::Device) {
@@ -88,6 +87,7 @@ fn fifo_push(device: &mut device::Device) {
device.ai.regs[AI_STATUS_REG as usize] |= AI_STATUS_BUSY;
do_dma(device);
ui::audio::resume_game_audio(&mut device.ui);
}
}
@@ -102,6 +102,7 @@ fn fifo_pop(device: &mut device::Device) {
} else {
device.ai.regs[AI_STATUS_REG as usize] &= !AI_STATUS_BUSY;
device.ai.delayed_carry = false;
ui::audio::pause_game_audio(&mut device.ui);
}
}
@@ -147,7 +148,7 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
if device.ai.regs[reg as usize] != value & mask {
device.ai.freq = device.vi.clock / (1 + (value & mask)) as u64;
ui::audio::close_game_audio(&mut device.ui);
ui::audio::init_game_audio(&mut device.ui, device.ai.freq)
ui::audio::init_game_audio(device)
}
device::memory::masked_write_32(&mut device.ai.regs[reg as usize], value, mask)
}
+1 -5
View File
@@ -130,8 +130,6 @@ pub fn format_eeprom(device: &mut device::Device) {
fn eeprom_read_block(device: &mut device::Device, block: usize, offset: usize) {
let address = device.pif.ram[block] as usize * EEPROM_BLOCK_SIZE;
format_eeprom(device);
device.pif.ram[offset..offset + EEPROM_BLOCK_SIZE].copy_from_slice(
&device.ui.storage.saves.eeprom.data[address..address + EEPROM_BLOCK_SIZE],
);
@@ -140,14 +138,12 @@ fn eeprom_read_block(device: &mut device::Device, block: usize, offset: usize) {
fn eeprom_write_block(device: &mut device::Device, block: usize, offset: usize, status: usize) {
let address = device.pif.ram[block] as usize * EEPROM_BLOCK_SIZE;
format_eeprom(device);
device.ui.storage.saves.eeprom.data[address..address + EEPROM_BLOCK_SIZE]
.copy_from_slice(&device.pif.ram[offset..offset + EEPROM_BLOCK_SIZE]);
device.pif.ram[status] = 0x00;
device.ui.storage.saves.eeprom.written = true
ui::storage::schedule_save(device, ui::storage::SaveTypes::Eeprom4k);
}
fn time2data(device: &mut device::Device, offset: usize) {
+11 -7
View File
@@ -1,5 +1,6 @@
use crate::device;
use crate::netplay;
use crate::ui;
use sha2::{Digest, Sha256};
pub const CART_MASK: usize = 0xFFFFFFF;
@@ -63,7 +64,7 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
.data
.insert((masked_address + i) as u32, *item);
}
device.ui.storage.saves.romsave.written = true;
ui::storage::schedule_save(device, ui::storage::SaveTypes::Romsave);
}
device.cart.latch = value & mask;
@@ -94,7 +95,7 @@ pub fn dma_read(
.unwrap_or(&0),
);
}
device.ui.storage.saves.romsave.written = true;
ui::storage::schedule_save(device, ui::storage::SaveTypes::Romsave);
}
device::pi::calculate_cycles(device, 1, length)
@@ -132,11 +133,10 @@ 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 device.netplay.is_some() {
let netplay = device.netplay.as_mut().unwrap();
if let Some(netplay) = &mut device.netplay {
if netplay.player_number == 0 {
netplay::send_rtc(netplay, device.cart.rtc_timestamp);
} else {
@@ -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()
}
+3 -7
View File
@@ -208,7 +208,7 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
.copy_from_slice(&data);
i += 4;
}
device.ui.storage.saves.sdcard.written = true;
ui::storage::schedule_save(device, ui::storage::SaveTypes::Sdcard);
}
'U' => {
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0;
@@ -376,7 +376,6 @@ pub fn read_mem(
_access_size: device::memory::AccessSize,
) -> u32 {
if address & 0x2000 != 0 {
device::cart::format_eeprom(device);
let masked_address = address as usize & SC64_EEPROM_MASK;
u32::from_be_bytes(
device.ui.storage.saves.eeprom.data[masked_address..masked_address + 4]
@@ -395,7 +394,6 @@ pub fn read_mem(
pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u32) {
if address & 0x2000 != 0 {
device::cart::format_eeprom(device);
let masked_address = address as usize & SC64_EEPROM_MASK;
let mut data = u32::from_be_bytes(
device.ui.storage.saves.eeprom.data[masked_address..masked_address + 4]
@@ -405,7 +403,7 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
device::memory::masked_write_32(&mut data, value, mask);
device.ui.storage.saves.eeprom.data[masked_address..masked_address + 4]
.copy_from_slice(&data.to_be_bytes());
device.ui.storage.saves.eeprom.written = true
ui::storage::schedule_save(device, ui::storage::SaveTypes::Eeprom4k);
} else {
let masked_address = address as usize & SC64_BUFFER_MASK;
let mut data = u32::from_be_bytes(
@@ -427,9 +425,8 @@ pub fn dma_read(
) -> u64 {
dram_addr &= device::rdram::RDRAM_MASK as u32;
let buffer = if cart_addr & 0x2000 != 0 {
device::cart::format_eeprom(device);
cart_addr &= SC64_EEPROM_MASK as u32;
device.ui.storage.saves.eeprom.written = true;
ui::storage::schedule_save(device, ui::storage::SaveTypes::Eeprom4k);
&mut device.ui.storage.saves.eeprom.data
} else {
cart_addr &= SC64_BUFFER_MASK as u32;
@@ -459,7 +456,6 @@ pub fn dma_write(
) -> u64 {
dram_addr &= device::rdram::RDRAM_MASK as u32;
let buffer = if cart_addr & 0x2000 != 0 {
device::cart::format_eeprom(device);
cart_addr &= SC64_EEPROM_MASK as u32;
&device.ui.storage.saves.eeprom.data
} else {
+7 -19
View File
@@ -28,13 +28,13 @@ pub struct Flashram {
pub silicon_id: [u32; 2],
}
fn format_sram(device: &mut device::Device) {
pub fn format_sram(device: &mut device::Device) {
if device.ui.storage.saves.sram.data.len() < SRAM_SIZE {
device.ui.storage.saves.sram.data.resize(SRAM_SIZE, 0xFF)
}
}
fn format_flash(device: &mut device::Device) {
pub fn format_flash(device: &mut device::Device) {
if device.ui.storage.saves.flash.data.len() < FLASHRAM_SIZE {
device
.ui
@@ -49,8 +49,6 @@ fn format_flash(device: &mut device::Device) {
fn read_mem_sram(device: &mut device::Device, address: u64) -> u32 {
let masked_address = address as usize & SRAM_MASK;
format_sram(device);
u32::from_be_bytes(
device.ui.storage.saves.sram.data[masked_address..masked_address + 4]
.try_into()
@@ -96,8 +94,6 @@ pub fn read_mem(
fn write_mem_sram(device: &mut device::Device, address: u64, value: u32, mask: u32) {
let masked_address = address as usize & SRAM_MASK;
format_sram(device);
let mut data = u32::from_be_bytes(
device.ui.storage.saves.sram.data[masked_address..masked_address + 4]
.try_into()
@@ -107,7 +103,7 @@ fn write_mem_sram(device: &mut device::Device, address: u64, value: u32, mask: u
device.ui.storage.saves.sram.data[masked_address..masked_address + 4]
.copy_from_slice(&data.to_be_bytes());
device.ui.storage.saves.sram.written = true
ui::storage::schedule_save(device, ui::storage::SaveTypes::Sram);
}
fn write_mem_flash(device: &mut device::Device, address: u64, value: u32, mask: u32) {
@@ -116,7 +112,6 @@ fn write_mem_flash(device: &mut device::Device, address: u64, value: u32, mask:
device.cart.flashram.status = (value & mask) & 0xff;
} else if (address & 0x1ffff) == 0x10000 {
/* set command */
format_flash(device);
flashram_command(device, value & mask);
} else {
/* other accesses are not implemented */
@@ -148,8 +143,6 @@ fn dma_read_sram(device: &mut device::Device, mut cart_addr: u32, mut dram_addr:
let mut i = dram_addr;
let mut j = cart_addr;
format_sram(device);
while i < dram_addr + length {
*device
.ui
@@ -167,12 +160,10 @@ fn dma_read_sram(device: &mut device::Device, mut cart_addr: u32, mut dram_addr:
j += 1;
}
device.ui.storage.saves.sram.written = true
ui::storage::schedule_save(device, ui::storage::SaveTypes::Sram);
}
fn dma_read_flash(device: &mut device::Device, cart_addr: u32, dram_addr: u32, length: u32) {
format_flash(device);
if (cart_addr & 0x1ffff) == 0x00000
&& length == 128
&& device.cart.flashram.mode == FlashramMode::PageProgram
@@ -217,8 +208,6 @@ fn dma_write_sram(
let mut i = dram_addr;
let mut j = cart_addr;
format_sram(device);
while i < dram_addr + length {
*device
.rdram
@@ -266,7 +255,6 @@ fn dma_write_flash(
} else if (cart_addr & 0x1ffff) < 0x10000
&& device.cart.flashram.mode == FlashramMode::ReadArray
{
format_flash(device);
/* adjust flashram address before starting DMA. */
if device.cart.flashram.silicon_id[1] == MX29L1100_ID
|| device.cart.flashram.silicon_id[1] == MX29L0000_ID
@@ -331,12 +319,12 @@ fn flashram_command(device: &mut device::Device, command: u32) {
for i in 0..128 * 128 {
device.ui.storage.saves.flash.data[offset + i] = 0xFF;
}
device.ui.storage.saves.flash.written = true
ui::storage::schedule_save(device, ui::storage::SaveTypes::Flash);
} else if device.cart.flashram.mode == FlashramMode::ChipErase {
for i in 0..FLASHRAM_SIZE {
device.ui.storage.saves.flash.data[i] = 0xFF;
}
device.ui.storage.saves.flash.written = true
ui::storage::schedule_save(device, ui::storage::SaveTypes::Flash);
} else {
panic!("Unexpected flash erase command")
}
@@ -356,7 +344,7 @@ fn flashram_command(device: &mut device::Device, command: u32) {
for i in 0..128 {
device.ui.storage.saves.flash.data[offset + i] = device.cart.flashram.page_buf[i];
}
device.ui.storage.saves.flash.written = true;
ui::storage::schedule_save(device, ui::storage::SaveTypes::Flash);
/* clear program busy flag, set program success flag, transition to status mode */
device.cart.flashram.status &= !0x01;
+24 -14
View File
@@ -56,15 +56,14 @@ pub fn process(device: &mut device::Device, channel: usize) {
}
JCMD_CONTROLLER_READ => {
let offset = device.pif.channels[channel].rx_buf.unwrap();
let input = if device.netplay.is_none() {
ui::input::get(&device.ui, channel)
} else {
if device.netplay.as_ref().unwrap().player_number as usize == channel {
let local_input = ui::input::get(&device.ui, 0);
netplay::send_input(device.netplay.as_ref().unwrap(), local_input);
let input = if let Some(netplay) = &mut device.netplay {
if netplay.player_number as usize == channel {
let local_input = ui::input::get(&mut device.ui, 0);
netplay::send_input(netplay, local_input);
}
netplay::get_input(device, channel)
} else {
ui::input::get(&mut device.ui, channel)
};
device.pif.ram[offset..offset + 4].copy_from_slice(&input.data.to_ne_bytes());
@@ -96,7 +95,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
device.pif.channels[channel].rx_buf.unwrap(),
channel,
),
_ => println!("unknown controller command {cmd}"),
_ => eprintln!("unknown controller command {cmd}"),
}
}
@@ -162,10 +161,12 @@ pub fn pak_switch_event(device: &mut device::Device) {
for (i, channel) in device.pif.channels.iter_mut().enumerate() {
if channel.change_pak != PakType::None {
//stop rumble if it is on
if device.netplay.is_none() {
if let Some(netplay) = &device.netplay {
if netplay.player_number as usize == i {
device::ui::input::set_rumble(&device.ui, 0, 0);
}
} else {
device::ui::input::set_rumble(&device.ui, i, 0);
} else if device.netplay.as_ref().unwrap().player_number as usize == i {
device::ui::input::set_rumble(&device.ui, 0, 0);
}
let new_pak_type = match channel.change_pak {
@@ -189,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(
&format!("P{} MemPak selected", i + 1),
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(
&format!("P{} RumblePak selected", i + 1),
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(
&format!("P{} TransferPak selected", i + 1),
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
}
channel.change_pak = PakType::None;
}
+8 -12
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,
@@ -231,7 +227,7 @@ fn write_mbc5(
} else if address < 0x6000 {
cart.ram_bank = (value & 0xf) as u16;
} else if address < 0xa000 {
println!("Unknown MBC5 write address {address:x}");
eprintln!("Unknown MBC5 write address {address:x}");
} else if (0xa000..0xc000).contains(&address) {
if !cart.ram_enabled {
return;
+8 -6
View File
@@ -1,9 +1,10 @@
use crate::device;
use crate::ui;
pub const MEMPAK_SIZE: usize = 0x8000;
const MPK_PAGE_SIZE: usize = 256;
fn format_mempak(device: &mut device::Device) {
pub fn format_mempak(device: &mut device::Device) {
if device.ui.storage.saves.mempak.data.len() < MEMPAK_SIZE * 4 {
device
.ui
@@ -42,6 +43,11 @@ fn format_mempak(device: &mut device::Device) {
];
for i in 0..4 {
let offset = i * MEMPAK_SIZE;
if device.ui.storage.saves.mempak.data[offset..offset + MEMPAK_SIZE] != [0; MEMPAK_SIZE]
{
// don't overwrite existing data
continue;
}
/* Fill Page 0 with pre-initialized content */
device.ui.storage.saves.mempak.data[offset..offset + MPK_PAGE_SIZE]
@@ -76,8 +82,6 @@ fn format_mempak(device: &mut device::Device) {
pub fn read(device: &mut device::Device, channel: usize, address: u16, data: usize, size: usize) {
if (address as usize) < MEMPAK_SIZE {
format_mempak(device);
let offset = (channel * MEMPAK_SIZE) + address as usize;
device.pif.ram[data..data + size]
.copy_from_slice(&device.ui.storage.saves.mempak.data[offset..offset + size])
@@ -90,12 +94,10 @@ pub fn read(device: &mut device::Device, channel: usize, address: u16, data: usi
pub fn write(device: &mut device::Device, channel: usize, address: u16, data: usize, size: usize) {
if (address as usize) < MEMPAK_SIZE {
format_mempak(device);
let offset = (channel * MEMPAK_SIZE) + address as usize;
device.ui.storage.saves.mempak.data[offset..offset + size]
.copy_from_slice(&device.pif.ram[data..data + size]);
device.ui.storage.saves.mempak.written = true
ui::storage::schedule_save(device, ui::storage::SaveTypes::Mempak);
}
}
+6 -3
View File
@@ -15,10 +15,13 @@ pub fn read(device: &mut device::Device, _channel: usize, address: u16, data: us
pub fn write(device: &mut device::Device, channel: usize, address: u16, data: usize, size: usize) {
if address == 0xc000 {
let rumble = device.pif.ram[data + size - 1];
if device.netplay.is_none() {
if let Some(netplay) = &device.netplay {
if netplay.player_number as usize == channel {
device::ui::input::set_rumble(&device.ui, 0, rumble);
}
} else {
device::ui::input::set_rumble(&device.ui, channel, rumble);
} else if device.netplay.as_ref().unwrap().player_number as usize == channel {
device::ui::input::set_rumble(&device.ui, 0, rumble);
}
}
}
+3 -3
View File
@@ -61,7 +61,7 @@ pub fn read(device: &mut device::Device, channel: usize, address: u16, data: usi
);
}
_ => {
println!("unknown transfer pak read {:x}", address >> 12);
eprintln!("unknown transfer pak read {:x}", address >> 12);
}
}
}
@@ -84,7 +84,7 @@ pub fn write(device: &mut device::Device, channel: usize, address: u16, data: us
}
pak.enabled = true;
}
_ => println!("unknown transfer pak value"),
_ => eprintln!("unknown transfer pak value"),
}
return;
}
@@ -127,7 +127,7 @@ pub fn write(device: &mut device::Device, channel: usize, address: u16, data: us
);
}
_ => {
println!("unknown transfer pak write {:x}", address >> 12);
eprintln!("unknown transfer pak write {:x}", address >> 12);
}
}
}
+7 -18
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;
@@ -29,14 +31,6 @@ pub struct Vru {
pub word_mappings: HashMap<String, String>,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct VruWindow {
#[serde(skip)]
pub window_notifier: Option<tokio::sync::mpsc::Sender<Option<Vec<String>>>>,
#[serde(skip)]
pub word_receiver: Option<tokio::sync::mpsc::Receiver<String>>,
}
pub fn init(device: &mut device::Device) {
reset_vru(device);
create_word_mappings(device);
@@ -187,15 +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 device.vru_window.window_notifier.is_some() {
ui::vru::prompt_for_match(
&device.vru.words,
device.vru_window.window_notifier.as_ref().unwrap(),
device.vru_window.word_receiver.as_mut().unwrap(),
)
} 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),
+47 -41
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) {
@@ -179,6 +181,8 @@ pub fn reserved(device: &mut device::Device, _opcode: u32) {
device::exceptions::reserved_exception(device, 0);
}
pub fn emux_xdetect(_device: &mut device::Device, _opcode: u32) {}
fn get_control_registers(device: &device::Device, index: u32) -> u64 {
match index {
COP0_COUNT_REG => device.cpu.cop0.regs[index as usize] >> 1,
@@ -218,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)
}
_ => {}
}
@@ -236,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,
@@ -261,38 +267,38 @@ pub fn add_cycles(device: &mut device::Device, cycles: u64) {
pub fn map_instructions(device: &mut device::Device) {
device.cpu.cop0.instrs = [
device::cop0::mfc0, // 0
device::cop0::dmfc0, // 1
device::cop0::reserved, // 2
device::cop0::reserved, // 3
device::cop0::mtc0, // 4
device::cop0::dmtc0, // 5
device::cop0::reserved, // 6
device::cop0::reserved, // 7
device::cop0::reserved, // 8
device::cop0::reserved, // 9
device::cop0::reserved, // 10
device::cop0::reserved, // 11
device::cop0::reserved, // 12
device::cop0::reserved, // 13
device::cop0::reserved, // 14
device::cop0::reserved, // 15
device::cop0::execute_cp0, // 16
device::cop0::reserved, // 17
device::cop0::reserved, // 18
device::cop0::reserved, // 19
device::cop0::reserved, // 20
device::cop0::reserved, // 21
device::cop0::reserved, // 22
device::cop0::reserved, // 23
device::cop0::reserved, // 24
device::cop0::reserved, // 25
device::cop0::reserved, // 26
device::cop0::reserved, // 27
device::cop0::reserved, // 28
device::cop0::reserved, // 29
device::cop0::reserved, // 30
device::cop0::reserved, // 31
device::cop0::mfc0, // 0
device::cop0::dmfc0, // 1
device::cop0::reserved, // 2
device::cop0::reserved, // 3
device::cop0::mtc0, // 4
device::cop0::dmtc0, // 5
device::cop0::reserved, // 6
device::cop0::reserved, // 7
device::cop0::reserved, // 8
device::cop0::reserved, // 9
device::cop0::reserved, // 10
device::cop0::reserved, // 11
device::cop0::reserved, // 12
device::cop0::reserved, // 13
device::cop0::reserved, // 14
device::cop0::reserved, // 15
device::cop0::execute_cp0, // 16
device::cop0::reserved, // 17
device::cop0::reserved, // 18
device::cop0::reserved, // 19
device::cop0::emux_xdetect, // 20
device::cop0::reserved, // 21
device::cop0::reserved, // 22
device::cop0::reserved, // 23
device::cop0::reserved, // 24
device::cop0::reserved, // 25
device::cop0::reserved, // 26
device::cop0::reserved, // 27
device::cop0::reserved, // 28
device::cop0::reserved, // 29
device::cop0::reserved, // 30
device::cop0::reserved, // 31
];
device.cpu.cop0.instrs2 = [
+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
};
+6 -1
View File
@@ -1,4 +1,5 @@
use crate::device;
use crate::ui;
pub const EVENT_TYPE_NONE: usize = 0;
pub const EVENT_TYPE_AI: usize = 1;
@@ -12,7 +13,9 @@ pub const EVENT_TYPE_SPDMA: usize = 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_COUNT: usize = 12;
pub const EVENT_TYPE_SAVE: usize = 12;
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 {
@@ -60,6 +63,8 @@ fn get_event_handler(name: usize) -> fn(&mut device::Device) {
EVENT_TYPE_COMPARE => device::cop0::compare_event,
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 -4
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,10 +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.pending_compare_interrupt = false;
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]
@@ -151,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
@@ -22,8 +22,8 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
let data = std::str::from_utf8(
&device.cart.is_viewer_buffer[0x20_usize..(0x20 + length) as usize],
);
if data.is_ok() {
print!("{}", data.unwrap());
if let Ok(data) = data {
print!("{}", data);
}
} else {
let mut data = u32::from_be_bytes(
+7 -2
View File
@@ -62,11 +62,16 @@ pub fn masked_write_64(dst: &mut u64, value: u64, mask: u64) {
pub fn translate_address(
device: &mut device::Device,
address: u64,
mut address: u64,
access_type: AccessType,
) -> (u64, bool, bool) {
if (address & 0xc0000000) != 0x80000000 {
return device::tlb::get_physical_address(device, address, access_type);
if (address & 0xFFFFFFFF00000000) == 0x9000000000000000 {
// this is a hack to support 64-bit addresses used by some homebrew
address |= 0xA0000000;
} else {
return device::tlb::get_physical_address(device, address, access_type);
}
}
(address & 0x1FFFFFFF, address & 0x20000000 == 0, false)
}
+9 -3
View File
@@ -1,6 +1,6 @@
use crate::device;
const MI_INIT_MODE_REG: u32 = 0;
pub const MI_INIT_MODE_REG: u32 = 0;
const MI_VERSION_REG: u32 = 1;
pub const MI_INTR_REG: u32 = 2;
pub const MI_INTR_MASK_REG: u32 = 3;
@@ -29,7 +29,7 @@ const MI_CLR_DP: u32 = 1 << 10;
const MI_SET_DP: u32 = 1 << 11;
/* mode read */
const MI_INIT_MODE: u32 = 1 << 7;
pub const MI_INIT_MODE: u32 = 1 << 7;
const MI_EBUS_MODE: u32 = 1 << 8;
const MI_RDRAM_MODE: u32 = 1 << 9;
@@ -42,7 +42,7 @@ const MI_CLR_DP_INTR: u32 = 1 << 11;
const MI_CLR_RDRAM: u32 = 1 << 12;
const MI_SET_RDRAM: u32 = 1 << 13;
const MI_INIT_LENGTH_MASK: u32 = 0b1111111;
pub const MI_INIT_LENGTH_MASK: u32 = 0b1111111;
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Mi {
@@ -78,9 +78,15 @@ fn update_init_mode(device: &mut device::Device, w: u32) {
device.mi.regs[MI_INIT_MODE_REG as usize] |= w & MI_INIT_LENGTH_MASK;
if w & MI_CLR_INIT != 0 {
for i in 0..(0x3F00000 >> 16) {
device.memory.memory_map_write[i] = device::rdram::write_mem;
}
device.mi.regs[MI_INIT_MODE_REG as usize] &= !MI_INIT_MODE
}
if w & MI_SET_INIT != 0 {
for i in 0..(0x3F00000 >> 16) {
device.memory.memory_map_write[i] = device::rdram::write_mem_repeat;
}
device.mi.regs[MI_INIT_MODE_REG as usize] |= MI_INIT_MODE
}
if w & MI_CLR_EBUS != 0 {
+6 -2
View File
@@ -1,5 +1,6 @@
use crate::device;
use rand_chacha::rand_core::RngCore;
use crate::ui;
use rand::Rng;
const PI_DRAM_ADDR_REG: u32 = 0;
const PI_CART_ADDR_REG: u32 = 1;
@@ -92,6 +93,9 @@ fn dma_write(device: &mut device::Device) {
length -= dram_addr & 0x7;
}
// check RDP before PI writes to RDRAM
ui::video::check_framebuffers(dram_addr, length);
let cycles = (handler.write)(device, cart_addr, dram_addr, length);
device::events::create_event(device, device::events::EVENT_TYPE_PI, cycles);
@@ -165,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
}
+24 -15
View File
@@ -218,11 +218,11 @@ pub fn process_ram(device: &mut device::Device) {
pub fn connect_pif_channels(device: &mut device::Device) {
for i in 0..4 {
if device.netplay.is_none() {
if device.ui.config.input.controller_enabled[i] {
if let Some(netplay) = &device.netplay {
if netplay.player_data[i].reg_id != 0 {
device.pif.channels[i].process = Some(device::controller::process);
}
} else if device.netplay.as_ref().unwrap().player_data[i].reg_id != 0 {
} else if device.ui.config.input.controller_enabled[i] {
device.pif.channels[i].process = Some(device::controller::process);
}
}
@@ -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 {
@@ -268,16 +277,16 @@ pub fn init(device: &mut device::Device) {
connect_pif_channels(device);
for i in 0..4 {
if device.netplay.is_none() {
if device.ui.config.input.controller_enabled[i] {
if device.transferpaks[i].cart.rom.is_empty() {
device.pif.channels[i].pak_handler = Some(default_handler);
} else {
device.pif.channels[i].pak_handler = Some(tpak_handler);
}
if let Some(netplay) = &device.netplay {
if netplay.player_data[i].reg_id != 0 {
device.pif.channels[i].pak_handler = Some(default_handler);
}
} else if device.ui.config.input.controller_enabled[i] {
if device.transferpaks[i].cart.rom.is_empty() {
device.pif.channels[i].pak_handler = Some(default_handler);
} else {
device.pif.channels[i].pak_handler = Some(tpak_handler);
}
} else if device.netplay.as_ref().unwrap().player_data[i].reg_id != 0 {
device.pif.channels[i].pak_handler = Some(default_handler);
}
}
if device.ui.config.input.emulate_vru && device.netplay.is_none() {
+41
View File
@@ -1,4 +1,6 @@
use crate::device;
use crate::retroachievements;
use crate::ui;
use std::alloc::{Layout, alloc_zeroed};
//const RDRAM_CONFIG_REG: u32 = 0;
@@ -50,6 +52,8 @@ pub fn read_mem(
);
let masked_address = address as usize & RDRAM_MASK;
ui::video::check_framebuffers(masked_address as u32, 4);
u32::from_ne_bytes(
device
.rdram
@@ -62,6 +66,8 @@ pub fn read_mem(
}
pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u32) {
ui::video::check_framebuffers(address as u32, 4);
let mut data = u32::from_ne_bytes(
device
.rdram
@@ -80,6 +86,39 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
.copy_from_slice(&data.to_ne_bytes());
}
pub fn write_mem_repeat(device: &mut device::Device, address: u64, value: u32, mask: u32) {
if mask != 0xFFFFFFFF {
panic!("RDRAM write_mem_repeat called with mask {:#x}", mask);
}
let repeat_length = (device.mi.regs[device::mi::MI_INIT_MODE_REG as usize]
& device::mi::MI_INIT_LENGTH_MASK)
+ 1;
if !repeat_length.is_multiple_of(4) {
panic!(
"RDRAM write_mem_repeat called with non-word-aligned length {}",
repeat_length
);
}
ui::video::check_framebuffers(address as u32, repeat_length);
for i in (0..repeat_length as u64).step_by(4) {
device
.rdram
.mem
.get_mut((address + i) as usize..(address + i) as usize + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&value.to_ne_bytes());
}
device.mi.regs[device::mi::MI_INIT_MODE_REG as usize] &= !device::mi::MI_INIT_MODE;
for i in 0..(0x3F00000 >> 16) {
device.memory.memory_map_write[i] = device::rdram::write_mem;
}
}
pub fn read_regs(
device: &mut device::Device,
address: u64,
@@ -113,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
+14 -10
View File
@@ -1,4 +1,5 @@
use crate::device;
use crate::ui;
const SP_MEM_ADDR_REG: u32 = 0;
const SP_DRAM_ADDR_REG: u32 = 1;
@@ -146,6 +147,7 @@ fn do_dma(device: &mut device::Device, dma: RspDma) {
let mut dram_addr = dma.dramaddr & 0xfffff8;
let offset = dma.memaddr & 0x1000;
ui::video::check_framebuffers(dram_addr, count * (length + skip) - skip);
if dma.dir == DmaDir::Read {
let mut j = 0;
while j < count {
@@ -219,6 +221,8 @@ fn fifo_push(device: &mut device::Device, dir: DmaDir) {
panic!("RSP DMA already full")
}
device.rsp.cpu.sync_point = true;
if device.rsp.regs[SP_DMA_BUSY_REG as usize] != 0 {
device.rsp.fifo[1].dir = dir;
if dir == DmaDir::Read {
@@ -276,21 +280,20 @@ pub fn read_regs(
}
let reg = (address & 0xFFFF) >> 2;
match reg as u32 {
SP_DMA_BUSY_REG | SP_DMA_FULL_REG => {
if device.rsp.regs[reg as usize] != 0 {
device.rsp.cpu.sync_point = true;
}
device.rsp.regs[reg as usize]
}
SP_STATUS_REG => {
let value = device.rsp.regs[reg as usize]
& !(SP_STATUS_HALT | SP_STATUS_BROKE | SP_STATUS_INTR_BREAK);
& (SP_STATUS_SIG0
| SP_STATUS_SIG1
| SP_STATUS_SIG2
| SP_STATUS_SIG3
| SP_STATUS_SIG4
| SP_STATUS_SIG5
| SP_STATUS_SIG6
| SP_STATUS_SIG7);
if value == device.rsp.last_status_value && value != 0 {
device.rsp.cpu.sync_point = true;
}
if value & (SP_STATUS_DMA_BUSY | SP_STATUS_DMA_FULL) != 0 {
device.rsp.cpu.sync_point = true;
}
device.rsp.last_status_value = value;
device.rsp.regs[reg as usize]
}
@@ -464,6 +467,7 @@ fn update_sp_status(device: &mut device::Device, w: u32) {
fn do_task(device: &mut device::Device) {
device.rsp.cpu.sync_point = false;
device.rsp.last_status_value = 0;
device.rdp.last_status_value = 0;
if device.rsp.regs[SP_DMA_BUSY_REG as usize] == 1 {
device.rsp.run_after_dma = true
} else {
+138 -43
View File
@@ -304,25 +304,44 @@ pub fn lh(device: &mut device::Device, opcode: u32) {
let address =
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
let mut w = [0; 2];
w[0] = device.rsp.mem[address as usize & 0xFFF];
w[1] = device.rsp.mem[(address as usize + 1) & 0xFFF];
// Optimize for aligned access when possible
let addr_masked = address as usize & 0xFFF;
let value = if addr_masked <= 0xFFE {
u16::from_be_bytes(
device.rsp.mem[addr_masked..addr_masked + 2]
.try_into()
.unwrap(),
)
} else {
let w0 = device.rsp.mem[addr_masked];
let w1 = device.rsp.mem[(addr_masked + 1) & 0xFFF];
((w0 as u16) << 8) | w1 as u16
};
device.rsp.cpu.gpr[rt(opcode) as usize] =
(((w[0] as u16) << 8) | w[1] as u16) as i16 as i32 as u32
device.rsp.cpu.gpr[rt(opcode) as usize] = value as i16 as i32 as u32;
}
pub fn lw(device: &mut device::Device, opcode: u32) {
let address =
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
let mut w = [0; 4];
for (i, item) in w.iter_mut().enumerate() {
*item = device.rsp.mem[(address as usize + i) & 0xFFF];
}
// Optimize for aligned access when possible
let addr_masked = address as usize & 0xFFF;
let value = if addr_masked <= 0xFFC {
u32::from_be_bytes(
device.rsp.mem[addr_masked..addr_masked + 4]
.try_into()
.unwrap(),
)
} else {
let mut value = 0;
for i in 0..4 {
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u32) << (8 * (3 - i));
}
value
};
device.rsp.cpu.gpr[rt(opcode) as usize] =
((w[0] as u32) << 24) | ((w[1] as u32) << 16) | ((w[2] as u32) << 8) | (w[3] as u32)
device.rsp.cpu.gpr[rt(opcode) as usize] = value;
}
pub fn lbu(device: &mut device::Device, opcode: u32) {
@@ -336,24 +355,44 @@ pub fn lhu(device: &mut device::Device, opcode: u32) {
let address =
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
let mut w = [0; 2];
w[0] = device.rsp.mem[address as usize & 0xFFF];
w[1] = device.rsp.mem[(address as usize + 1) & 0xFFF];
// Optimize for aligned access when possible
let addr_masked = address as usize & 0xFFF;
let value = if addr_masked <= 0xFFE {
u16::from_be_bytes(
device.rsp.mem[addr_masked..addr_masked + 2]
.try_into()
.unwrap(),
)
} else {
let w0 = device.rsp.mem[addr_masked];
let w1 = device.rsp.mem[(addr_masked + 1) & 0xFFF];
((w0 as u16) << 8) | w1 as u16
};
device.rsp.cpu.gpr[rt(opcode) as usize] = (((w[0] as u16) << 8) | w[1] as u16) as u32
device.rsp.cpu.gpr[rt(opcode) as usize] = value as u32;
}
pub fn lwu(device: &mut device::Device, opcode: u32) {
let address =
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
let mut w = [0; 4];
for (i, item) in w.iter_mut().enumerate() {
*item = device.rsp.mem[(address as usize + i) & 0xFFF];
}
// Optimize for aligned access when possible
let addr_masked = address as usize & 0xFFF;
let value = if addr_masked <= 0xFFC {
u32::from_be_bytes(
device.rsp.mem[addr_masked..addr_masked + 4]
.try_into()
.unwrap(),
)
} else {
let mut value = 0;
for i in 0..4 {
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u32) << (8 * (3 - i));
}
value
};
device.rsp.cpu.gpr[rt(opcode) as usize] =
((w[0] as u32) << 24) | ((w[1] as u32) << 16) | ((w[2] as u32) << 8) | (w[3] as u32)
device.rsp.cpu.gpr[rt(opcode) as usize] = value;
}
pub fn sb(device: &mut device::Device, opcode: u32) {
@@ -367,18 +406,30 @@ pub fn sh(device: &mut device::Device, opcode: u32) {
let address =
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
device.rsp.mem[address as usize & 0xFFF] = (device.rsp.cpu.gpr[rt(opcode) as usize] >> 8) as u8;
device.rsp.mem[(address as usize + 1) & 0xFFF] =
(device.rsp.cpu.gpr[rt(opcode) as usize]) as u8;
let addr_masked = address as usize & 0xFFF;
let value = device.rsp.cpu.gpr[rt(opcode) as usize] as u16;
// Optimize for aligned access when possible
if addr_masked <= 0xFFE {
device.rsp.mem[addr_masked..addr_masked + 2].copy_from_slice(&value.to_be_bytes());
} else {
device.rsp.mem[addr_masked] = (value >> 8) as u8;
device.rsp.mem[(addr_masked + 1) & 0xFFF] = value as u8;
}
}
pub fn sw(device: &mut device::Device, opcode: u32) {
let address =
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
for i in 0..4 {
device.rsp.mem[(address as usize + i) & 0xFFF] =
(device.rsp.cpu.gpr[rt(opcode) as usize] >> ((3 - i) * 8)) as u8;
let addr_masked = address as usize & 0xFFF;
let value = device.rsp.cpu.gpr[rt(opcode) as usize];
// Optimize for aligned access when possible
if addr_masked <= 0xFFC {
device.rsp.mem[addr_masked..addr_masked + 4].copy_from_slice(&value.to_be_bytes());
} else {
for i in 0..4 {
device.rsp.mem[(addr_masked + i) & 0xFFF] = (value >> ((3 - i) * 8)) as u8;
}
}
}
@@ -737,10 +788,21 @@ pub fn llv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element.is_multiple_of(4) {
let mut value = 0;
for i in 0..4 {
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u32) << (8 * (3 - i));
}
// Optimize for aligned access when possible
let addr_masked = address as usize & 0xFFF;
let value = if addr_masked <= 0xFFC {
u32::from_be_bytes(
device.rsp.mem[addr_masked..addr_masked + 4]
.try_into()
.unwrap(),
)
} else {
let mut value = 0;
for i in 0..4 {
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u32) << (8 * (3 - i));
}
value
};
modify_vpr32(
&mut device.rsp.cpu.vpr[rt(opcode) as usize],
element / 4,
@@ -767,10 +829,21 @@ pub fn ldv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element.is_multiple_of(8) {
let mut value = 0;
for i in 0..8 {
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u64) << (8 * (7 - i));
}
// Optimize for aligned access when possible
let addr_masked = address as usize & 0xFFF;
let value = if addr_masked <= 0xFF8 {
u64::from_be_bytes(
device.rsp.mem[addr_masked..addr_masked + 8]
.try_into()
.unwrap(),
)
} else {
let mut value = 0;
for i in 0..8 {
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u64) << (8 * (7 - i));
}
value
};
modify_vpr64(
&mut device.rsp.cpu.vpr[rt(opcode) as usize],
element / 8,
@@ -797,10 +870,21 @@ pub fn lqv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element == 0 && address.is_multiple_of(16) {
let mut value = 0;
for i in 0..16 {
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u128) << (8 * (15 - i));
}
// Optimize for aligned 16-byte access
let addr_masked = address as usize & 0xFFF;
let value = if addr_masked <= 0xFF0 {
u128::from_be_bytes(
device.rsp.mem[addr_masked..addr_masked + 16]
.try_into()
.unwrap(),
)
} else {
let mut value = 0;
for i in 0..16 {
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u128) << (8 * (15 - i));
}
value
};
modify_vpr128(&mut device.rsp.cpu.vpr[rt(opcode) as usize], value);
} else {
let end = std::cmp::min(16 + element - ((address & 15) as u8), 16);
@@ -823,10 +907,21 @@ pub fn lrv(device: &mut device::Device, opcode: u32) {
let mut element = 16u8.wrapping_sub(((address & 15) as u8).wrapping_sub(velement(opcode)));
address &= !15;
if element == 0 {
let mut value = 0;
for i in 0..16 {
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u128) << (8 * (15 - i));
}
// Optimize for aligned 16-byte access
let addr_masked = address as usize & 0xFFF;
let value = if addr_masked <= 0xFF0 {
u128::from_be_bytes(
device.rsp.mem[addr_masked..addr_masked + 16]
.try_into()
.unwrap(),
)
} else {
let mut value = 0;
for i in 0..16 {
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u128) << (8 * (15 - i));
}
value
};
modify_vpr128(&mut device.rsp.cpu.vpr[rt(opcode) as usize], value);
} else {
while element < 16 {
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -1,6 +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;
@@ -38,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
}
@@ -98,6 +98,8 @@ fn copy_pif_rdram(device: &mut device::Device) {
i += 4;
}
} else if device.si.dma_dir == DmaDir::Read {
// check RDP before SI writes to RDRAM
ui::video::check_framebuffers(dram_addr as u32, device::pif::PIF_RAM_SIZE as u32);
let mut i = 0;
while i < device::pif::PIF_RAM_SIZE {
let data = u32::from_be_bytes(device.pif.ram[i..i + 4].try_into().unwrap());
+72 -56
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,13 +61,10 @@ fn set_vertical_interrupt(device: &mut device::Device) {
}
fn set_current_line(device: &mut device::Device) {
let delay = device.vi.delay;
let next_vi = device::events::get_event(device, device::events::EVENT_TYPE_VI);
if let Some(next_vi) = next_vi {
device.vi.regs[VI_CURRENT_REG as usize] = ((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] {
@@ -120,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);
}
}
_ => {
@@ -137,29 +125,41 @@ pub fn vertical_interrupt_event(device: &mut device::Device) {
if device.cheats.enabled {
cheats::execute_cheats(device, device.cheats.cheats.clone());
}
let mut speed_limiter_toggled = ui::video::check_callback(device);
if device.netplay.is_some() {
netplay::send_sync_check(device);
if device.vi.enable_speed_limiter == device.netplay.as_ref().unwrap().fast_forward {
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);
if let Some(netplay) = &mut device.netplay {
netplay::send_sync_check(netplay, device.cpu.cop0.regs.as_ref());
if device.vi.enable_speed_limiter == netplay.fast_forward {
speed_limiter_toggled = true;
device.vi.enable_speed_limiter = !device.netplay.as_ref().unwrap().fast_forward;
device.vi.enable_speed_limiter = !netplay.fast_forward;
}
}
device.vi.vi_counter += 1;
if device.vi.vi_counter.is_multiple_of(device.vi.limit_freq) && device.vi.enable_speed_limiter {
speed_limiter(device, speed_limiter_toggled);
}
ui::video::update_screen();
/*
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;
ui::video::update_screen();
device.vi.vi_counter += 1;
if device.netplay.is_none() && paused {
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() };
/* toggle vi field if in interlaced mode */
device.vi.field ^= (device.vi.regs[VI_STATUS_REG as usize] >> 6) & 0x1;
@@ -181,35 +181,51 @@ pub fn init(device: &mut device::Device) {
}
}
fn speed_limiter(device: &mut device::Device, speed_limiter_toggled: bool) {
let result = device.vi.limiter.as_ref().unwrap().check();
if let Err(outcome) = result {
let dur = outcome.wait_time_from(governor::clock::DefaultClock::default().now());
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);
}
}
+244 -111
View File
@@ -6,172 +6,305 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
mod cheats;
mod device;
mod netplay;
mod retroachievements;
mod savestates;
mod ui;
use clap::Parser;
use ui::gui;
use std::io::Error;
/// 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)."
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(
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,
}
#[tokio::main]
async fn main() {
#[tokio::main(worker_threads = 4)]
async fn main() -> std::io::Result<()> {
let dirs = ui::get_dirs();
let mut result = std::fs::create_dir_all(dirs.config_dir);
if result.is_err() {
panic!("could not create config dir: {}", result.err().unwrap())
}
result = std::fs::create_dir_all(dirs.data_dir.join("saves"));
if result.is_err() {
panic!("could not create save dir: {}", result.err().unwrap())
}
result = std::fs::create_dir_all(dirs.data_dir.join("states"));
if result.is_err() {
panic!("could not create state dir: {}", result.err().unwrap())
}
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();
let args_as_strings: Vec<String> = std::env::args().collect();
let args_count = args_as_strings.len();
if args_count > 1 && args.game.is_none() {
let mut ui = ui::Ui::new();
if let Some(game) = args.game {
let file_path = std::path::Path::new(&game);
let Some(rom_contents) = device::get_rom_contents(file_path) else {
return Err(Error::other(format!(
"Could not read ROM file: {}",
file_path.display()
)));
};
if args.clear_input_bindings {
ui::input::clear_bindings(&mut ui);
return;
if let Some(slot) = args.load_state
&& slot > 9
{
return Err(Error::other("Savestate slot must be between 0 and 9"));
}
if args.port.is_some() {
let port = args.port.unwrap();
if !(1..=4).contains(&port) {
println!("Port must be betwen 1 and 4");
return;
}
}
if args.list_controllers {
let controllers = ui::input::get_controller_names(&ui);
for (i, controller) in controllers.iter().enumerate() {
println!("Controller {i}: {controller}");
}
return;
}
if args.configure_input_profile.is_some() {
ui::input::configure_input_profile(
&mut ui,
args.configure_input_profile.unwrap(),
args.use_dinput,
);
return;
}
if args.assign_controller.is_some() {
if args.port.is_none() {
println!("Must specify port number");
return;
}
ui::input::assign_controller(
&mut ui,
args.assign_controller.unwrap(),
args.port.unwrap(),
);
}
if args.bind_input_profile.is_some() {
if args.port.is_none() {
println!("Must specify port number");
return;
}
ui::input::bind_input_profile(
&mut ui,
args.bind_input_profile.unwrap(),
args.port.unwrap(),
);
}
} else if args.game.is_some() {
let file_path = std::path::Path::new(args.game.as_ref().unwrap());
if let Some(rom_contents) = device::get_rom_contents(file_path) {
let handle = std::thread::Builder::new()
.name("n64".to_string())
.stack_size(env!("N64_STACK_SIZE").parse().unwrap())
.spawn(move || {
let mut device = device::Device::new();
let overclock = device.ui.config.emulation.overclock;
let disable_expansion_pak = device.ui.config.emulation.disable_expansion_pak;
let game_cheats = {
let game_crc = ui::storage::get_game_crc(&rom_contents);
let cheats = ui::config::Cheats::new();
cheats.cheats.get(&game_crc).cloned().unwrap_or_default()
};
device::run_game(
&mut device,
rom_contents,
ui::gui::GameSettings {
fullscreen: args.fullscreen,
overclock,
disable_expansion_pak,
cheats: game_cheats,
},
);
})
.unwrap();
let mut device = device::Device::new();
handle.join().unwrap();
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 {
println!("Could not read rom file");
return;
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()
&& let Ok(rom) = std::fs::read(&device.ui.config.input.gb_rom_path[i])
&& let Ok(ram) = std::fs::read(&device.ui.config.input.gb_ram_path[i])
{
device.transferpaks[i].cart.rom = rom;
device.transferpaks[i].cart.ram = ram;
}
}
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()
&& !device.transferpaks[i].cart.ram.is_empty()
&& let Err(e) = tokio::fs::write(
&device.ui.config.input.gb_ram_path[i],
&device.transferpaks[i].cart.ram,
)
.await
{
eprintln!("Error writing GB RAM: {}", e);
}
}
}
if let Some(shutdown_tx) = &shutdown_tx {
ui::usb::close(shutdown_tx);
}
} else if std::env::args().count() > 1 {
let mut config = ui::config::Config::new();
if let Some(profile) = args.configure_input_profile {
ui::input::configure_input_profile(
&mut config,
profile,
args.use_dinput,
args.deadzone.unwrap_or(ui::input::DEADZONE_DEFAULT),
);
ui::sdl_close();
return Ok(());
} else {
if args.clear_input_bindings {
ui::input::clear_bindings(&mut config);
return Ok(());
}
if let Some(port) = args.port
&& !(1..=4).contains(&port)
{
return Err(Error::other("Port must be between 1 and 4"));
}
ui::sdl_init(sdl3_sys::init::SDL_INIT_GAMEPAD);
if args.list_controllers {
let controllers = ui::input::get_controller_names();
for (i, controller) in controllers.iter().enumerate() {
println!("Controller {i}: {controller}");
}
} else {
if let Some(assign_controller) = args.assign_controller {
let Some(port) = args.port else {
ui::sdl_close();
return Err(Error::other("Must specify port number"));
};
ui::input::assign_controller(&mut config, assign_controller - 1, port);
}
if let Some(profile) = args.bind_input_profile {
let Some(port) = args.port else {
ui::sdl_close();
return Err(Error::other("Must specify port number"));
};
ui::input::bind_input_profile(&mut config, profile, port);
}
}
}
} else {
gui::app_window();
#[cfg(feature = "gui")]
{
retroachievements::init_client(false, false, false);
ui::gui::app_window();
retroachievements::shutdown_client();
}
}
ui::sdl_close();
Ok(())
}
+19 -12
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();
}
@@ -103,14 +103,13 @@ pub fn receive_save(netplay: &mut Netplay, save_type: &str, save_data: &mut Vec<
*save_data = response;
}
pub fn send_sync_check(device: &mut device::Device) {
let netplay = device.netplay.as_mut().unwrap();
pub fn send_sync_check(netplay: &mut Netplay, regs: &[u64]) {
if netplay.vi_counter.is_multiple_of(600) {
let mut request: Vec<u8> = [UDP_SYNC_DATA].to_vec();
request.extend_from_slice(&(netplay.vi_counter).to_be_bytes());
for i in 0..device::cop0::COP0_REGS_COUNT as usize {
request.extend_from_slice(&(device.cpu.cop0.regs[i] as u32).to_be_bytes());
for item in regs.iter() {
request.extend_from_slice(&(*item as u32).to_be_bytes());
}
netplay.udp_socket.send(&request).unwrap();
@@ -136,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);
@@ -147,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,
@@ -178,9 +180,9 @@ 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) {
while netplay.udp_socket.recv(&mut buf).is_ok() {
match buf[0] {
UDP_RECEIVE_KEY_INFO | UDP_RECEIVE_KEY_INFO_GRATUITOUS => {
let player = buf[1] as usize;
@@ -192,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;
+239
View File
@@ -0,0 +1,239 @@
include!(concat!(env!("OUT_DIR"), "/retroachievements_bindings.rs"));
use crate::ui;
#[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();
ui::WEB_CLIENT
.post(url)
.body(post_data)
.header(reqwest::header::CONTENT_TYPE, content_type)
.send()
} else {
ui::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) };
}
+78 -41
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,37 +73,61 @@ 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 data: &[(&[u8], &str)] = &[
(&postcard::to_stdvec(device).unwrap(), "device"),
(
&postcard::to_stdvec(&device.ui.storage.saves).unwrap(),
"saves",
),
(&rdp_state, "rdp_state"),
];
let compressed_file = ui::storage::compress_file(data);
std::fs::write(
device.ui.storage.paths.savestate_file_path.clone(),
compressed_file,
)
.unwrap();
ui::video::onscreen_message(
&device.ui,
&format!(
"Savestate created in slot {}",
device.ui.storage.save_state_slot
),
);
let mut ra_state: Vec<u8> = vec![0; retroachievements::state_size()];
retroachievements::save_state(ra_state.as_mut_ptr(), ra_state.len());
let save_path = device.ui.storage.paths.savestate_file_path.clone();
if let Ok(device_data) = postcard::to_stdvec(device)
&& let Ok(saves_data) = postcard::to_stdvec(&device.ui.storage.saves)
&& let Ok(compressed_file) = ui::storage::compress_file(&[
(&device_data, "device"),
(&saves_data, "saves"),
(&rdp_state, "rdp_state"),
(&ra_state, "ra_state"),
])
{
tokio::spawn(async move {
if let Err(e) = tokio::fs::write(save_path, compressed_file).await {
eprintln!("Error writing savestate: {}", e);
}
});
ui::video::onscreen_message(
&format!(
"Savestate created in slot {}",
device.ui.storage.save_state_slot
),
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
} else {
ui::video::onscreen_message(
&format!(
"Failed to create savestate 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");
if let Ok(state) = postcard::from_bytes::<device::Device>(&device_bytes) {
device.ui.storage.saves = postcard::from_bytes(&save_bytes).unwrap();
if let Ok(device_bytes) = ui::storage::decompress_file(savestate, "device")
&& let Ok(save_bytes) = ui::storage::decompress_file(savestate, "saves")
&& let Ok(rdp_state) = ui::storage::decompress_file(savestate, "rdp_state")
&& let Ok(ra_state) = ui::storage::decompress_file(savestate, "ra_state")
&& let Ok(state) = postcard::from_bytes::<device::Device>(&device_bytes)
&& let Ok(saves) = postcard::from_bytes(&save_bytes)
{
device.ui.storage.saves = saves;
device.cpu = state.cpu;
device.pif = state.pif;
@@ -163,28 +187,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,
@@ -196,24 +214,43 @@ 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::audio::init_game_audio(device);
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,
);
}
}
+79 -62
View File
@@ -1,28 +1,43 @@
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 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()
});
pub struct Dirs {
pub config_dir: std::path::PathBuf,
pub data_dir: std::path::PathBuf,
pub cache_dir: std::path::PathBuf,
}
pub struct Audio {
pub audio_device_spec: sdl3_sys::audio::SDL_AudioSpec,
pub audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
pub audio_device: u32,
pub gain: f32,
}
pub struct Input {
pub joysticks: Vec<sdl3_sys::joystick::SDL_JoystickID>,
pub keyboard_state: *const bool,
pub controllers: [input::Controllers; 4],
}
@@ -37,6 +52,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 {
@@ -44,12 +63,19 @@ 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,
pub game_id: String,
pub game_hash: String,
pub with_sdl: bool,
pub audio: Audio,
pub input: Input,
pub storage: Storage,
@@ -57,25 +83,19 @@ pub struct Ui {
pub usb: Usb,
}
impl Drop for Ui {
fn drop(&mut self) {
if self.with_sdl {
unsafe {
sdl3_ttf_sys::ttf::TTF_Quit();
sdl3_sys::init::SDL_Quit();
}
}
}
}
pub fn sdl_init(flag: sdl3_sys::init::SDL_InitFlags) {
unsafe {
let init = sdl3_sys::init::SDL_WasInit(0);
if init & flag == 0 && !sdl3_sys::init::SDL_InitSubSystem(flag) {
let err = std::ffi::CStr::from_ptr(sdl3_sys::error::SDL_GetError())
.to_str()
.unwrap();
panic!("Could not initialize SDL subsystem: {flag}, {err}");
if sdl3_sys::init::SDL_WasInit(flag) == 0 && !sdl3_sys::init::SDL_InitSubSystem(flag) {
let err = sdl3_sys::error::SDL_GetError();
panic!(
"Could not initialize SDL subsystem: {}, {}",
u32::from(flag),
if err.is_null() {
"Unknown error"
} else {
std::ffi::CStr::from_ptr(err).to_str().unwrap()
}
);
}
}
}
@@ -88,30 +108,38 @@ pub fn ttf_init() {
}
}
pub fn sdl_close() {
unsafe {
sdl3_ttf_sys::ttf::TTF_Quit();
sdl3_sys::init::SDL_Quit();
}
}
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"),
}
}
}
impl Ui {
fn construct_ui(joysticks: Vec<u32>, with_sdl: bool) -> Ui {
pub fn new() -> 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: [
@@ -119,25 +147,32 @@ impl Ui {
game_controller: std::ptr::null_mut(),
joystick: std::ptr::null_mut(),
rumble: false,
guid: sdl3_sys::guid::SDL_GUID::default(),
last_key_state: 0,
},
input::Controllers {
game_controller: std::ptr::null_mut(),
joystick: std::ptr::null_mut(),
rumble: false,
guid: sdl3_sys::guid::SDL_GUID::default(),
last_key_state: 0,
},
input::Controllers {
game_controller: std::ptr::null_mut(),
joystick: std::ptr::null_mut(),
rumble: false,
guid: sdl3_sys::guid::SDL_GUID::default(),
last_key_state: 0,
},
input::Controllers {
game_controller: std::ptr::null_mut(),
joystick: std::ptr::null_mut(),
rumble: false,
guid: sdl3_sys::guid::SDL_GUID::default(),
last_key_state: 0,
},
],
keyboard_state: std::ptr::null_mut(),
joysticks,
},
storage: Storage {
save_state_slot: 0,
@@ -155,27 +190,27 @@ impl Ui {
write_to_disk: true,
eeprom: storage::Save {
data: Vec::new(),
written: false,
write_pending: false,
},
sram: storage::Save {
data: Vec::new(),
written: false,
write_pending: false,
},
flash: storage::Save {
data: Vec::new(),
written: false,
write_pending: false,
},
mempak: storage::Save {
data: Vec::new(),
written: false,
write_pending: false,
},
sdcard: storage::Save {
data: Vec::new(),
written: false,
write_pending: false,
},
romsave: storage::RomSave {
data: std::collections::HashMap::new(),
written: false,
write_pending: false,
},
},
},
@@ -183,40 +218,22 @@ impl Ui {
game_id: String::new(),
game_hash: String::new(),
audio: Audio {
audio_device_spec: Default::default(),
audio_stream: std::ptr::null_mut(),
audio_device: 0,
gain: 1.0,
},
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,
},
dirs,
with_sdl,
}
}
pub fn default() -> Ui {
Self::construct_ui(vec![], false)
}
pub fn new() -> Ui {
sdl_init(sdl3_sys::init::SDL_INIT_GAMEPAD);
let mut num_joysticks = 0;
let joysticks = unsafe { sdl3_sys::joystick::SDL_GetJoysticks(&mut num_joysticks) };
if joysticks.is_null() {
panic!("Could not get joystick list");
}
let mut joystick_vec = vec![];
for i in 0..num_joysticks {
joystick_vec.push(unsafe { *joysticks.add(i as usize) });
}
unsafe { sdl3_sys::stdinc::SDL_free(joysticks as *mut std::ffi::c_void) }
Self::construct_ui(joystick_vec, true)
}
}
+104 -60
View File
@@ -1,63 +1,80 @@
use crate::device;
use crate::ui;
pub fn init(ui: &mut ui::Ui, frequency: u64) {
static ADJUST_LOCKED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
pub fn init(device: &mut device::Device) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_AUDIO);
let device_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: 48000,
channels: 2,
};
ui.audio.audio_device = unsafe {
sdl3_sys::audio::SDL_OpenAudioDevice(
sdl3_sys::audio::SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
&device_audio_spec,
)
};
if ui.audio.audio_device == 0 {
panic!("Could not open audio device");
}
if !unsafe {
sdl3_sys::audio::SDL_GetAudioDeviceFormat(
ui.audio.audio_device,
&mut ui.audio.audio_device_spec,
std::ptr::null_mut(),
)
} {
panic!("Could not get audio device format");
}
init_game_audio(ui, frequency);
init_game_audio(device);
}
pub fn init_game_audio(ui: &mut ui::Ui, frequency: u64) {
unsafe extern "C" fn audio_callback(
_userdata: *mut std::ffi::c_void,
audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
additional_amount: i32,
_total_amount: i32,
) {
if additional_amount > 0 {
unsafe {
sdl3_sys::audio::SDL_PutAudioStreamData(
audio_stream,
vec![0; additional_amount as usize].as_ptr() as *const std::ffi::c_void,
additional_amount,
)
};
adjust_audio_frequency(audio_stream, -0.0005);
}
}
pub fn init_game_audio(device: &mut device::Device) {
let game_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: frequency as i32,
freq: device.ai.freq as i32,
channels: 2,
};
ui.audio.audio_stream = unsafe {
sdl3_sys::audio::SDL_CreateAudioStream(&game_audio_spec, &ui.audio.audio_device_spec)
device.ui.audio.audio_stream = unsafe {
sdl3_sys::audio::SDL_OpenAudioDeviceStream(
sdl3_sys::audio::SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
&game_audio_spec,
None,
std::ptr::null_mut(),
)
};
if ui.audio.audio_stream.is_null() {
return;
if device.ui.audio.audio_stream.is_null() {
panic!("Could not create audio stream");
}
if !unsafe {
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain)
&& sdl3_sys::audio::SDL_BindAudioStream(ui.audio.audio_device, ui.audio.audio_stream)
sdl3_sys::audio::SDL_SetAudioStreamGain(device.ui.audio.audio_stream, device.ui.audio.gain)
&& sdl3_sys::audio::SDL_SetAudioStreamGetCallback(
device.ui.audio.audio_stream,
Some(audio_callback),
std::ptr::null_mut(),
)
} {
panic!("Could not bind audio stream");
panic!("Could not initialize audio stream");
}
if device.ai.regs[device::ai::AI_STATUS_REG as usize] & device::ai::AI_STATUS_BUSY != 0 {
resume_game_audio(&mut device.ui);
} else {
pause_game_audio(&mut device.ui);
}
}
pub fn close(ui: &mut ui::Ui) {
close_game_audio(ui);
}
pub fn resume_game_audio(ui: &mut ui::Ui) {
unsafe {
sdl3_sys::audio::SDL_CloseAudioDevice(ui.audio.audio_device);
ui.audio.audio_device = 0;
sdl3_sys::audio::SDL_ResumeAudioStreamDevice(ui.audio.audio_stream);
}
}
pub fn pause_game_audio(ui: &mut ui::Ui) {
unsafe {
sdl3_sys::audio::SDL_PauseAudioStreamDevice(ui.audio.audio_stream);
}
}
@@ -78,6 +95,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(
&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) {
@@ -88,6 +109,43 @@ 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(
&format!("Audio volume: {:.0}%", ui.audio.gain * 100.0),
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
);
}
unsafe extern "C" fn lock_callback(
_userdata: *mut std::ffi::c_void,
_id: sdl3_sys::timer::SDL_TimerID,
_interval: u32,
) -> u32 {
ADJUST_LOCKED.store(false, std::sync::atomic::Ordering::Relaxed);
0
}
fn adjust_audio_frequency(audio_stream: *mut sdl3_sys::audio::SDL_AudioStream, frequency: f32) {
if ADJUST_LOCKED.load(std::sync::atomic::Ordering::Relaxed) {
return;
}
ADJUST_LOCKED.store(true, std::sync::atomic::Ordering::Relaxed);
unsafe {
sdl3_sys::timer::SDL_AddTimer(1000, Some(lock_callback), std::ptr::null_mut());
let current_ratio = sdl3_sys::everything::SDL_GetAudioStreamFrequencyRatio(audio_stream);
sdl3_sys::everything::SDL_SetAudioStreamFrequencyRatio(
audio_stream,
(current_ratio + frequency).clamp(0.995, 1.005),
);
/*
println!(
"Adjusted audio frequency ratio to {}",
sdl3_sys::everything::SDL_GetAudioStreamFrequencyRatio(audio_stream)
);
*/
}
}
pub fn play_audio(device: &device::Device, dram_addr: usize, length: u64) {
@@ -110,31 +168,17 @@ pub fn play_audio(device: &device::Device, dram_addr: usize, length: u64) {
let audio_queued =
unsafe { sdl3_sys::audio::SDL_GetAudioStreamQueued(device.ui.audio.audio_stream) } as f64;
let acceptable_latency = (device.ai.freq as f64 * 0.2) * 4.0;
let min_latency = (device.ai.freq as f64 * 0.02) * 4.0;
if audio_queued < min_latency {
let silence_buffer: Vec<u8> = vec![0; min_latency as usize & !3];
if !unsafe {
sdl3_sys::audio::SDL_PutAudioStreamData(
device.ui.audio.audio_stream,
silence_buffer.as_ptr() as *const std::ffi::c_void,
silence_buffer.len() as i32,
)
} {
panic!("Could not play audio");
}
}
if audio_queued < acceptable_latency
&& !unsafe {
let samples_per_frame = device.ai.freq as f64 * device.vi.frame_time * 4.0;
let max_latency = samples_per_frame * 8.0;
if audio_queued < max_latency {
unsafe {
sdl3_sys::audio::SDL_PutAudioStreamData(
device.ui.audio.audio_stream,
primary_buffer.as_ptr() as *const std::ffi::c_void,
primary_buffer.len() as i32 * 2,
)
}
{
panic!("Could not play audio");
};
} else if device.vi.enable_speed_limiter {
adjust_audio_frequency(device.ui.audio.audio_stream, 0.0005);
}
}
+8 -36
View File
@@ -1,25 +1,14 @@
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| {
let select_rom = if !rom_dir.is_empty()
&& let Ok(exists) = std::fs::exists(&rom_dir)
&& exists
{
let select_rom = if !rom_dir.is_empty() && std::fs::exists(&rom_dir).unwrap_or(false) {
rfd::AsyncFileDialog::new().set_directory(rom_dir)
} else {
rfd::AsyncFileDialog::new()
@@ -38,7 +27,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 +66,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 +117,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();
+21 -4
View File
@@ -29,6 +29,7 @@ pub struct InputProfile {
pub joystick_hat: [InputJoystickHat; ui::input::PROFILE_SIZE],
pub joystick_axis: [InputControllerAxis; ui::input::PROFILE_SIZE],
pub dinput: bool,
pub deadzone: i32,
}
#[derive(serde::Serialize, serde::Deserialize)]
@@ -38,15 +39,19 @@ 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,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Video {
pub upscale: u32,
pub ssaa: bool,
pub integer_scaling: bool,
pub fullscreen: bool,
pub widescreen: bool,
pub vsync: bool,
pub crt: bool,
}
@@ -69,6 +74,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 {
@@ -80,8 +86,11 @@ impl Drop for Cheats {
fn write_cheats(cheats: &Cheats) {
let dirs = ui::get_dirs();
let file_path = dirs.config_dir.join("cheats.json");
let f = std::fs::File::create(file_path).unwrap();
serde_json::to_writer_pretty(f, &cheats).unwrap();
if let Ok(f) = std::fs::File::create(file_path)
&& let Err(e) = serde_json::to_writer_pretty(f, &cheats)
{
eprintln!("Error writing cheats: {}", e);
}
}
impl Cheats {
@@ -110,8 +119,11 @@ impl Drop for Config {
fn write_config(config: &Config) {
let dirs = ui::get_dirs();
let file_path = dirs.config_dir.join("config.json");
let f = std::fs::File::create(file_path).unwrap();
serde_json::to_writer_pretty(f, &config).unwrap();
if let Ok(f) = std::fs::File::create(file_path)
&& let Err(e) = serde_json::to_writer_pretty(f, &config)
{
eprintln!("Error writing config: {}", e);
}
}
impl Config {
@@ -139,13 +151,17 @@ 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 {
upscale: 1,
ssaa: false,
integer_scaling: false,
fullscreen: false,
widescreen: false,
vsync: true,
crt: false,
},
emulation: Emulation {
@@ -154,6 +170,7 @@ impl Config {
usb: false,
},
rom_dir: std::path::PathBuf::new(),
recent_roms: Vec::new(),
}
}
}
+442 -314
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,30 +19,16 @@ pub struct NetplayDevice {
pub player_number: u8,
}
pub struct GbPaths {
pub rom: [Option<std::path::PathBuf>; 4],
pub ram: [Option<std::path::PathBuf>; 4],
}
pub struct VruChannel {
pub vru_window_notifier: Option<tokio::sync::mpsc::Sender<Option<Vec<String>>>>,
pub vru_word_receiver: Option<tokio::sync::mpsc::Receiver<String>>,
}
#[derive(Clone)]
pub struct GameSettings {
pub fullscreen: bool,
pub overclock: bool,
pub disable_expansion_pak: bool,
pub cheats: std::collections::HashMap<String, Option<String>>,
pub struct RASettings {
pub enabled: bool,
pub hardcore: bool,
pub challenge: bool,
pub leaderboard: bool,
}
fn check_latest_version(weak: slint::Weak<AppWindow>) {
let client = reqwest::Client::builder()
.user_agent(env!("CARGO_PKG_NAME"))
.build()
.unwrap();
let task = client
let task = ui::WEB_CLIENT
.get("https://api.github.com/repos/gopher64/gopher64/releases/latest")
.send();
tokio::spawn(async move {
@@ -50,9 +36,12 @@ fn check_latest_version(weak: slint::Weak<AppWindow>) {
if let Ok(response) = response {
let data: Result<GithubData, reqwest::Error> = response.json().await;
let latest_version = if let Ok(data) = data {
semver::Version::parse(&data.tag_name[1..]).unwrap()
let latest_version = if let Ok(data) = data
&& let Ok(github_version) = semver::Version::parse(&data.tag_name[1..])
{
github_version
} else {
eprintln!("Error getting latest version from GitHub");
semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap()
};
let current_version = semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
@@ -64,31 +53,94 @@ 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) {
let weak2 = weak.clone();
weak.upgrade_in_event_loop(move |handle| {
if handle.get_game_running() {
return;
}
save_settings(&handle);
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) {
let weak = app.as_weak();
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());
}
slint::winit_030::EventResult::Propagate
});
}
fn local_game_window(app: &AppWindow, config: &ui::config::Config) {
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| std::fs::exists(x).unwrap_or(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 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());
}
weak.upgrade_in_event_loop(move |handle| {
save_settings(&handle, &controller_paths);
open_rom(&handle, ui::Usb { usb_tx, cart_rx })
save_settings(&handle);
open_rom(&handle)
})
.unwrap();
});
let weak = app.as_weak();
app.on_recent_rom_button_clicked(move |rom| {
weak.upgrade_in_event_loop(move |handle| {
run_with_path(handle.as_weak(), std::path::PathBuf::from(rom.to_string()));
})
.unwrap();
});
let saves_path = dirs.data_dir.join("saves");
app.on_saves_folder_button_clicked(move || {
open::that_detached(saves_path.clone()).unwrap();
if let Err(e) = open::that_detached(saves_path.clone()) {
eprintln!("Error opening saves folder: {}", e);
}
});
file_dropped(app);
}
fn input_profiles(config: &ui::config::Config) -> Vec<String> {
@@ -101,8 +153,10 @@ fn input_profiles(config: &ui::config::Config) -> Vec<String> {
fn settings_window(app: &AppWindow, config: &ui::config::Config) {
app.set_integer_scaling(config.video.integer_scaling);
app.set_ssaa(config.video.ssaa);
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);
@@ -111,6 +165,7 @@ fn settings_window(app: &AppWindow, config: &ui::config::Config) {
1 => 0,
2 => 1,
4 => 2,
8 => 3,
_ => 0,
};
app.set_resolution(combobox_value);
@@ -123,7 +178,6 @@ fn settings_window(app: &AppWindow, config: &ui::config::Config) {
fn update_input_profiles(weak: &slint::Weak<AppWindow>, config: &ui::config::Config) {
let profiles = input_profiles(config);
let config_bindings = config.input.input_profile_binding.clone();
let weak2 = weak.clone();
weak.upgrade_in_event_loop(move |handle| {
let profile_bindings = slint::VecModel::default();
for (i, input_profile_binding) in handle.get_selected_profile_binding().iter().enumerate() {
@@ -135,82 +189,138 @@ fn update_input_profiles(weak: &slint::Weak<AppWindow>, config: &ui::config::Con
let position = profiles
.iter()
.position(|profile| *profile == currently_selected);
profile_bindings.push(position.unwrap() as i32);
profile_bindings.push(position.unwrap_or(0) 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));
// this is a workaround to make the input profile combobox update
handle.set_blank_profiles(true);
slint::Timer::single_shot(std::time::Duration::from_millis(200), move || {
weak2
.upgrade_in_event_loop(move |handle| {
handle.set_blank_profiles(false);
})
.unwrap();
});
handle
.set_selected_profile_binding(slint::ModelRc::from(std::rc::Rc::new(profile_bindings)));
})
.unwrap();
}
fn controller_window(
app: &AppWindow,
config: &ui::config::Config,
controller_names: &Vec<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()),
);
fn clear_gb_paths(weak: &slint::Weak<AppWindow>, player: i32) {
weak.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();
}
fn controller_window(app: &AppWindow, config: &ui::config::Config) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_GAMEPAD);
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_changed(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(vec![false, false, false, false]),
)));
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_controllers_model: std::rc::Rc<slint::VecModel<i32>> =
std::rc::Rc::new(selected_controllers);
app.set_selected_controller(slint::ModelRc::from(selected_controllers_model));
let config_controller_assignment = config.input.controller_assignment.clone();
let weak_app = app.as_weak();
app.on_controller_window_created(move || {
let controller_assignment = config_controller_assignment.clone();
weak_app
.upgrade_in_event_loop(move |handle| {
let mut current_selected_paths = vec![None; 4];
for (i, selected_controller) in handle.get_selected_controller().iter().enumerate()
{
current_selected_paths[i] = handle
.get_controller_paths()
.row_data(selected_controller as usize);
}
let controller_names = ui::input::get_controller_names();
handle.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_paths = ui::input::get_controller_paths();
handle.set_controller_paths(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(
controller_paths
.iter()
.map(|x| x.into())
.collect::<Vec<slint::SharedString>>(),
),
)));
let selected_controllers = slint::VecModel::default();
for i in 0..4 {
let assigned_path =
if let Some(current_selected_path) = &current_selected_paths[i] {
current_selected_path.to_string()
} else if let Some(config_assigned_path) = &controller_assignment[i] {
config_assigned_path.to_string()
} else {
String::new()
};
let selected_index = controller_paths
.iter()
.position(|controller_path| assigned_path == *controller_path)
.unwrap_or(0) as i32;
selected_controllers.push(selected_index);
}
handle.set_selected_controller(slint::ModelRc::from(std::rc::Rc::new(
selected_controllers,
)));
})
.unwrap();
});
let weak_app = app.as_weak();
app.on_input_profile_button_clicked(move || {
let dialog = InputProfileDialog::new().unwrap();
dialog.set_deadzone(ui::input::DEADZONE_DEFAULT);
let weak_dialog = dialog.as_weak();
let weak_app = weak_app.clone();
dialog.on_profile_creation_button_clicked(move || {
@@ -218,31 +328,109 @@ 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);
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 weak_app3 = weak_app2.clone();
tokio::spawn(async move {
if let Some(gb_rom) = select_gb_rom.await {
let weak_app4 = weak_app3.clone();
weak_app3
.upgrade_in_event_loop(move |_handle| {
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();
tokio::spawn(async move {
if let Some(gb_ram) = select_gb_ram.await {
weak_app4
.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 {
clear_gb_paths(&weak_app4, player);
}
});
})
.unwrap();
} else {
clear_gb_paths(&weak_app3, player);
}
});
}
});
}
pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
pub fn save_settings(app: &AppWindow) {
let mut config = ui::config::Config::new();
config.rom_dir = app.get_rom_dir().to_string().into();
config.video.integer_scaling = app.get_integer_scaling();
config.video.ssaa = app.get_ssaa();
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();
config.emulation.usb = app.get_emulate_usb();
let upscale_values = [1, 2, 4];
let upscale_values = [1, 2, 4, 8];
config.video.upscale = upscale_values[app.get_resolution() as usize];
config.input.emulate_vru = app.get_emulate_vru();
@@ -251,6 +439,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
@@ -261,188 +451,180 @@ pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
}
for (i, selected_controller) in app.get_selected_controller().iter().enumerate() {
config.input.controller_assignment[i] =
controller_paths[selected_controller as usize].clone();
if app.get_controller_changed().row_data(i).unwrap_or(false) {
let controller_path = app
.get_controller_paths()
.row_data(selected_controller as usize)
.unwrap()
.to_string();
if controller_path.is_empty() {
config.input.controller_assignment[i] = None;
} else {
config.input.controller_assignment[i] = Some(controller_path);
}
}
}
}
fn about_window(app: &AppWindow) {
app.on_wiki_button_clicked(move || {
open::that_detached("https://github.com/gopher64/gopher64/wiki").unwrap();
if let Err(e) = open::that_detached("https://github.com/gopher64/gopher64/wiki") {
eprintln!("Error opening wiki: {}", e);
}
});
app.on_discord_button_clicked(move || {
open::that_detached("https://discord.gg/9RGXq8W8JQ").unwrap();
if let Err(e) = open::that_detached("https://discord.gg/9RGXq8W8JQ") {
eprintln!("Error opening Discord: {}", e);
}
});
app.on_patreon_button_clicked(move || {
open::that_detached("https://patreon.com/loganmc10").unwrap();
if let Err(e) = open::that_detached("https://patreon.com/loganmc10") {
eprintln!("Error opening Patreon: {}", e);
}
});
app.on_github_sponsors_button_clicked(move || {
open::that_detached("https://github.com/sponsors/loganmc10").unwrap();
if let Err(e) = open::that_detached("https://github.com/sponsors/loganmc10") {
eprintln!("Error opening GitHub Sponsors: {}", e);
}
});
app.on_source_code_button_clicked(move || {
open::that_detached("https://github.com/gopher64/gopher64").unwrap();
if let Err(e) = open::that_detached("https://github.com/gopher64/gopher64") {
eprintln!("Error opening source code: {}", e);
}
});
app.on_newversion_button_clicked(move || {
open::that_detached("https://github.com/gopher64/gopher64/releases/latest").unwrap();
if let Err(e) = open::that_detached("https://github.com/gopher64/gopher64/releases/latest")
{
eprintln!("Error opening new version: {}", e);
}
});
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());
//flatpak and itch.io have their own update checking mechanism
if std::env::var("FLATPAK_ID").is_err() && std::env::var("ITCHIO_APP").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;
let mut controller_paths;
ui::retroachievements::ra_window(&app);
{
let game_ui = ui::Ui::new();
let mut controller_names = ui::input::get_controller_names(&game_ui);
controller_names.insert(0, "None".into());
controller_paths = ui::input::get_controller_paths(&game_ui);
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());
}
let config = ui::config::Config::new();
settings_window(&app, &config);
controller_window(&app, &config);
local_game_window(&app, &config);
}
local_game_window(&app, &controller_paths, usb);
ui::netplay::netplay_window(&app, &controller_paths);
ui::netplay::netplay_window(&app);
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);
}
fn setup_vru_word_watcher(
weak_vru: slint::Weak<AppWindow>,
vru_word_notifier: tokio::sync::mpsc::Sender<String>,
mut vru_window_receiver: tokio::sync::mpsc::Receiver<Option<Vec<String>>>,
) {
tokio::spawn(async move {
loop {
let notifier = vru_word_notifier.clone();
let notifier_closed = vru_word_notifier.clone();
let result = vru_window_receiver.recv().await;
if let Some(Some(words)) = result {
weak_vru
.upgrade_in_event_loop(move |_handle| {
let vru_dialog = VruDialog::new().unwrap();
let vru_dialog_weak = vru_dialog.as_weak();
vru_dialog.on_vru_button_clicked(move |chosen_word| {
notifier.try_send(chosen_word.to_string()).unwrap();
vru_dialog_weak.unwrap().window().hide().unwrap();
});
vru_dialog.window().on_close_requested(move || {
notifier_closed.try_send("".to_string()).unwrap();
slint::CloseRequestResponse::HideWindow
});
let words_vec = slint::VecModel::default();
for word in words {
words_vec.push(word.into());
}
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();
} else {
return;
}
}
});
save_settings(&app);
}
pub fn run_rom(
gb_paths: GbPaths,
file_path: std::path::PathBuf,
mut game_settings: GameSettings,
vru_channel: VruChannel,
game_settings: ui::GameSettings,
netplay: Option<NetplayDevice>,
usb: ui::Usb,
ra_settings: RASettings,
weak: slint::Weak<AppWindow>,
) {
std::thread::Builder::new()
.name("n64".to_string())
.stack_size(env!("N64_STACK_SIZE").parse().unwrap())
.spawn(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.vru_window.window_notifier = vru_channel.vru_window_notifier;
device.vru_window.word_receiver = vru_channel.vru_word_receiver;
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);
}
} else {
println!("Could not read rom file");
}
if let Some(vru_window_notifier) = device.vru_window.window_notifier {
vru_window_notifier.try_send(None).unwrap();
}
let rom_dir = device.ui.config.rom_dir.clone();
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());
}
handle.set_game_running(false);
})
tokio::spawn(async move {
weak.upgrade_in_event_loop(move |handle| handle.set_game_running(true))
.unwrap();
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([
"--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 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) = 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
&& std::fs::exists(&rom.0).unwrap_or(false)
{
recent_roms.push(rom);
}
}
handle.set_recent_roms(slint::ModelRc::from(std::rc::Rc::new(recent_roms)));
}
handle.set_game_running(false);
})
.unwrap();
});
}
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)
&& exists
{
let select_rom = if !rom_dir.is_empty() && std::fs::exists(&rom_dir).unwrap_or(false) {
rfd::AsyncFileDialog::new().set_directory(rom_dir)
} else {
rfd::AsyncFileDialog::new()
@@ -450,86 +632,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", "SAV", "RAM"])
.pick_file(),
);
}
}
#[allow(clippy::type_complexity)]
let (vru_window_notifier, vru_window_receiver): (
tokio::sync::mpsc::Sender<Option<Vec<String>>>,
tokio::sync::mpsc::Receiver<Option<Vec<String>>>,
) = tokio::sync::mpsc::channel(5);
let (vru_word_notifier, vru_word_receiver): (
tokio::sync::mpsc::Sender<String>,
tokio::sync::mpsc::Receiver<String>,
) = tokio::sync::mpsc::channel(5);
let fullscreen = app.get_fullscreen();
let overclock = app.get_overclock_n64_cpu();
let emulate_vru = app.get_emulate_vru();
let disable_expansion_pak = app.get_disable_expansion_pak();
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();
if emulate_vru {
setup_vru_word_watcher(app.as_weak(), vru_word_notifier, vru_window_receiver);
}
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
},
if emulate_vru {
VruChannel {
vru_window_notifier: Some(vru_window_notifier),
vru_word_receiver: Some(vru_word_receiver),
}
} else {
VruChannel {
vru_window_notifier: None,
vru_word_receiver: None,
}
load_savestate_slot: None,
},
None,
usb,
RASettings {
enabled: ra_enabled,
hardcore: ra_hardcore,
challenge: ra_challenge,
leaderboard: ra_leaderboard,
},
weak,
);
}
+52 -4
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,28 @@ 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);
callback controller_window_created;
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 ssaa <=> SettingsData.ssaa;
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;
@@ -47,13 +67,17 @@ export component AppWindow inherits Window {
in-out property rom_dir <=> SettingsData.rom_dir;
in-out property emulate_vru <=> ControllerData.emulate_vru;
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 controller_paths <=> ControllerData.controller_paths;
in-out property selected_controller <=> ControllerData.selected_controller;
in-out property game_running <=> State.game_running;
in-out property blank_profiles <=> ControllerData.blank_profiles;
in-out property recent_roms <=> State.recent_roms;
in-out property cheat_game_name <=> CheatsData.cheat_game_name;
in-out property cheat_game_crc <=> CheatsData.cheat_game_crc;
in-out property cheats <=> CheatsData.cheats;
@@ -70,6 +94,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")
];
@@ -85,11 +110,20 @@ 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);
}
controller_window_created => {
controller_window_created();
}
}
if(side-bar.current-item == 2): Netplay {
create_session_button_clicked => {
@@ -116,8 +150,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;
+17 -6
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,17 +9,26 @@ 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;
in-out property <[string]> controller_paths;
in-out property <[int]> selected_controller;
in-out property <bool> blank_profiles;
in-out property <[bool]> controller_changed;
}
export component ControllerConfig inherits Page {
callback input_profile_button_clicked;
callback transferpak_toggled(int,bool);
callback controller_window_created;
title: @tr("Controller Configuration");
init => {
controller_window_created();
}
VerticalBox {
alignment: start;
HorizontalBox {
@@ -51,7 +60,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;
@@ -64,7 +73,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 => {
@@ -78,16 +87,17 @@ 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);
}
}
}
}
if !ControllerData.blank_profiles: GroupBox {
GroupBox {
title: @tr("Profile");
horizontal-stretch: 0.25;
VerticalLayout {
@@ -114,6 +124,7 @@ export component ControllerConfig inherits Page {
current-index: ControllerData.selected_controller[player];
selected => {
ControllerData.selected_controller[player] = self.current-index;
ControllerData.controller_changed[player] = true;
}
}
}
+28 -4
View File
@@ -1,19 +1,21 @@
import { StandardButton, HorizontalBox, VerticalBox, Button, LineEdit, CheckBox } from "std-widgets.slint";
import { StandardButton, HorizontalBox, VerticalBox, Button, LineEdit, Switch, Slider } from "std-widgets.slint";
export global InputProfileData {
in-out property <bool> dinput;
in-out property <string> profile_name;
in-out property <int> deadzone;
}
export component InputProfileDialog inherits Window {
title: @tr("Create Input Profile");
title: @tr("Create/Modify Input Profile");
callback profile_creation_button_clicked;
in-out property dinput <=> InputProfileData.dinput;
in-out property profile_name <=> InputProfileData.profile_name;
in-out property deadzone <=> InputProfileData.deadzone;
VerticalBox {
HorizontalBox {
LineEdit {
placeholder-text: @tr("Enter profile name");
placeholder-text: @tr("Enter profile name to create or modify");
preferred-width: 300px;
edited(text) => {
InputProfileData.profile_name = text;
@@ -23,7 +25,29 @@ export component InputProfileDialog inherits Window {
HorizontalBox {
alignment: center;
CheckBox {
Text {
text: @tr("Deadzone Percentage");
vertical-alignment: center;
}
Slider {
min-width: 160px;
maximum: 99;
value: InputProfileData.deadzone;
changed(value) => {
InputProfileData.deadzone = value;
}
}
Text {
text: InputProfileData.deadzone + "%";
vertical-alignment: center;
}
}
HorizontalBox {
alignment: center;
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;
}
+6 -4
View File
@@ -9,6 +9,7 @@ export component NetplayJoin inherits Window {
in-out property sessions <=> NetplayData.sessions;
in-out property current_session <=> NetplayData.current_session;
in-out property room_urls <=> NetplayData.room_urls;
in-out property room_ports <=> NetplayData.room_ports;
in-out property game_name <=> NetplayData.game_name;
in-out property game_hash <=> NetplayData.game_hash;
in-out property game_cheats <=> NetplayData.game_cheats;
@@ -19,18 +20,18 @@ export component NetplayJoin inherits Window {
in-out property rom_dir <=> NetplayData.rom_dir;
callback refresh_session();
callback select_rom(string);
callback join_session(string, string, string, string, string);
callback join_session(string, string, string, string, int);
preferred-width: 700px;
VerticalBox {
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;
}
@@ -93,6 +94,7 @@ export component NetplayJoin inherits Window {
NetplayData.current_session = -1;
NetplayData.sessions = [];
NetplayData.room_urls = [];
NetplayData.room_ports = [];
refresh_session();
}
}
@@ -129,7 +131,7 @@ export component NetplayJoin inherits Window {
NetplayData.game_hash,
NetplayData.password,
NetplayData.room_urls[NetplayData.current_session],
NetplayData.sessions[NetplayData.current_session][1].text);
NetplayData.room_ports[NetplayData.current_session]);
}
}
}
+1
View File
@@ -19,6 +19,7 @@ export global NetplayData {
in-out property <string> session_name;
in-out property <[[StandardListViewItem]]> sessions;
in-out property <[string]> room_urls;
in-out property <[int]> room_ports;
in-out property <int> current_session: -1;
in-out property <string> rom_dir;
in-out property <string> custom_server_url;
+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();
}
}
}
}
}
+36 -12
View File
@@ -1,13 +1,15 @@
// 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> ssaa;
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;
@@ -29,7 +31,7 @@ export component Settings inherits Page {
}
ComboBox {
model: ["1x", "2x", "4x"];
model: ["1x", "2x", "4x", "8x"];
current-index: SettingsData.resolution;
selected => {
SettingsData.resolution = self.current-index;
@@ -39,7 +41,18 @@ export component Settings inherits Page {
HorizontalBox {
alignment: start;
CheckBox {
Switch {
text: @tr("SSAA downscaling");
checked: SettingsData.ssaa;
toggled => {
SettingsData.ssaa = self.checked;
}
}
}
HorizontalBox {
alignment: start;
Switch {
text: @tr("Integer Scaling");
checked: SettingsData.integer_scaling;
toggled => {
@@ -50,8 +63,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 +74,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 +85,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 +111,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 +122,7 @@ export component Settings inherits Page {
HorizontalBox {
alignment: start;
CheckBox {
Switch {
text: @tr("Disable Expansion Pak");
checked: SettingsData.disable_expansion_pak;
toggled => {
@@ -109,8 +133,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;
+342 -164
View File
@@ -20,7 +20,7 @@ const AXIS_LEFT: usize = 14;
const AXIS_RIGHT: usize = 15;
const AXIS_UP: usize = 16;
const AXIS_DOWN: usize = 17;
const CHANGE_PAK: usize = 18;
const HOTKEY: usize = 18;
pub const PROFILE_SIZE: usize = 19;
const X_AXIS_SHIFT: usize = 16;
@@ -28,10 +28,14 @@ const Y_AXIS_SHIFT: usize = 24;
const MAX_AXIS_VALUE: f64 = 85.0;
pub const DEADZONE_DEFAULT: i32 = 5;
pub struct Controllers {
pub rumble: bool,
pub game_controller: *mut sdl3_sys::gamepad::SDL_Gamepad,
pub joystick: *mut sdl3_sys::joystick::SDL_Joystick,
pub guid: sdl3_sys::guid::SDL_GUID,
pub last_key_state: u32,
}
pub struct InputData {
@@ -40,7 +44,7 @@ pub struct InputData {
}
fn bound_axis(x: &mut f64, y: &mut f64) {
let radius = 95.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));
@@ -53,6 +57,28 @@ fn bound_axis(x: &mut f64, y: &mut f64) {
}
}
fn apply_deadzone(x: &mut f64, y: &mut f64, deadzone: i32) {
let axis_deadzone = MAX_AXIS_VALUE * (deadzone as f64 / 100.0);
// Calculate the distance from the origin (0, 0)
let distance = f64::sqrt((*x) * (*x) + (*y) * (*y));
if distance <= axis_deadzone {
*x = 0.0;
*y = 0.0;
return;
}
let new_distance =
(distance - axis_deadzone) * MAX_AXIS_VALUE / (MAX_AXIS_VALUE - axis_deadzone);
*x = *x / distance * new_distance;
*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,
@@ -64,7 +90,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 {
@@ -72,7 +98,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 {
@@ -80,7 +106,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 {
@@ -88,45 +114,12 @@ 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)
}
fn get_axis_from_i32(axis: i32) -> sdl3_sys::gamepad::SDL_GamepadAxis {
match axis {
0 => sdl3_sys::gamepad::SDL_GamepadAxis::LEFTX,
1 => sdl3_sys::gamepad::SDL_GamepadAxis::LEFTY,
2 => sdl3_sys::gamepad::SDL_GamepadAxis::RIGHTX,
3 => sdl3_sys::gamepad::SDL_GamepadAxis::RIGHTY,
4 => sdl3_sys::gamepad::SDL_GamepadAxis::LEFT_TRIGGER,
5 => sdl3_sys::gamepad::SDL_GamepadAxis::RIGHT_TRIGGER,
_ => panic!("Invalid axis"),
}
}
fn get_button_from_i32(button: i32) -> sdl3_sys::gamepad::SDL_GamepadButton {
match button {
0 => sdl3_sys::gamepad::SDL_GamepadButton::SOUTH,
1 => sdl3_sys::gamepad::SDL_GamepadButton::EAST,
2 => sdl3_sys::gamepad::SDL_GamepadButton::WEST,
3 => sdl3_sys::gamepad::SDL_GamepadButton::NORTH,
4 => sdl3_sys::gamepad::SDL_GamepadButton::BACK,
5 => sdl3_sys::gamepad::SDL_GamepadButton::GUIDE,
6 => sdl3_sys::gamepad::SDL_GamepadButton::START,
7 => sdl3_sys::gamepad::SDL_GamepadButton::LEFT_STICK,
8 => sdl3_sys::gamepad::SDL_GamepadButton::RIGHT_STICK,
9 => sdl3_sys::gamepad::SDL_GamepadButton::LEFT_SHOULDER,
10 => sdl3_sys::gamepad::SDL_GamepadButton::RIGHT_SHOULDER,
11 => sdl3_sys::gamepad::SDL_GamepadButton::DPAD_UP,
12 => sdl3_sys::gamepad::SDL_GamepadButton::DPAD_DOWN,
13 => sdl3_sys::gamepad::SDL_GamepadButton::DPAD_LEFT,
14 => sdl3_sys::gamepad::SDL_GamepadButton::DPAD_RIGHT,
_ => panic!("Invalid button"),
}
}
fn set_axis_from_controller(
profile: &ui::config::InputProfile,
controller: *mut sdl3_sys::gamepad::SDL_Gamepad,
@@ -137,44 +130,44 @@ fn set_axis_from_controller(
let axis_position = unsafe {
sdl3_sys::gamepad::SDL_GetGamepadAxis(
controller,
get_axis_from_i32(profile.controller_axis[AXIS_LEFT].id),
sdl3_sys::gamepad::SDL_GamepadAxis(profile.controller_axis[AXIS_LEFT].id),
)
};
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 {
let axis_position = unsafe {
sdl3_sys::gamepad::SDL_GetGamepadAxis(
controller,
get_axis_from_i32(profile.controller_axis[AXIS_RIGHT].id),
sdl3_sys::gamepad::SDL_GamepadAxis(profile.controller_axis[AXIS_RIGHT].id),
)
};
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 {
let axis_position = unsafe {
sdl3_sys::gamepad::SDL_GetGamepadAxis(
controller,
get_axis_from_i32(profile.controller_axis[AXIS_DOWN].id),
sdl3_sys::gamepad::SDL_GamepadAxis(profile.controller_axis[AXIS_DOWN].id),
)
};
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 {
let axis_position = unsafe {
sdl3_sys::gamepad::SDL_GetGamepadAxis(
controller,
get_axis_from_i32(profile.controller_axis[AXIS_UP].id),
sdl3_sys::gamepad::SDL_GamepadAxis(profile.controller_axis[AXIS_UP].id),
)
};
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)
@@ -255,7 +248,7 @@ fn set_buttons_from_controller(
*keys |= (unsafe {
sdl3_sys::gamepad::SDL_GetGamepadButton(
controller,
get_button_from_i32(profile_controller_button.id),
sdl3_sys::gamepad::SDL_GamepadButton(profile_controller_button.id),
)
} as u32)
<< i;
@@ -266,7 +259,7 @@ fn set_buttons_from_controller(
let axis_position = unsafe {
sdl3_sys::gamepad::SDL_GetGamepadAxis(
controller,
get_axis_from_i32(profile_controller_axis.id),
sdl3_sys::gamepad::SDL_GamepadAxis(profile_controller_axis.id),
)
};
if axis_position as isize * profile_controller_axis.axis as isize > 0
@@ -289,7 +282,7 @@ pub fn set_rumble(ui: &ui::Ui, channel: usize, rumble: u8) {
controller,
(rumble & 1) as u16 * u16::MAX,
(rumble & 1) as u16 * u16::MAX,
(rumble & 1) as u32 * 60000,
(rumble & 1) as u32 * sdl3_sys::haptic::SDL_HAPTIC_INFINITY,
)
};
} else if !joystick.is_null() {
@@ -298,31 +291,40 @@ pub fn set_rumble(ui: &ui::Ui, channel: usize, rumble: u8) {
joystick,
(rumble & 1) as u16 * u16::MAX,
(rumble & 1) as u16 * u16::MAX,
(rumble & 1) as u32 * 60000,
(rumble & 1) as u32 * sdl3_sys::haptic::SDL_HAPTIC_INFINITY,
)
};
}
}
fn change_paks(
fn hotkey_pressed(
profile: &ui::config::InputProfile,
joystick: *mut sdl3_sys::joystick::SDL_Joystick,
controller: *mut sdl3_sys::gamepad::SDL_Gamepad,
keyboard_state: *const bool,
) -> bool {
let controller_button = profile.controller_buttons[CHANGE_PAK];
let joystick_button = profile.joystick_buttons[CHANGE_PAK];
let joystick_hat = profile.joystick_hat[CHANGE_PAK];
let key = profile.keys[CHANGE_PAK];
let controller_button = profile.controller_buttons[HOTKEY];
let controller_axis = profile.controller_axis[HOTKEY];
let joystick_button = profile.joystick_buttons[HOTKEY];
let joystick_hat = profile.joystick_hat[HOTKEY];
let joystick_axis = profile.joystick_axis[HOTKEY];
let mut pressed = false;
if controller_button.enabled && !controller.is_null() {
pressed = unsafe {
sdl3_sys::gamepad::SDL_GetGamepadButton(
controller,
get_button_from_i32(controller_button.id),
sdl3_sys::gamepad::SDL_GamepadButton(controller_button.id),
)
};
} else if controller_axis.enabled && !controller.is_null() {
let axis_position = unsafe {
sdl3_sys::gamepad::SDL_GetGamepadAxis(
controller,
sdl3_sys::gamepad::SDL_GamepadAxis(controller_axis.id),
)
};
pressed = axis_position as isize * controller_axis.axis as isize > 0
&& axis_position.saturating_abs() > i16::MAX / 2
} else if joystick_button.enabled && !joystick.is_null() {
pressed =
unsafe { sdl3_sys::joystick::SDL_GetJoystickButton(joystick, joystick_button.id) };
@@ -330,45 +332,159 @@ fn change_paks(
pressed = (unsafe { sdl3_sys::joystick::SDL_GetJoystickHat(joystick, joystick_hat.id) }
& joystick_hat.direction)
!= 0;
} else if key.enabled {
pressed = unsafe { *keyboard_state.offset(key.id as isize) };
} else if joystick_axis.enabled && !joystick.is_null() {
let axis_position =
unsafe { sdl3_sys::joystick::SDL_GetJoystickAxis(joystick, joystick_axis.id) };
pressed = axis_position as isize * joystick_axis.axis as isize > 0
&& axis_position.saturating_abs() > i16::MAX / 2
}
pressed
}
pub fn get_controller_names(game_ui: &ui::Ui) -> Vec<String> {
pub fn get_controller_names() -> Vec<String> {
let mut controllers: Vec<String> = vec![];
for joystick in game_ui.input.joysticks.iter() {
let name = unsafe {
std::ffi::CStr::from_ptr(sdl3_sys::joystick::SDL_GetJoystickNameForID(*joystick))
};
controllers.push(name.to_string_lossy().to_string());
for joystick in get_joysticks().iter() {
let name = unsafe { sdl3_sys::joystick::SDL_GetJoystickNameForID(*joystick) };
controllers.push(if name.is_null() {
"Unknown controller".to_string()
} else {
unsafe { std::ffi::CStr::from_ptr(name).to_str().unwrap() }.to_string()
});
}
controllers.insert(0, "None".into());
controllers
}
pub fn get_controller_paths(game_ui: &ui::Ui) -> Vec<Option<String>> {
let mut controller_paths: Vec<Option<String>> = vec![];
#[cfg(feature = "gui")]
pub fn get_controller_paths() -> Vec<String> {
let mut controller_paths: Vec<String> = vec![];
for joystick in game_ui.input.joysticks.iter() {
let path = unsafe {
std::ffi::CStr::from_ptr(sdl3_sys::joystick::SDL_GetJoystickPathForID(*joystick))
.to_string_lossy()
.to_string()
};
controller_paths.push(Some(path));
for joystick in get_joysticks().iter() {
let path = unsafe { sdl3_sys::joystick::SDL_GetJoystickPathForID(*joystick) };
controller_paths.push(if path.is_null() {
String::new()
} else {
unsafe { std::ffi::CStr::from_ptr(path).to_str().unwrap() }.to_string()
});
}
controller_paths.insert(0, String::new());
controller_paths
}
pub fn get(ui: &ui::Ui, channel: usize) -> InputData {
unsafe { sdl3_sys::events::SDL_PumpEvents() };
fn handle_joystick_events(ui: &mut ui::Ui) {
let joystick_event = unsafe { ui::video::get_joystick_event() };
if joystick_event.joystick_id != 0 {
let joystick_id = sdl3_sys::joystick::SDL_JoystickID(joystick_event.joystick_id);
for (i, controller) in ui.input.controllers.iter_mut().enumerate() {
if joystick_event.connected {
if let Some(profile) = ui
.config
.input
.input_profiles
.get(&ui.config.input.input_profile_binding[i])
{
if profile.dinput {
if controller.joystick.is_null()
&& controller.guid
== unsafe {
sdl3_sys::joystick::SDL_GetJoystickGUIDForID(joystick_id)
}
{
controller.joystick =
unsafe { sdl3_sys::joystick::SDL_OpenJoystick(joystick_id) };
}
} else {
if controller.game_controller.is_null()
&& controller.guid
== unsafe {
sdl3_sys::gamepad::SDL_GetGamepadGUIDForID(joystick_id)
}
{
controller.game_controller =
unsafe { sdl3_sys::gamepad::SDL_OpenGamepad(joystick_id) };
}
}
}
} else {
if !controller.joystick.is_null()
&& controller.joystick
== unsafe { sdl3_sys::joystick::SDL_GetJoystickFromID(joystick_id) }
{
unsafe { sdl3_sys::joystick::SDL_CloseJoystick(controller.joystick) };
controller.joystick = std::ptr::null_mut();
} else if !controller.game_controller.is_null()
&& controller.game_controller
== unsafe { sdl3_sys::gamepad::SDL_GetGamepadFromID(joystick_id) }
{
unsafe { sdl3_sys::gamepad::SDL_CloseGamepad(controller.game_controller) };
controller.game_controller = std::ptr::null_mut();
}
}
}
}
}
let profile_name = ui.config.input.input_profile_binding[channel].clone();
let profile = ui.config.input.input_profiles.get(&profile_name).unwrap();
fn handle_hotkeys(keys: u32, last_key_state: u32) {
if keys & (1 << L_TRIG) != 0 && last_key_state & (1 << L_TRIG) == 0 {
unsafe {
sdl3_sys::events::SDL_PushEvent(&mut sdl3_sys::events::SDL_Event {
user: sdl3_sys::events::SDL_UserEvent {
r#type: u32::from(sdl3_sys::events::SDL_EVENT_USER),
code: 1, //save state
..Default::default()
},
})
};
}
if keys & (1 << R_TRIG) != 0 && last_key_state & (1 << R_TRIG) == 0 {
unsafe {
sdl3_sys::events::SDL_PushEvent(&mut sdl3_sys::events::SDL_Event {
user: sdl3_sys::events::SDL_UserEvent {
r#type: u32::from(sdl3_sys::events::SDL_EVENT_USER),
code: 2, //load state
..Default::default()
},
})
};
}
if keys & (1 << START_BUTTON) != 0 && last_key_state & (1 << START_BUTTON) == 0 {
unsafe {
sdl3_sys::events::SDL_PushEvent(&mut sdl3_sys::events::SDL_Event {
user: sdl3_sys::events::SDL_UserEvent {
r#type: u32::from(sdl3_sys::events::SDL_EVENT_USER),
code: 3, //exit game
..Default::default()
},
})
};
}
if keys & (1 << Z_TRIG) != 0 && last_key_state & (1 << Z_TRIG) == 0 {
unsafe {
sdl3_sys::events::SDL_PushEvent(&mut sdl3_sys::events::SDL_Event {
user: sdl3_sys::events::SDL_UserEvent {
r#type: u32::from(sdl3_sys::events::SDL_EVENT_USER),
code: 4, //fast forward
..Default::default()
},
})
};
}
}
pub fn get(ui: &mut ui::Ui, channel: usize) -> InputData {
handle_joystick_events(ui);
let profile_name = &ui.config.input.input_profile_binding[channel];
let Some(profile) = ui.config.input.input_profiles.get(profile_name) else {
eprintln!("Invalid profile name: {profile_name}");
return InputData {
data: 0,
pak_change_pressed: false,
};
};
let mut keys = 0;
let controller = ui.input.controllers[channel].game_controller;
let joystick = ui.input.controllers[channel].joystick;
@@ -409,54 +525,76 @@ pub fn get(ui: &ui::Ui, channel: usize) -> InputData {
}
if !controller.is_null() {
(x, y) = set_axis_from_controller(profile, controller)
(x, y) = set_axis_from_controller(profile, controller);
apply_deadzone(&mut x, &mut y, profile.deadzone);
} else if !joystick.is_null() {
(x, y) = set_axis_from_joystick(profile, joystick)
(x, y) = set_axis_from_joystick(profile, joystick);
apply_deadzone(&mut x, &mut y, profile.deadzone);
}
bound_axis(&mut x, &mut y);
keys |= (x.round() as i8 as u8 as u32) << X_AXIS_SHIFT;
keys |= (y.round() as i8 as u8 as u32) << Y_AXIS_SHIFT;
InputData {
data: keys,
pak_change_pressed: change_paks(profile, joystick, controller, ui.input.keyboard_state),
}
}
let last_key_state = ui.input.controllers[channel].last_key_state;
ui.input.controllers[channel].last_key_state = keys;
pub fn assign_controller(ui: &mut ui::Ui, controller: i32, port: usize) {
if controller < ui.input.joysticks.len() as i32 {
let path = unsafe {
std::ffi::CStr::from_ptr(sdl3_sys::joystick::SDL_GetJoystickPathForID(
ui.input.joysticks[controller as usize],
))
.to_string_lossy()
.to_string()
};
ui.config.input.controller_assignment[port - 1] = Some(path);
if hotkey_pressed(profile, joystick, controller) {
handle_hotkeys(keys, last_key_state);
InputData {
data: 0,
pak_change_pressed: keys & (1 << B_BUTTON) != 0,
}
} else {
println!("Invalid controller number")
let key = profile.keys[HOTKEY];
InputData {
data: keys,
pak_change_pressed: key.enabled
&& unsafe { *ui.input.keyboard_state.offset(key.id as isize) },
}
}
}
pub fn bind_input_profile(ui: &mut ui::Ui, profile: String, port: usize) {
if ui.config.input.input_profiles.contains_key(&profile) {
ui.config.input.input_profile_binding[port - 1] = profile;
pub fn assign_controller(config: &mut ui::config::Config, controller: i32, port: usize) {
let joysticks = get_joysticks();
if controller < 0 {
config.input.controller_assignment[port - 1] = None;
} else if controller < joysticks.len() as i32 {
let path =
unsafe { sdl3_sys::joystick::SDL_GetJoystickPathForID(joysticks[controller as usize]) };
if !path.is_null() {
config.input.controller_assignment[port - 1] =
Some(unsafe { std::ffi::CStr::from_ptr(path).to_str().unwrap().to_string() });
} else {
eprintln!("Invalid controller path for controller {controller}");
}
} else {
println!("Invalid profile name")
eprintln!("Invalid controller number")
}
}
pub fn clear_bindings(ui: &mut ui::Ui) {
pub fn bind_input_profile(config: &mut ui::config::Config, profile: String, port: usize) {
if config.input.input_profiles.contains_key(&profile) {
config.input.input_profile_binding[port - 1] = profile;
} else {
eprintln!("Invalid profile name")
}
}
pub fn clear_bindings(config: &mut ui::config::Config) {
for i in 0..4 {
ui.config.input.controller_assignment[i] = None;
ui.config.input.input_profile_binding[i] = "default".to_string();
config.input.controller_assignment[i] = None;
config.input.input_profile_binding[i] = "default".to_string();
}
}
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) }
@@ -464,24 +602,37 @@ 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) {
pub fn configure_input_profile(
config: &mut ui::config::Config,
profile: String,
dinput: bool,
deadzone: i32,
) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
ui::sdl_init(sdl3_sys::init::SDL_INIT_GAMEPAD);
ui::ttf_init();
if profile == "default" {
println!("Profile name cannot be default");
eprintln!("Profile name cannot be default");
return;
}
if profile.is_empty() {
println!("Profile name cannot be empty");
eprintln!("Profile name cannot be empty");
return;
}
let mut open_joysticks: Vec<*mut sdl3_sys::joystick::SDL_Joystick> = Vec::new();
let mut open_controllers: Vec<*mut sdl3_sys::gamepad::SDL_Gamepad> = Vec::new();
for joystick in ui.input.joysticks.iter() {
for joystick in get_joysticks().iter() {
if !dinput {
let controller = unsafe { sdl3_sys::gamepad::SDL_OpenGamepad(*joystick) };
if !controller.is_null() {
@@ -501,9 +652,9 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
if !unsafe {
sdl3_sys::render::SDL_CreateWindowAndRenderer(
title.as_ptr(),
640,
852,
480,
0,
sdl3_sys::video::SDL_WindowFlags(0),
&mut window,
&mut renderer,
)
@@ -533,7 +684,7 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
("Control Stick Down", AXIS_DOWN),
("Control Stick Left", AXIS_LEFT),
("Control Stick Right", AXIS_RIGHT),
("Change Pak", CHANGE_PAK),
("Hotkey Activator", HOTKEY),
];
let mut new_keys = [ui::config::InputKeyButton {
@@ -572,7 +723,7 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
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,
@@ -587,22 +738,27 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
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);
if event.event_type() == sdl3_sys::events::SDL_EVENT_WINDOW_CLOSE_REQUESTED {
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) {
} else if event.event_type() == sdl3_sys::events::SDL_EVENT_KEY_DOWN {
if unsafe {
!event.key.repeat
&& event.key.scancode != sdl3_sys::scancode::SDL_SCANCODE_LALT
@@ -614,7 +770,7 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
};
key_set = true
}
} else if event_type == u32::from(sdl3_sys::events::SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
} else if event.event_type() == sdl3_sys::events::SDL_EVENT_GAMEPAD_BUTTON_DOWN {
if !open_controllers.is_empty() {
new_controller_buttons[*value] = ui::config::InputKeyButton {
enabled: true,
@@ -622,7 +778,7 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
};
key_set = true
}
} else if event_type == u32::from(sdl3_sys::events::SDL_EVENT_GAMEPAD_AXIS_MOTION) {
} else if event.event_type() == sdl3_sys::events::SDL_EVENT_GAMEPAD_AXIS_MOTION {
let axis_value = unsafe { event.gaxis.value };
let axis = unsafe { event.gaxis.axis };
if !open_controllers.is_empty()
@@ -639,8 +795,7 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
key_set = true
}
}
} else if event_type == u32::from(sdl3_sys::events::SDL_EVENT_JOYSTICK_BUTTON_DOWN)
{
} else if event.event_type() == sdl3_sys::events::SDL_EVENT_JOYSTICK_BUTTON_DOWN {
if !open_joysticks.is_empty() {
new_joystick_buttons[*value] = ui::config::InputKeyButton {
enabled: true,
@@ -648,7 +803,7 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
};
key_set = true
}
} else if event_type == u32::from(sdl3_sys::events::SDL_EVENT_JOYSTICK_HAT_MOTION) {
} else if event.event_type() == sdl3_sys::events::SDL_EVENT_JOYSTICK_HAT_MOTION {
let state = unsafe { event.jhat.value };
let hat = unsafe { event.jhat.hat };
if !open_joysticks.is_empty() && state != sdl3_sys::joystick::SDL_HAT_CENTERED {
@@ -659,8 +814,7 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
};
key_set = true
}
} else if event_type == u32::from(sdl3_sys::events::SDL_EVENT_JOYSTICK_AXIS_MOTION)
{
} else if event.event_type() == sdl3_sys::events::SDL_EVENT_JOYSTICK_AXIS_MOTION {
let axis_value = unsafe { event.jaxis.value };
let axis = unsafe { event.jaxis.axis };
if !open_joysticks.is_empty()
@@ -680,16 +834,18 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
}
}
}
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,
@@ -699,8 +855,9 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
joystick_hat: new_joystick_hat,
joystick_axis: new_joystick_axis,
dinput,
deadzone,
};
ui.config.input.input_profiles.insert(profile, new_profile);
config.input.input_profiles.insert(profile, new_profile);
}
pub fn get_default_profile() -> ui::config::InputProfile {
@@ -789,7 +946,7 @@ pub fn get_default_profile() -> ui::config::InputProfile {
enabled: true,
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_DOWN),
};
default_keys[CHANGE_PAK] = ui::config::InputKeyButton {
default_keys[HOTKEY] = ui::config::InputKeyButton {
enabled: true,
id: i32::from(sdl3_sys::scancode::SDL_SCANCODE_COMMA),
};
@@ -875,7 +1032,7 @@ pub fn get_default_profile() -> ui::config::InputProfile {
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_AXIS_LEFTY),
axis: 1,
};
default_controller_buttons[CHANGE_PAK] = ui::config::InputKeyButton {
default_controller_buttons[HOTKEY] = ui::config::InputKeyButton {
enabled: true,
id: i32::from(sdl3_sys::gamepad::SDL_GAMEPAD_BUTTON_BACK),
};
@@ -899,10 +1056,28 @@ pub fn get_default_profile() -> ui::config::InputProfile {
axis: 0,
}; PROFILE_SIZE],
dinput: false,
deadzone: DEADZONE_DEFAULT,
}
}
fn get_joysticks() -> Vec<sdl3_sys::joystick::SDL_JoystickID> {
unsafe { sdl3_sys::events::SDL_PumpEvents() };
let mut num_joysticks = 0;
let sdl_joysticks = unsafe { sdl3_sys::joystick::SDL_GetJoysticks(&mut num_joysticks) };
if !sdl_joysticks.is_null() {
let parts =
unsafe { std::slice::from_raw_parts(sdl_joysticks, num_joysticks as usize) }.to_vec();
unsafe { sdl3_sys::stdinc::SDL_free(sdl_joysticks as *mut std::ffi::c_void) };
parts
} else {
eprintln!("Could not get joysticks");
vec![]
}
}
pub fn init(ui: &mut ui::Ui) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_GAMEPAD);
ui.input.keyboard_state =
unsafe { sdl3_sys::keyboard::SDL_GetKeyboardState(std::ptr::null_mut()) };
if ui.input.keyboard_state.is_null() {
@@ -910,20 +1085,16 @@ pub fn init(ui: &mut ui::Ui) {
}
for i in 0..4 {
let controller_assignment = &ui.config.input.controller_assignment[i];
if controller_assignment.is_some() && ui.config.input.controller_enabled[i] {
let mut joystick_id = 0;
let assigned_path = controller_assignment.as_ref().unwrap();
if let Some(controller_assignment) = &ui.config.input.controller_assignment[i]
&& ui.config.input.controller_enabled[i]
{
let mut joystick_id = sdl3_sys::everything::SDL_JoystickID(0);
for joystick in ui.input.joysticks.iter() {
let path = unsafe {
std::ffi::CStr::from_ptr(sdl3_sys::joystick::SDL_GetJoystickPathForID(
*joystick,
))
.to_string_lossy()
.to_string()
};
if path == *assigned_path
for joystick in get_joysticks().iter() {
let path = unsafe { sdl3_sys::joystick::SDL_GetJoystickPathForID(*joystick) };
if !path.is_null()
&& unsafe { std::ffi::CStr::from_ptr(path).to_str().unwrap() }
== controller_assignment
&& unsafe { sdl3_sys::joystick::SDL_GetJoystickFromID(*joystick) }.is_null()
&& unsafe { sdl3_sys::gamepad::SDL_GetGamepadFromID(*joystick) }.is_null()
{
@@ -932,20 +1103,25 @@ pub fn init(ui: &mut ui::Ui) {
}
}
if joystick_id != 0 {
let profile_name = ui.config.input.input_profile_binding[i].clone();
let profile = ui.config.input.input_profiles.get(&profile_name).unwrap();
if joystick_id != 0
&& let Some(profile) = ui
.config
.input
.input_profiles
.get(&ui.config.input.input_profile_binding[i])
{
if !profile.dinput {
let gamepad = unsafe { sdl3_sys::gamepad::SDL_OpenGamepad(joystick_id) };
if gamepad.is_null() {
println!("could not connect gamepad: {joystick_id}")
eprintln!("could not connect gamepad: {}", u32::from(joystick_id))
} else {
ui.input.controllers[i].game_controller = gamepad;
ui.input.controllers[i].guid =
unsafe { sdl3_sys::gamepad::SDL_GetGamepadGUIDForID(joystick_id) };
let properties =
unsafe { sdl3_sys::gamepad::SDL_GetGamepadProperties(gamepad) };
if properties == 0 {
println!("could not get gamepad properties");
eprintln!("could not get gamepad properties");
}
ui.input.controllers[i].rumble = unsafe {
sdl3_sys::properties::SDL_GetBooleanProperty(
@@ -958,13 +1134,15 @@ pub fn init(ui: &mut ui::Ui) {
} else {
let joystick = unsafe { sdl3_sys::joystick::SDL_OpenJoystick(joystick_id) };
if joystick.is_null() {
println!("could not connect joystick: {joystick_id}")
eprintln!("could not connect joystick: {}", u32::from(joystick_id))
} else {
ui.input.controllers[i].joystick = joystick;
ui.input.controllers[i].guid =
unsafe { sdl3_sys::joystick::SDL_GetJoystickGUIDForID(joystick_id) };
let properties =
unsafe { sdl3_sys::joystick::SDL_GetJoystickProperties(joystick) };
if properties == 0 {
println!("could not get joystick properties");
eprintln!("could not get joystick properties");
}
ui.input.controllers[i].rumble = unsafe {
sdl3_sys::properties::SDL_GetBooleanProperty(
@@ -976,7 +1154,7 @@ pub fn init(ui: &mut ui::Ui) {
}
}
} else {
println!("Could not bind assigned controller");
eprintln!("Could not bind assigned controller");
}
}
}
+172 -181
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, VruChannel, 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};
@@ -109,62 +109,67 @@ impl NetplayPages for NetplayJoin {
}
fn populate_server_names<T: ComponentHandle + NetplayPages + 'static>(weak: slint::Weak<T>) {
let task = reqwest::Client::new()
let task = ui::WEB_CLIENT
.get("https://dispatch.gopher64.com/getRegions")
.header("netplay-id", EMU_NAME)
.header("netplay-id", env!("NETPLAY_ID"))
.send();
tokio::spawn(async move {
let mut local_servers: Vec<(String, String)> = vec![];
let broadcast_sock = tokio::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0))
.await
.unwrap();
broadcast_sock.set_broadcast(true).unwrap();
let data: [u8; 1] = [1];
broadcast_sock
.send_to(&data, (std::net::Ipv4Addr::BROADCAST, 45000))
.await
.unwrap();
let mut buffer = [0; 1024];
if let Ok(Ok(result)) = tokio::time::timeout(
std::time::Duration::from_millis(200),
broadcast_sock.recv(&mut buffer),
)
.await
if let Ok(broadcast_sock) =
tokio::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0)).await
&& broadcast_sock.set_broadcast(true).is_ok()
{
let data: std::collections::HashMap<String, String> =
serde_json::from_slice(&buffer[..result]).unwrap();
for server in data.iter() {
local_servers.push((server.0.into(), server.1.into()));
let data: [u8; 1] = [1];
if let Err(e) = broadcast_sock
.send_to(&data, (std::net::Ipv4Addr::BROADCAST, 45000))
.await
{
eprintln!("Error sending broadcast: {}", e);
}
let mut buffer = [0; 1024];
if let Ok(Ok(result)) = tokio::time::timeout(
std::time::Duration::from_millis(200),
broadcast_sock.recv(&mut buffer),
)
.await
&& let Ok(data) = serde_json::from_slice::<std::collections::HashMap<String, String>>(
&buffer[..result],
)
{
for server in data.iter() {
local_servers.push((server.0.into(), server.1.into()));
}
}
} else {
eprintln!("Error creating netplay broadcast socket");
}
let response = task.await;
if let Ok(response) = response
let public_servers = if let Ok(response) = response
&& let Ok(servers) = response.json::<Vec<String>>().await
{
weak.upgrade_in_event_loop(move |handle| {
let server_names: slint::VecModel<slint::SharedString> = slint::VecModel::default();
let server_urls: slint::VecModel<slint::SharedString> = slint::VecModel::default();
for local_server in local_servers {
server_names.push(local_server.0.into());
server_urls.push(local_server.1.into());
}
for server in servers {
server_names.push(server.clone().into());
server_urls.push(format!("dispatcher:{server}").into());
}
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));
})
.unwrap();
}
servers
} else {
vec![]
};
weak.upgrade_in_event_loop(move |handle| {
let server_names: slint::VecModel<slint::SharedString> = slint::VecModel::default();
let server_urls: slint::VecModel<slint::SharedString> = slint::VecModel::default();
for local_server in local_servers {
server_names.push(local_server.0.into());
server_urls.push(local_server.1.into());
}
for server in public_servers {
server_names.push(server.clone().into());
server_urls.push(format!("dispatcher:{server}").into());
}
server_names.push("Custom".into());
handle.refresh_sessions();
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();
});
}
@@ -172,10 +177,7 @@ fn select_rom<T: ComponentHandle + NetplayPages + 'static>(
weak: slint::Weak<T>,
rom_dir: slint::SharedString,
) {
let select_rom = if !rom_dir.is_empty()
&& let Ok(exists) = std::fs::exists(&rom_dir)
&& exists
{
let select_rom = if !rom_dir.is_empty() && std::fs::exists(&rom_dir).unwrap_or(false) {
rfd::AsyncFileDialog::new().set_directory(rom_dir)
} else {
rfd::AsyncFileDialog::new()
@@ -190,7 +192,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()
{
@@ -222,10 +224,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();
}
@@ -249,7 +251,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>,
) {
@@ -293,10 +296,10 @@ pub fn setup_create_window(
let weak_dialog = message_dialog.as_weak();
message_dialog.show().unwrap();
let task = reqwest::Client::new()
let task = ui::WEB_CLIENT
.get("https://dispatch.gopher64.com/createServer")
.query(&[("region", server_url.strip_prefix("dispatcher:").unwrap())])
.header("netplay-id", EMU_NAME)
.header("netplay-id", env!("NETPLAY_ID"))
.send();
let netplay_read_sender = netplay_read_sender.clone();
let netplay_write_receiver = netplay_write_receiver.resubscribe();
@@ -305,6 +308,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
@@ -336,6 +340,7 @@ pub fn setup_create_window(
game_cheats.to_string(),
password.to_string(),
game_settings,
ra_settings,
weak_app,
weak.clone(),
);
@@ -365,6 +370,7 @@ pub fn setup_create_window(
game_cheats.to_string(),
password.to_string(),
game_settings.clone(),
ra_settings.clone(),
weak_app.clone(),
weak.clone(),
);
@@ -446,9 +452,9 @@ fn show_netplay_error(message: String) {
}
fn update_sessions(weak: slint::Weak<NetplayJoin>) {
let task = reqwest::Client::new()
let task = ui::WEB_CLIENT
.get("https://dispatch.gopher64.com/getServers")
.header("netplay-id", EMU_NAME)
.header("netplay-id", env!("NETPLAY_ID"))
.send();
tokio::spawn(async move {
let mut dispatcher_servers = std::collections::HashMap::new();
@@ -491,6 +497,7 @@ fn update_sessions(weak: slint::Weak<NetplayJoin>) {
tokio::spawn(async move {
let mut sessions = vec![];
let mut room_urls = vec![];
let mut room_ports = vec![];
for (server_name, server_url) in servers.iter() {
if let Ok(Ok((socket, _response))) = tokio::time::timeout(
std::time::Duration::from_secs(2),
@@ -513,7 +520,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
@@ -533,6 +546,7 @@ fn update_sessions(weak: slint::Weak<NetplayJoin>) {
for room in rooms {
let mut session = vec![];
room_urls.push(server_url.into());
room_ports.push(room.port.unwrap());
session.push(slint::StandardListViewItem::from(
slint::SharedString::from(server_name),
@@ -569,21 +583,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()),
)));
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);
@@ -605,7 +617,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>,
) {
@@ -627,7 +640,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,
@@ -635,7 +648,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),
@@ -688,13 +707,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,
);
@@ -732,81 +751,40 @@ fn join_session(
player_name: String,
game_hash: String,
password: String,
room_name: String,
fullscreen: bool,
room_port: i32,
ra_settings: RASettings,
weak_app: slint::Weak<AppWindow>,
weak: slint::Weak<NetplayJoin>,
) {
tokio::spawn(async move {
let now_utc = chrono::Utc::now().timestamp_millis().to_string();
let hasher = Sha256::new().chain_update(&now_utc).chain_update(EMU_NAME);
let request_rooms = NetplayMessage {
message_type: "request_get_rooms".to_string(),
player_name: None,
client_sha: None,
netplay_version: Some(NETPLAY_VERSION),
player_names: None,
emulator: Some(EMU_NAME.to_string()),
accept: None,
rooms: None,
message: None,
auth_time: Some(now_utc),
auth: Some(format!("{:x}", hasher.finalize())),
room: None,
};
netplay_write_sender.send(Some(request_rooms)).unwrap();
let mut port = 0;
match tokio::time::timeout(
std::time::Duration::from_secs(2),
netplay_read_receiver.recv(),
)
.await
{
Ok(Ok(message)) => {
if message.message_type == "reply_get_rooms"
&& message.accept.unwrap() == 0
&& let Some(rooms) = message.rooms
{
for room in rooms {
if room.room_name.unwrap() == room_name {
port = room.port.unwrap_or(0);
}
}
}
}
Ok(Err(err)) => {
panic!("netplay_read_receiver error: {err}");
}
Err(_) => {
weak.upgrade_in_event_loop(move |handle| {
handle.set_pending_session(false);
show_netplay_error("Server did not respond".to_string());
})
.unwrap();
}
}
let join_room = NetplayMessage {
message_type: "request_join_room".to_string(),
player_name: Some(player_name),
client_sha: Some(env!("GIT_HASH").to_string()),
netplay_version: None,
emulator: None,
client_sha: Some(env!("GIT_DESCRIBE").to_string()),
netplay_version: Some(NETPLAY_VERSION),
emulator: Some(EMU_NAME.to_string()),
accept: None,
message: None,
rooms: None,
auth_time: None,
player_names: None,
auth: None,
auth_time: Some(now_utc),
auth: Some(
hasher
.finalize()
.iter()
.map(|b| format!("{:02x}", b))
.collect(),
),
room: Some(NetplayRoom {
room_name: None,
password: Some(password),
game_name: None,
md5: Some(game_hash),
protected: None,
port: Some(port),
port: Some(room_port),
features: None,
buffer_target: None,
}),
@@ -852,13 +830,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,
);
@@ -898,8 +876,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>,
) {
@@ -912,7 +890,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();
@@ -1069,17 +1046,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();
@@ -1110,37 +1093,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,
},
VruChannel {
vru_window_notifier: None,
vru_word_receiver: None,
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);
}
@@ -1149,7 +1128,7 @@ fn setup_wait_window(
}
}
_ => {
println!("Unknown netplay message type: {}", response.message_type);
eprintln!("Unknown netplay message type: {}", response.message_type);
}
},
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
@@ -1167,8 +1146,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): (
@@ -1200,7 +1179,7 @@ pub fn setup_join_window(
});
let weak = join_window.as_weak();
join_window.on_join_session(
move |player_name, game_hash, password, room_url, room_name| {
move |player_name, game_hash, password, room_url, room_port| {
let _ = netplay_write_sender.send(None); // close current websocket if any
manage_websocket(
room_url.to_string(),
@@ -1215,8 +1194,8 @@ pub fn setup_join_window(
player_name.to_string(),
game_hash.to_string(),
password.to_string(),
room_name.to_string(),
fullscreen,
room_port,
ra_settings.clone(),
weak_app.clone(),
weak.clone(),
);
@@ -1226,24 +1205,28 @@ pub fn setup_join_window(
join_window.show().unwrap();
}
pub fn netplay_window(app: &AppWindow, controller_paths: &[Option<String>]) {
pub fn netplay_window(app: &AppWindow) {
let weak_create = app.as_weak();
let weak_app = app.as_weak();
let controller_paths_create = controller_paths.to_owned();
app.on_create_session_button_clicked(move || {
let controller_paths = controller_paths_create.clone();
let weak_app = weak_app.clone();
weak_create
.upgrade_in_event_loop(move |handle| {
let create_window = NetplayCreate::new().unwrap();
save_settings(&handle, &controller_paths);
save_settings(&handle);
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,
@@ -1254,18 +1237,21 @@ pub fn netplay_window(app: &AppWindow, controller_paths: &[Option<String>]) {
let weak_join = app.as_weak();
let weak_app = app.as_weak();
let controller_paths_join = controller_paths.to_owned();
app.on_join_session_button_clicked(move || {
let controller_paths = controller_paths_join.clone();
let weak_app = weak_app.clone();
weak_join
.upgrade_in_event_loop(move |handle| {
let join_window = NetplayJoin::new().unwrap();
save_settings(&handle, &controller_paths);
save_settings(&handle);
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,
);
})
@@ -1273,9 +1259,14 @@ pub fn netplay_window(app: &AppWindow, controller_paths: &[Option<String>]) {
});
app.on_netplay_discord_button_clicked(move || {
open::that_detached("https://discord.gg/JyW6ZgBUyS").unwrap();
if let Err(e) = open::that_detached("https://discord.gg/JyW6ZgBUyS") {
eprintln!("Error opening Discord: {}", e);
}
});
app.on_netplay_feedback_button_clicked(move || {
open::that_detached("https://github.com/gopher64/gopher64/discussions/453").unwrap();
if let Err(e) = open::that_detached("https://github.com/gopher64/gopher64/discussions/453")
{
eprintln!("Error opening feedback: {}", e);
}
});
}
+128
View File
@@ -0,0 +1,128 @@
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,
}
};
if let Ok(f) = std::fs::File::create(&file_path)
&& let Err(e) = serde_json::to_writer_pretty(f, &raconfig)
{
eprintln!("Error writing RA config: {}", e);
}
});
app.on_ra_games_clicked(move || {
if let Err(e) =
open::that_detached("https://retroachievements.org/system/2-nintendo-64/games")
{
eprintln!("Error opening games: {}", e);
}
});
app.on_ra_show_profile_clicked(move || {
if let Err(e) = open::that_detached(format!(
"https://retroachievements.org/user/{}",
retroachievements::get_username().unwrap_or_default()
)) {
eprintln!("Error opening profile: {}", e);
}
});
}
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();
});
}
+148 -91
View File
@@ -28,13 +28,13 @@ pub struct Paths {
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Save {
pub data: Vec<u8>,
pub written: bool,
pub write_pending: bool,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct RomSave {
pub data: std::collections::HashMap<u32, u8>,
pub written: bool,
pub write_pending: bool,
}
#[derive(serde::Serialize, serde::Deserialize)]
@@ -49,8 +49,9 @@ pub struct Saves {
}
fn get_save_type(rom: &[u8], game_id: &str) -> Vec<SaveTypes> {
let header_type = std::str::from_utf8(rom[0x3C..0x3E].try_into().unwrap());
if header_type.is_ok() && header_type.unwrap() == "ED" {
if let Ok(header_type) = std::str::from_utf8(rom[0x3C..0x3E].try_into().unwrap())
&& header_type == "ED"
{
let save_type = rom[0x3F] >> 4;
match save_type {
0 => return vec![],
@@ -135,13 +136,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 +164,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>) {
@@ -231,8 +229,10 @@ pub fn load_saves(ui: &mut ui::Ui, netplay: &mut Option<netplay::Netplay>) {
ui.storage.saves.sdcard.data = sdcard;
}
let romsave = std::fs::read(&ui.storage.paths.romsave_file_path);
if let Ok(romsave) = romsave {
ui.storage.saves.romsave.data = postcard::from_bytes(romsave.as_ref()).unwrap();
if let Ok(romsave) = romsave
&& let Ok(romsave_data) = postcard::from_bytes(romsave.as_ref())
{
ui.storage.saves.romsave.data = romsave_data;
}
}
@@ -264,8 +264,10 @@ pub fn load_saves(ui: &mut ui::Ui, netplay: &mut Option<netplay::Netplay>) {
);
let mut compressed_sd = Vec::new();
if !ui.storage.saves.sdcard.data.is_empty() {
compressed_sd = compress_file(&[(&ui.storage.saves.sdcard.data, "save")]);
if !ui.storage.saves.sdcard.data.is_empty()
&& let Ok(file_data) = compress_file(&[(&ui.storage.saves.sdcard.data, "save")])
{
compressed_sd = file_data;
}
netplay::send_save(
netplay.as_mut().unwrap(),
@@ -275,11 +277,11 @@ pub fn load_saves(ui: &mut ui::Ui, netplay: &mut Option<netplay::Netplay>) {
);
let mut compressed_romsave = Vec::new();
if !ui.storage.saves.romsave.data.is_empty() {
compressed_romsave = compress_file(&[(
&postcard::to_stdvec(&ui.storage.saves.romsave.data).unwrap(),
"save",
)]);
if !ui.storage.saves.romsave.data.is_empty()
&& let Ok(file_data) = postcard::to_stdvec(&ui.storage.saves.romsave.data)
&& let Ok(compressed_file_data) = compress_file(&[(&file_data, "save")])
{
compressed_romsave = compressed_file_data;
}
netplay::send_save(
netplay.as_mut().unwrap(),
@@ -311,58 +313,70 @@ pub fn load_saves(ui: &mut ui::Ui, netplay: &mut Option<netplay::Netplay>) {
let mut compressed_sd = Vec::new();
netplay::receive_save(netplay.as_mut().unwrap(), "img", &mut compressed_sd);
if !compressed_sd.is_empty() {
ui.storage.saves.sdcard.data = decompress_file(&compressed_sd, "save");
if !compressed_sd.is_empty()
&& let Ok(file_data) = decompress_file(&compressed_sd, "save")
{
ui.storage.saves.sdcard.data = file_data;
}
let mut compressed_romsave = Vec::new();
netplay::receive_save(netplay.as_mut().unwrap(), "rom", &mut compressed_romsave);
if !compressed_romsave.is_empty() {
let romsave_bytes = decompress_file(&compressed_romsave, "save");
ui.storage.saves.romsave.data = postcard::from_bytes(&romsave_bytes).unwrap();
if !compressed_romsave.is_empty()
&& let Ok(romsave_bytes) = decompress_file(&compressed_romsave, "save")
&& let Ok(romsave_data) = postcard::from_bytes(&romsave_bytes)
{
ui.storage.saves.romsave.data = romsave_data;
}
}
}
}
pub fn decompress_file(input: &[u8], name: &str) -> Vec<u8> {
let mut decompressed_file = Vec::new();
{
let mut reader = zip::ZipArchive::new(std::io::Cursor::new(input)).unwrap();
let mut file = reader.by_name(name).unwrap();
file.read_to_end(&mut decompressed_file).unwrap();
}
decompressed_file
pub fn format_saves(device: &mut device::Device) {
device::cart::format_eeprom(device);
device::cart::sram::format_sram(device);
device::cart::sram::format_flash(device);
device::controller::mempak::format_mempak(device);
}
pub fn compress_file(data: &[(&[u8], &str)]) -> Vec<u8> {
pub fn decompress_file(input: &[u8], name: &str) -> Result<Vec<u8>, zip::result::ZipError> {
let mut decompressed_file = Vec::new();
{
if let Ok(mut reader) = zip::ZipArchive::new(std::io::Cursor::new(input))
&& let Ok(mut file) = reader.by_name(name)
&& file.read_to_end(&mut decompressed_file).is_ok()
{
return Ok(decompressed_file);
}
}
Err(zip::result::ZipError::FileNotFound)
}
pub fn compress_file(data: &[(&[u8], &str)]) -> Result<Vec<u8>, zip::result::ZipError> {
let mut compressed_file = Vec::new();
{
let mut writer = zip::ZipWriter::new(std::io::Cursor::new(&mut compressed_file));
for item in data {
writer
if writer
.start_file(
item.1,
zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Zstd)
.compression_level(Some(1)),
)
.unwrap();
writer.write_all(item.0).unwrap();
.is_err()
|| writer.write_all(item.0).is_err()
{
return Err(zip::result::ZipError::FileNotFound);
}
}
}
compressed_file
}
fn write_rom_save(ui: &ui::Ui) {
let data = postcard::to_stdvec(&ui.storage.saves.romsave.data).unwrap();
std::fs::write(ui.storage.paths.romsave_file_path.clone(), data).unwrap();
Ok(compressed_file)
}
fn writeback_sdcard(device: &mut device::Device) {
let length;
let save_data: &[u8];
if device.ui.storage.saves.eeprom.written {
if device.ui.storage.saves.eeprom.write_pending {
if device
.ui
.storage
@@ -374,12 +388,15 @@ fn writeback_sdcard(device: &mut device::Device) {
length = 4;
}
save_data = device.ui.storage.saves.eeprom.data.as_ref();
} else if device.ui.storage.saves.sram.written {
device.ui.storage.saves.eeprom.write_pending = false;
} else if device.ui.storage.saves.sram.write_pending {
length = device.ui.storage.saves.sram.data.len() / 512;
save_data = device.ui.storage.saves.sram.data.as_ref();
} else if device.ui.storage.saves.flash.written {
device.ui.storage.saves.sram.write_pending = false;
} else if device.ui.storage.saves.flash.write_pending {
length = device.ui.storage.saves.flash.data.len() / 512;
save_data = device.ui.storage.saves.flash.data.as_ref();
device.ui.storage.saves.flash.write_pending = false;
} else {
return;
}
@@ -389,71 +406,111 @@ fn writeback_sdcard(device: &mut device::Device) {
device.ui.storage.saves.sdcard.data[offset..offset + 512]
.copy_from_slice(&save_data[i * 512..(i + 1) * 512]);
}
device.ui.storage.saves.sdcard.written = true;
device.ui.storage.saves.sdcard.write_pending = true;
}
pub fn write_saves(device: &mut device::Device) {
if device.netplay.is_none() || device.netplay.as_ref().unwrap().player_number == 0 {
if device.ui.storage.saves.write_to_disk {
if device.ui.storage.saves.eeprom.written {
write_save(&device.ui, SaveTypes::Eeprom16k)
if device.ui.storage.saves.eeprom.write_pending {
write_save(&mut device.ui, SaveTypes::Eeprom16k)
}
if device.ui.storage.saves.sram.written {
write_save(&device.ui, SaveTypes::Sram)
if device.ui.storage.saves.sram.write_pending {
write_save(&mut device.ui, SaveTypes::Sram)
}
if device.ui.storage.saves.flash.written {
write_save(&device.ui, SaveTypes::Flash)
if device.ui.storage.saves.flash.write_pending {
write_save(&mut device.ui, SaveTypes::Flash)
}
if device.ui.storage.saves.romsave.written {
write_save(&device.ui, SaveTypes::Romsave)
if device.ui.storage.saves.romsave.write_pending {
write_save(&mut device.ui, SaveTypes::Romsave)
}
} else {
writeback_sdcard(device)
}
if device.ui.storage.saves.mempak.written {
write_save(&device.ui, SaveTypes::Mempak)
if device.ui.storage.saves.mempak.write_pending {
write_save(&mut device.ui, SaveTypes::Mempak)
}
if device.ui.storage.saves.sdcard.written {
write_save(&device.ui, SaveTypes::Sdcard)
if device.ui.storage.saves.sdcard.write_pending {
write_save(&mut device.ui, SaveTypes::Sdcard)
}
}
}
fn write_save(ui: &ui::Ui, save_type: SaveTypes) {
let path: &std::path::Path;
let data: &Vec<u8>;
pub fn save_event(device: &mut device::Device) {
write_saves(device);
}
pub fn schedule_save(device: &mut device::Device, save_type: SaveTypes) {
match save_type {
SaveTypes::Eeprom4k | SaveTypes::Eeprom16k => {
path = ui.storage.paths.eep_file_path.as_ref();
data = ui.storage.saves.eeprom.data.as_ref();
device.ui.storage.saves.eeprom.write_pending = true;
}
SaveTypes::Sram => {
path = ui.storage.paths.sra_file_path.as_ref();
data = ui.storage.saves.sram.data.as_ref();
device.ui.storage.saves.sram.write_pending = true;
}
SaveTypes::Flash => {
path = ui.storage.paths.fla_file_path.as_ref();
data = ui.storage.saves.flash.data.as_ref();
device.ui.storage.saves.flash.write_pending = true;
}
SaveTypes::Mempak => {
path = ui.storage.paths.pak_file_path.as_ref();
data = ui.storage.saves.mempak.data.as_ref();
device.ui.storage.saves.mempak.write_pending = true;
}
SaveTypes::Sdcard => {
path = ui.storage.paths.sdcard_file_path.as_ref();
data = ui.storage.saves.sdcard.data.as_ref();
device.ui.storage.saves.sdcard.write_pending = true;
}
SaveTypes::Romsave => {
write_rom_save(ui);
return;
device.ui.storage.saves.romsave.write_pending = true;
}
}
let result = std::fs::write(path, data);
if result.is_err() {
panic!(
"could not save {} {}",
path.display(),
result.err().unwrap()
)
}
device::events::create_event(
device,
device::events::EVENT_TYPE_SAVE,
device.cpu.clock_rate, // 1 second
);
}
fn write_save(ui: &mut ui::Ui, save_type: SaveTypes) {
let save_path: std::path::PathBuf;
let save_data: Vec<u8>;
match save_type {
SaveTypes::Eeprom4k | SaveTypes::Eeprom16k => {
save_path = ui.storage.paths.eep_file_path.clone();
save_data = ui.storage.saves.eeprom.data.clone();
ui.storage.saves.eeprom.write_pending = false;
}
SaveTypes::Sram => {
save_path = ui.storage.paths.sra_file_path.clone();
save_data = ui.storage.saves.sram.data.clone();
ui.storage.saves.sram.write_pending = false;
}
SaveTypes::Flash => {
save_path = ui.storage.paths.fla_file_path.clone();
save_data = ui.storage.saves.flash.data.clone();
ui.storage.saves.flash.write_pending = false;
}
SaveTypes::Mempak => {
save_path = ui.storage.paths.pak_file_path.clone();
save_data = ui.storage.saves.mempak.data.clone();
ui.storage.saves.mempak.write_pending = false;
}
SaveTypes::Sdcard => {
save_path = ui.storage.paths.sdcard_file_path.clone();
save_data = ui.storage.saves.sdcard.data.clone();
ui.storage.saves.sdcard.write_pending = false;
}
SaveTypes::Romsave => {
save_path = ui.storage.paths.romsave_file_path.clone();
ui.storage.saves.romsave.write_pending = false;
if let Ok(rom_save) = postcard::to_stdvec(&ui.storage.saves.romsave.data) {
save_data = rom_save;
} else {
eprintln!("Error serializing rom save data");
return;
};
}
}
tokio::spawn(async move {
if let Err(e) = tokio::fs::write(save_path, save_data).await {
eprintln!("Error writing save: {}", e);
}
});
}
+3 -49
View File
@@ -26,56 +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
},
ui::gui::VruChannel {
vru_window_notifier: None,
vru_word_receiver: None,
},
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();
@@ -150,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();
}
@@ -171,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>,
@@ -195,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;
}
+146 -72
View File
@@ -1,12 +1,44 @@
#![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,
ssaa: device.ui.config.video.ssaa,
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,71 +50,101 @@ 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.ssaa {
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 {
std::ffi::CStr::from_ptr(sdl3_sys::error::SDL_GetError())
.to_str()
.unwrap()
});
let err = sdl3_sys::error::SDL_GetError();
panic!(
"Could not create window: {}",
if err.is_null() {
"Unknown error"
} else {
unsafe { std::ffi::CStr::from_ptr(err).to_str().unwrap() }
}
);
}
if !unsafe { sdl3_sys::video::SDL_ShowWindow(device.ui.video.window) } {
panic!("Could not show window: {}", unsafe {
std::ffi::CStr::from_ptr(sdl3_sys::error::SDL_GetError())
.to_str()
.unwrap()
});
let err = sdl3_sys::error::SDL_GetError();
panic!(
"Could not show window: {}",
if err.is_null() {
"Unknown error"
} else {
unsafe { std::ffi::CStr::from_ptr(err).to_str().unwrap() }
}
);
}
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) {
@@ -93,10 +155,13 @@ pub fn close(ui: &ui::Ui) {
}
pub fn update_screen() {
// when the window is closed, running is set to false
unsafe { rdp_update_screen() }
}
pub fn render_frame() {
unsafe { rdp_render_frame() }
}
pub fn state_size() -> usize {
unsafe { rdp_state_size() }
}
@@ -106,21 +171,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);
@@ -130,9 +181,22 @@ pub fn load_state(device: &mut device::Device, rdp_state: *const u8) {
}
}
pub fn check_callback(device: &mut device::Device) -> bool {
pub fn pause_loop(frame_time: f64) {
let mut paused = true;
let mut frame_advance = false;
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;
}
}
pub fn check_callback(device: &mut device::Device) -> (bool, bool) {
let mut speed_limiter_toggled = false;
let mut callback = unsafe { rdp_check_callback() };
let callback = unsafe { rdp_check_callback() };
device.cpu.running = callback.emu_running;
if device.netplay.is_none() {
if callback.save_state {
@@ -140,21 +204,28 @@ pub fn check_callback(device: &mut device::Device) -> 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;
}
while callback.paused && !callback.frame_advance {
std::thread::sleep(std::time::Duration::from_secs_f64(1.0 / 60.0));
unsafe { sdl3_sys::events::SDL_PumpEvents() };
callback = unsafe { rdp_check_callback() };
}
}
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
@@ -162,14 +233,14 @@ pub fn check_callback(device: &mut device::Device) -> 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);
} else if callback.raise_volume {
ui::audio::raise_audio_volume(&mut device.ui);
}
speed_limiter_toggled
(speed_limiter_toggled, callback.paused)
}
pub fn set_register(reg: u32, value: u32) {
@@ -182,8 +253,15 @@ pub fn process_rdp_list() -> u64 {
unsafe { rdp_process_commands() }
}
pub fn onscreen_message(_ui: &ui::Ui, message: &str) {
unsafe { rdp_onscreen_message(std::ffi::CString::new(message).unwrap().as_ptr()) };
pub fn check_framebuffers(address: u32, length: u32) {
unsafe { rdp_check_framebuffers(address, length) }
}
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(
@@ -196,12 +274,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);
+42 -15
View File
@@ -1,21 +1,48 @@
pub fn prompt_for_match(
words: &[String],
window_notifier: &tokio::sync::mpsc::Sender<Option<Vec<String>>>,
word_index_receiver: &mut tokio::sync::mpsc::Receiver<String>,
) -> u16 {
use crate::retroachievements;
use crate::ui::gui;
use slint::ComponentHandle;
pub fn prompt_for_match(words: &[String], frame_time: f64) -> u16 {
let mut dedup_words = words.to_owned();
dedup_words.sort();
dedup_words.dedup();
window_notifier.try_send(Some(dedup_words)).unwrap();
let mut result = word_index_receiver.try_recv();
while result.is_err() {
result = word_index_receiver.try_recv();
std::thread::sleep(std::time::Duration::from_secs_f64(1.0 / 60.0));
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;
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let vru_dialog = gui::VruDialog::new().unwrap();
let vru_dialog_weak = vru_dialog.as_weak();
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.set_words(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(
dedup_words
.iter()
.map(|x| x.into())
.collect::<Vec<slint::SharedString>>(),
),
)));
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();
},
);
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;
}
}
}
0x7FFF