Compare commits

..

220 Commits

Author SHA1 Message Date
Luke Street 595a6f1c9e Enable DoF (+ setting) & fix texture cache leak 2026-04-21 14:52:26 -06:00
MelonSpeedruns 18d70df188 Small Imgui changes for better visibility by end user (#473)
Co-authored-by: MelonSpeedruns <melonspeedruns@stratobox.net>
2026-04-21 10:17:56 -06:00
TakaRikka 46f6dc67c1 make file select card wait times obey instantSaves setting 2026-04-21 00:46:22 -07:00
madeline 6267b79da3 Merge branch 'main' of https://github.com/TakaRikka/dusk 2026-04-20 20:22:12 -07:00
madeline 62f3d09076 fix incorrect eldin field name on map 2026-04-20 20:22:06 -07:00
Luke Street 4b3c559df3 CMake: Don't build a game static lib 2026-04-20 20:47:03 -06:00
Luke Street 30a99c22f1 Reorganize ImGui menus (#456)
* Reorganize ImGui menus

* Fix crash_reporting.cpp

* Update aurora
2026-04-20 20:45:16 -06:00
TakaRikka 360cb37028 Merge pull request #463 from TwilitRealm/fix/interp_map_save
Frame Interp: d_menu_save and d_menu_fmap2D
2026-04-20 19:17:56 -07:00
TakaRikka 4b6b41a6aa Merge pull request #461 from TwilitRealm/26-04-21-thp-shutdown-crash
Fix THP shutdown crash
2026-04-20 18:45:29 -07:00
Pheenoh faa8618124 Fix frame interpolation on save and world map menus 2026-04-20 19:37:38 -06:00
PJB3005 89acf923e0 Fix THP shutdown crash
Cleanly shut down movie player threads on exit.

I'm not 100% fond of having to manually insert a call like this into the shutdown logic, but the other solution is shutting down all processes and that's likely to result in causing more crashes.
2026-04-21 01:11:49 +02:00
Irastris c42a33154c Frame Interp: Fix flickering refraction in cutscenes 2026-04-20 18:11:27 -04:00
madeline a074e30147 Merge branch 'main' of https://github.com/TakaRikka/dusk 2026-04-20 06:45:09 -07:00
madeline 2ce272d586 fix some menus scaling wrong 2026-04-20 06:45:05 -07:00
TakaRikka e1636e20bd Merge branch 'main' of https://github.com/TakaRikka/dusk 2026-04-20 06:14:28 -07:00
TakaRikka 9904720e5a better lv5key fix 2026-04-20 06:14:21 -07:00
madeline 9910320fb4 Merge branch 'main' of https://github.com/TakaRikka/dusk 2026-04-20 06:10:22 -07:00
madeline b43a9e2ccc menu cursor resizing fixes #313 closes #314 2026-04-20 06:10:18 -07:00
TakaRikka 23d81492e6 Merge branch 'main' of https://github.com/TakaRikka/dusk 2026-04-20 06:02:11 -07:00
TakaRikka 65b0ec3f90 add failsafes to lv5key to avoid reverse unlock softlock 2026-04-20 06:02:03 -07:00
madeline 91248d10db disable water refraction in debug, fixes #422 2026-04-20 05:36:31 -07:00
TakaRikka f76a4d7087 add dMsgUnit_c::setTag bug fix 2026-04-20 05:15:02 -07:00
TakaRikka c7b609945b fix option menu widescreen regression 2026-04-20 04:48:58 -07:00
TakaRikka 6ca6ea8065 fix crawl arrow direction with mirror mode 2026-04-20 04:16:35 -07:00
TakaRikka 315f621176 Merge pull request #450 from TwilitRealm/discord
Discord rich presence
2026-04-20 03:36:03 -07:00
madeline 10d9a818c2 fix apple ci? 2026-04-20 00:31:41 -07:00
madeline e3761784af rich presence 2026-04-20 00:21:09 -07:00
Phillip Stephens d85556a063 Use vendored SDL on ubuntu for libusb (#448) 2026-04-19 23:27:29 -06:00
madeline c43f33a5bc Merge branch 'main' of https://github.com/TakaRikka/dusk 2026-04-19 21:43:32 -07:00
madeline e63897d1f7 add --console to launch.json 2026-04-19 21:43:29 -07:00
Howard Luck 35a69e7ff1 Frame interp pacing/camera cuts, King Bulblin reins, and crash fixes
* misc fixes round 1

* kb1 fixes

* fixes for encounter/ira
2026-04-20 00:34:13 -04:00
Luke Street 8b9f09bda5 Frame interp: Fix cloud shadow flickering (#446) 2026-04-19 21:31:34 -06:00
Jasper St. Pierre 32cddc725b mirror flicker fix 2 2026-04-19 16:21:30 -07:00
Jasper St. Pierre 55eec3b0de hitstun camera interp fix 2026-04-19 16:01:51 -07:00
TakaRikka 1cf14f176c Merge pull request #438 from TwilitRealm/frame-interp-simplify
Frame interp simplify
2026-04-19 14:58:38 -07:00
TakaRikka 74550b031f Merge pull request #436 from TwilitRealm/feat/pause_on_unfocus
Add pause on unfocus feature
2026-04-19 14:26:58 -07:00
Jasper St. Pierre 2f84f0eaa4 j3d small cleanup 2026-04-19 14:16:38 -07:00
Jasper St. Pierre 3ed7ef1c88 fix stone shadow flicker 2026-04-19 13:22:00 -07:00
Jasper St. Pierre eace147652 mirror interp fix 2026-04-19 12:57:30 -07:00
Jasper St. Pierre 53c005c4f1 jutfader doc 2026-04-19 12:48:50 -07:00
Jasper St. Pierre a4ff97564c minor cleanup 2026-04-19 12:39:08 -07:00
Jasper St. Pierre c6beeba14f proct 2026-04-19 11:56:48 -07:00
Jasper St. Pierre e470278717 grass/flower/shadow interp fixes 2026-04-19 10:27:37 -07:00
Pheenoh 783230b654 Add pause on unfocus feature 2026-04-19 10:02:55 -06:00
TakaRikka 4e7711725a attempt to fix clawshot aim model position 2026-04-19 03:58:22 -07:00
Jasper St. Pierre e5bf7606ec run d_a_bg::draw on interp frames
to set up lights. this is a somewhat hacky workaround for lighting setup, but it might be good enough?
2026-04-19 03:19:44 -07:00
Jasper St. Pierre aa377cd5c1 add alternate to interp callbacks, flags on the leafdraw 2026-04-19 02:43:25 -07:00
Jasper St. Pierre bb9a88d7dc frame interp camera cleanups 2026-04-19 02:25:00 -07:00
Jasper St. Pierre 341e97ba82 simple shadow interp fix 2026-04-19 01:14:58 -07:00
Jasper St. Pierre 8d3cb51157 frame interp simplify 2026-04-18 23:26:28 -07:00
TakaRikka 7fff3b5ae0 Merge pull request #430 from TwilitRealm/fix/JAISeqMgr
Fix JAISeqMgr
2026-04-18 22:57:57 -07:00
TakaRikka 6beb73b7a6 Merge pull request #429 from TwilitRealm/fix/d_menu_fmap2d_interp
Fix d_menu_fmap2d interpolation for tears of light
2026-04-18 22:57:45 -07:00
Jasper St. Pierre c239a5a226 freelook fix 2026-04-18 22:45:54 -07:00
Jasper St. Pierre b3f8fecfe4 water interpolation fix 2026-04-18 22:17:35 -07:00
Phillip Stephens 6dcf4942f5 Update aurora for CARDFormat speedup 2026-04-18 21:51:28 -07:00
Phillip Stephens cd7e429a66 Fix memory card not properly attaching on init. (#433)
* Fix memory card not properly attaching on init.

Previously the card status was forced to ready which caused quite a bit of default state getting set properly,
reverting that and setting mCardCommand to `COMM_ATTACH_e` allows the memory card system to properly probe and
detect the card status.

A companion fix in aurora addresses the "Memory Card corrupted" error message.

* Update aurora
2026-04-18 21:33:02 -07:00
Pheenoh fcfcb35929 fix JAISeqMgr beginStartSeq_ to check free memory before allocation 2026-04-18 19:53:40 -06:00
Pheenoh 82b20be436 Merge remote-tracking branch 'origin/main' into fix/d_menu_fmap2d_interp 2026-04-18 19:34:57 -06:00
Pheenoh 33101d8050 fix minimap tear of light flash not respecting interpolation 2026-04-18 19:34:38 -06:00
TakaRikka 05160a968c Merge branch 'main' of https://github.com/TakaRikka/dusk 2026-04-18 18:08:40 -07:00
TakaRikka 623f29eb24 volume setting effects movie now 2026-04-18 18:08:33 -07:00
TakaRikka 0a3833818a Merge pull request #427 from TwilitRealm/fix/interp_d_file_select
Fix file select fade not rendering with frame interpolation
2026-04-18 16:17:54 -07:00
TakaRikka 4f276252a3 Merge branch 'main' of https://github.com/TakaRikka/dusk 2026-04-18 15:54:47 -07:00
TakaRikka d17c629ce0 fix bulblin eye glow 2026-04-18 15:54:42 -07:00
Pheenoh 5bcc969778 fix file select fade not rendering with frame interpolation 2026-04-18 16:53:48 -06:00
Luke Street 09e3e17aa6 Disable vendored dawn for x-macos-ci 2026-04-18 16:23:15 -06:00
Luke Street f02a39d921 Update aurora 2026-04-18 16:19:59 -06:00
TakaRikka effe5cb9e2 Merge pull request #426 from TwilitRealm/fix/cat_cs
More interpolation fixes
2026-04-18 13:44:05 -07:00
TakaRikka 767bb6826f Merge pull request #418 from TwilitRealm/unhackify-widescreen
Widescreen rework & IR scaling
2026-04-18 13:40:38 -07:00
Pheenoh a9f8595901 fix interpolation for cat cs, pop up 2d text 2026-04-18 14:27:01 -06:00
Luke Street 56239e77ff Update aurora 2026-04-18 14:16:24 -06:00
Luke Street 763cb7f92f Merge branch 'main' into unhackify-widescreen
# Conflicts:
#	src/dusk/imgui/ImGuiMenuGame.cpp
#	src/m_Do/m_Do_lib.cpp
2026-04-18 14:12:21 -06:00
Luke Street 85120648b1 Fix shadow draw viewport/scissor overflow 2026-04-18 14:04:28 -06:00
Luke Street b77ef63d5e Reintroduce x offset change for mDoLib_project 2026-04-18 13:34:22 -06:00
TakaRikka 45adce4af2 Merge pull request #424 from TwilitRealm/fix/button_combo_conflict
Fix quick transform conflicting with debug move combo
2026-04-18 12:27:02 -07:00
Luke Street 7b8f9c6f46 Fix blur scale in mDoGph_gInf_c::bloom_c::draw2 2026-04-18 13:08:14 -06:00
Luke Street 1c8bb1206e Unhackify mDoLib_project 2026-04-18 12:46:44 -06:00
Howard Luck 78d1596f66 Frame Interp: d_meter_haihai, d_msg_out_font, d_meter_button
* fix menu option haihai cursor and description icon animation speeds under frame interpolation

* also fix d_meter_button
2026-04-18 14:06:32 -04:00
Howard Luck 4cda2cd5f0 Merge pull request #417 from TwilitRealm/ira/data-folder
Add "Open Data Folder" to Game menu
2026-04-18 11:07:34 -06:00
Pheenoh d34be5143a fix quick transform conflicting with debug move combo 2026-04-18 10:20:55 -06:00
TakaRikka c00fc756e3 wip readme 2026-04-18 04:57:22 -07:00
TakaRikka 6ea3fef8c6 fix mirrored shops / projection 2026-04-18 03:38:16 -07:00
TakaRikka 8010e16cab add some cheats 2026-04-18 01:14:14 -07:00
Luke Street 2ec6f65572 Merge remote-tracking branch 'refs/remotes/origin/main' into unhackify-widescreen
# Conflicts:
#	src/m_Do/m_Do_main.cpp
2026-04-17 23:12:58 -06:00
Luke Street 5c20f527ac Widescreen rework & IR scaling 2026-04-17 23:11:43 -06:00
Irastris 3d9dfbd447 Add "Open Data Folder" to Game menu 2026-04-18 00:46:00 -04:00
Irastris 3a538d45cf Frame Interp: Game Clock Refactor & Ring Item Selection 2026-04-17 23:24:47 -04:00
Irastris 59d2014fb9 Frame Interp: UI Pacing Simplification 2026-04-17 23:24:47 -04:00
TakaRikka bc6c132d69 Merge pull request #413 from TwilitRealm/uninitialized-inko
add uninitialized variable fix
2026-04-17 15:04:08 -07:00
TakaRikka 83ddb2c6c8 Merge pull request #412 from TwilitRealm/feature/auto-hide-cursor
Auto-Hide Cursor
2026-04-17 15:03:46 -07:00
TakaRikka 3fe26f8dd6 Merge pull request #410 from TwilitRealm/instant-text
Hold B for instant text setting
2026-04-17 15:03:35 -07:00
Irastris 054c4384be Gyro: Add Subjectivity ProcIDs 2026-04-17 17:13:12 -04:00
Irastris 439cc936a1 Frame interp: wideZoom support 2026-04-17 16:19:20 -04:00
roeming 570d00ee8c Update src/d/actor/d_a_npc_inko.cpp
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2026-04-17 15:14:00 -04:00
roeming 7ae1818550 add uninitialized variable fix 2026-04-17 15:05:12 -04:00
MelonSpeedruns 4ddd243eca Auto-Hide Cursor after 3 seconds if F1 menu not open 2026-04-17 13:22:19 -04:00
Lurs edea6a1418 fix #260 and potentially more 2026-04-17 16:12:53 +02:00
gymnast86 9614614981 remove unecessary comment 2026-04-17 01:32:50 -07:00
gymnast86 4e433f646b Merge branch 'main' of https://github.com/TwilitRealm/dusk into instant-text 2026-04-17 01:30:19 -07:00
gymnast86 5ba04524eb implement instant text setting 2026-04-17 01:30:08 -07:00
CraftyBoss e566202c26 change android icon to not-midna icon 2026-04-16 23:58:27 -07:00
Luke Street f293281eb2 Merge pull request #408 from TwilitRealm/ci-2
macOS CI, iOS file picker, icons, safe area avoidance

Resolves #382
2026-04-17 00:00:34 -06:00
Luke Street e9689f9abb Disable macOS x86_64 for now 2026-04-16 23:47:52 -06:00
Luke Street 36bfce534c Only upload artifacts on tag builds 2026-04-16 23:46:49 -06:00
Luke Street 8ba1133e34 iOS file picker, ImGui drag scroll & more 2026-04-16 23:26:27 -06:00
Luke Street 2db4040843 Make HUD viewport respect SDL_GetWindowSafeArea 2026-04-16 22:47:02 -06:00
Luke Street f2848f0be0 Add iOS and macOS icons 2026-04-16 22:44:52 -06:00
Irastris 363788accd Frame interp: Cap the accumulator 2026-04-16 23:29:13 -04:00
Irastris c7de341484 Frame interp: Ganondorf's Cape 2026-04-16 22:29:44 -04:00
TakaRikka b182e18fab Merge pull request #407 from TwilitRealm/event-flag-table
Add UI for better manipulation of event flags
2026-04-16 16:30:31 -07:00
roeming 72a73ba661 Add UI for better manipulation of event flags 2026-04-16 19:11:20 -04:00
Luke Street d882ce47fc Merge remote-tracking branch 'origin/main' into ci-2 2026-04-16 16:34:55 -06:00
MelonSpeedruns 2cc11c74b7 Lots of Imgui Adjustments (#405)
- Store if the F1 menu is open for the next boot
- Auto-save controller changes without having the press the Save button
- Changed FPS X position so it doesn't clip to the right of the window
- Changed F1 Toast to be 2.5 secs so it doesn't go over the title screen

Co-authored-by: MelonSpeedruns <melonspeedruns@stratobox.net>
2026-04-16 13:58:39 -06:00
Irastris c7d9a8733f Gyro: Revisions & Rollgoal Mirror Mode 2026-04-16 15:31:58 -04:00
TakaRikka 88bdea1fd3 mirror minimap 2026-04-16 04:46:06 -07:00
TakaRikka 6c055d462f Merge branch 'main' of https://github.com/TakaRikka/dusk 2026-04-16 03:33:24 -07:00
TakaRikka dd84c3f46d add get item flags to save editor 2026-04-16 03:33:16 -07:00
Irastris 3db0281088 Gyro: ROLLGOAL! 2026-04-16 05:17:50 -04:00
TakaRikka 8230ebcced Merge branch 'main' of https://github.com/TakaRikka/dusk 2026-04-16 01:44:51 -07:00
TakaRikka a2882711ee add dungeon item flags to save editor 2026-04-16 01:44:44 -07:00
Jasper St. Pierre 6bc1d3d847 bloom blend tweakk 2026-04-16 01:05:03 -07:00
Jasper St. Pierre 06e6a97ee7 e&c 2026-04-16 00:43:13 -07:00
Jasper St. Pierre ff92ba3abc bloom viewport fix 2026-04-16 00:22:40 -07:00
Luke Street 5c61c5ede8 Add Bloom debug window 2026-04-16 00:34:17 -06:00
Luke Street 98b0c8296e And this too 2026-04-15 23:01:37 -06:00
Luke Street 269505a69f Fix bloom viewport hack 2026-04-15 22:36:37 -06:00
Irastris 433f058aea Frame interp: Revised camera system
FOV is now smooth, and the individual hacks for stars and Epona's reins are removed
2026-04-16 00:04:32 -04:00
SuperDude88 1c057bb24a Fix Unloading Gates (#397)
* Fix Unloading Gates

- Fix CitS/Lakebed gates not loading if you don't restart the game between the different instances

Comment should be upstreamed to decomp (+ daDsh_c brief should be changed from "Death Sword Shutter Gate" which isn't accurate)

* Clarify Comment

- Add details about GC/Wii vs TPHD
2026-04-15 20:00:05 -06:00
Howard Luck b0b1978c76 Fix dangling daMyna_c file-statics across scenes (#392) 2026-04-15 10:52:30 -06:00
Irastris 965e958222 Consolidate the Enhancements menu (#349)
* Consolidate the Enhancements menu

* Merge "main" branch again

* added Preferences separator
2026-04-15 09:11:30 -06:00
Pieter-Jan Briers 8de4863426 Validate that provided ISO is GZ2E01 in pre-launch UI (#390)
Fixes #377
2026-04-15 09:10:56 -06:00
Howard Luck 9235833413 Fix OSGetTime overflow on iOS/Android (#389)
* Fix OSGetTime overflow on iOS/Android

* Fetch latest aurora main
2026-04-15 09:10:45 -06:00
PJB3005 a0e9ee73a8 Fix mDoExt_3DlineMat0_c drawing
Fixes shadow beast hair etc, fixes #204

Regression introduced by https://github.com/TwilitRealm/dusk/commit/fe21abb1ec1a9f4fc320dfb191be58e864bc079c#diff-1c53210cfc3c1892c88d6c56110608fa017738f0b099e2cf5766f9107cefa4e8L2382-R2386
2026-04-15 15:17:03 +02:00
TakaRikka 99fd2f9bc5 save editor minigame tab / item amounts 2026-04-15 05:00:38 -07:00
TakaRikka 7eb61b71f1 some save editor cleanup 2026-04-15 04:29:33 -07:00
Luke Street 2a49673dce Merge branch 'main' into ci-2 2026-04-14 23:06:04 -06:00
Luke Street 20e958bf12 Update macOS CI stuff 2 2026-04-14 22:33:06 -06:00
TakaRikka d8df116047 Merge pull request #386 from TwilitRealm/fix/telma_dialog
Fix Missing Telma Dialog During Escort
2026-04-14 20:39:32 -07:00
Luke Street d432fbcd10 Update macOS CI stuff 2026-04-14 21:27:07 -06:00
Pheenoh 5f69895326 Merge remote-tracking branch 'origin/main' into fix/telma_dialog 2026-04-14 21:05:29 -06:00
TakaRikka 194cb94d43 Merge pull request #380 from TwilitRealm/feature/disable-main-hud
Disable Main HUD Feature
2026-04-14 20:03:23 -07:00
TakaRikka 22535ba84d Merge pull request #379 from TwilitRealm/minigame-wide-hud
Widescreen Minigame Counters
2026-04-14 20:02:41 -07:00
Pheenoh ab65f42816 Wait to finish creating Telma actor during escort on PC until the player is loaded 2026-04-14 20:47:30 -06:00
Irastris 0508acaa79 Frame interp: Dreamworks' Turbo (2013) 2026-04-14 21:59:24 -04:00
Luke Street bf83f40ec5 Update rustup command for iOS 2026-04-14 19:21:13 -06:00
Luke Street ada67f1be1 Try self-hosted macOS/iOS builds 2026-04-14 18:06:49 -06:00
Phillip Stephens d876c62822 Fix missing libusb in Linux AppImage (#384) 2026-04-14 17:49:08 -06:00
Luke Street c26b05b1dd Update CI for self-hosted runners 2026-04-14 16:15:55 -06:00
Irastris 97c459a614 Frame interp: GOAT IN! 2026-04-14 17:51:00 -04:00
Irastris cd9bb31b02 Frame interp: Howl 2 Electric Boogaloo 2026-04-14 17:17:29 -04:00
Irastris e37c912053 Frame interp: Stable keying for simple shadows 2026-04-14 16:55:24 -04:00
MelonSpeedruns 4e42af6cd8 Disable Main HUD Feature 2026-04-14 15:43:52 -04:00
Irastris 87a95fb1b8 Frame interp: Howl UI scrolling dot 2026-04-14 15:27:29 -04:00
MelonSpeedruns 83b210e26c Widescreen minigame counters 2026-04-14 15:10:28 -04:00
Luke Street 158f31d056 Patch Twilight Hack 2026-04-14 11:33:14 -06:00
Irastris 3bcb07dba4 Frame interp: Title logo presentation sync & minor sync refactor 2026-04-14 12:22:08 -04:00
madeline 86bea34bb2 Merge branch 'main' of https://github.com/TakaRikka/dusk 2026-04-14 07:07:23 -07:00
madeline 09e9c6ad34 redo keybinds for debug tab 2026-04-14 07:07:19 -07:00
TakaRikka 54db8e4b5b Merge pull request #370 from TwilitRealm/android-building
Android building
2026-04-14 03:20:13 -07:00
CraftyBoss ec091ac3af change ANDROID define to TARGET_ANDROID 2026-04-14 03:18:21 -07:00
CraftyBoss 35650784c3 Merge branch 'main' into android-building 2026-04-14 03:17:07 -07:00
CraftyBoss 86c1e21ac1 fix compilation error
GlyphTextures no longer exists
2026-04-14 03:07:47 -07:00
CraftyBoss a2f8fffc8d Merge branch 'main' into android-building 2026-04-14 02:48:24 -07:00
CraftyBoss ddfe21b18a fix shadow resolution changing causing a crash
operator delete[] was used instead of JKR_DELETE_ARRAY, which causes standard allocator to freak out
2026-04-14 02:45:16 -07:00
TakaRikka 93450dce7b Merge pull request #367 from TwilitRealm/flake
Add flake.nix to build on NixOS
2026-04-14 02:41:18 -07:00
TakaRikka 38cfaae940 Merge pull request #360 from TwilitRealm/26-04-13-font-opts
Text Rendering optimizations
2026-04-14 02:38:59 -07:00
TakaRikka 7e56747e33 Merge pull request #359 from TwilitRealm/feature/time-stones
Sun's Song
2026-04-14 02:38:10 -07:00
TakaRikka 72a97d62d8 Merge pull request #136 from TwilitRealm/26-03-27-better-mapping
Show better button names in controller mapping UI
2026-04-14 02:35:49 -07:00
madeline 130e5c9502 Merge branch 'main' of https://github.com/TakaRikka/dusk 2026-04-14 01:55:18 -07:00
madeline 505ef363fd more save state fixes 2026-04-14 01:55:14 -07:00
CraftyBoss b7f3dbb8b3 Merge remote-tracking branch 'origin/main' into android-building
# Conflicts:
#	CMakeLists.txt
2026-04-14 01:03:50 -07:00
Luke Street 27d95c37d5 Create log files under configDir/logs; seed initial pipeline cache 2026-04-14 01:32:58 -06:00
Luke Street 588910c642 Fix DUSK_VERSION_STRING on tag commits 2026-04-14 01:13:43 -06:00
Luke Street 00803b53ab I'm silly 2026-04-14 00:57:45 -06:00
Luke Street 4b8248b130 Integrate Sentry crash reporting for public builds 2026-04-14 00:43:56 -06:00
Irastris d32dc7481e Frame interp: Initial presentation sync implementation 2026-04-14 02:41:11 -04:00
madeline e08807f44f style 2026-04-13 23:34:57 -07:00
Irastris 8ea0352fed Frame interp: Initial presentation sync implementation 2026-04-14 02:33:44 -04:00
madeline 7f6696715a fix crash when loading states with ZA from title screen 2026-04-13 23:23:07 -07:00
madeline c27a2a3b75 fix -4 properly 2026-04-13 22:33:53 -07:00
madeline 863ce9117d fix bugs on exporting during fade 2026-04-13 22:20:50 -07:00
madeline c5c7e89b13 fix -4 bug 2026-04-13 22:05:30 -07:00
madeline 0e25434c17 state share feature 2026-04-13 21:55:55 -07:00
madeline 48c9f1e984 fix menu capture crash fixes #300 2026-04-13 20:08:48 -07:00
madeline 62fc79425e Merge branch 'main' of https://github.com/TakaRikka/dusk 2026-04-13 19:35:33 -07:00
madeline 266f36118f fix hot springs water 2026-04-13 19:35:29 -07:00
Jasper St. Pierre c6e67daeac d_drawlist: shadow fix
dDlst_shadowControl_c::reset() is called every frame, dDlst_shadowControl_c::init() is not. Fix up dDlst_shadowControl_c::init() to not leak and reset its own data when needed, and remove texture reset calls which tear down the texobj.
2026-04-13 19:34:25 -07:00
Jasper St. Pierre 03a46635a0 d_drawlist: naming cleanup 2026-04-13 19:34:25 -07:00
Luke Street 3f92989af8 Add Win32 resources, icon & copy DLLs/res to install 2026-04-13 20:27:59 -06:00
Luke Street b5d2ded2ca Hide Win32 console by default, show with --console/DUSK_CONSOLE=1 2026-04-13 20:25:44 -06:00
kipcode66 5155270fd3 Add flake.nix to build on NixOS 2026-04-13 22:17:06 -04:00
Luke Street 7136c90336 Add Dusk logo to imgui prelaunch 2026-04-13 19:25:59 -06:00
Luke Street 50a8ebab28 Move CMakePresets windows build dirs from out/build/ to build/ 2026-04-13 19:24:03 -06:00
MelonSpeedruns d865c82f76 Added howling anywhere (R+X) 2026-04-13 18:08:05 -04:00
Lurs 3252d70276 Add slider functionality to classic bloom too 2026-04-13 23:58:15 +02:00
Lurs b3f2ae63b3 Add slider for bloom brightness 2026-04-13 23:35:21 +02:00
MelonSpeedruns ad94d9656f Merge branch 'main' into feature/time-stones 2026-04-13 16:44:00 -04:00
Luke Street 49215dbc7b bloom2: Rework divRects calc 2026-04-13 14:14:19 -06:00
Luke Street fba3114d4f bloom2: Ensure draws/copies are pixel aligned 2026-04-13 14:00:32 -06:00
Luke Street b3cc9ba02e Add "Bloom Mode" config option (Off, Classic, Dusk) 2026-04-13 13:06:14 -06:00
Jasper St. Pierre a3a36508d6 bloom2 work 2026-04-13 11:26:56 -07:00
PJB3005 143aa51eb3 Make JUTResFont load all texture data into one texture
Together with the previous change, this enables entire blocks of text to be rendered in one draw call.
2026-04-13 19:09:16 +02:00
MelonSpeedruns e3b3eabfeb oops forgot a define somewhere 2026-04-13 13:05:11 -04:00
MelonSpeedruns f15a4f07e5 Time Stones now with Song of Time 2026-04-13 13:03:28 -04:00
MelonSpeedruns 277d16c110 Time Stones Feature 2026-04-13 10:46:09 -04:00
PJB3005 18995f3d7c Enable some draw call merging in text rendering
Don't set state between characters if possible.

Next step is page merging so it can do full lines of text at once.
2026-04-13 13:27:21 +02:00
CraftyBoss 5e6d240d0f Merge remote-tracking branch 'origin/main' into android-building 2026-04-13 01:05:16 -07:00
CraftyBoss fbec777b0a bump sdk & gradle version, add game mode data to manifest 2026-04-12 20:04:31 -07:00
CraftyBoss 8cd8d1b90c Merge remote-tracking branch 'origin/main' into android-building 2026-04-12 20:02:23 -07:00
CraftyBoss fc659b966f Merge branch 'main' into android-building 2026-04-12 03:34:51 -07:00
CraftyBoss 7e1e193f64 adjust logging to use std funcs, hide all system bars from showing 2026-04-12 03:32:58 -07:00
CraftyBoss 75a4fe4429 Merge remote-tracking branch 'origin/main' into android-building 2026-04-12 02:53:05 -07:00
PJB3005 6a07f82425 Merge branch 'main' into 26-03-27-better-mapping 2026-04-11 20:03:40 +02:00
CraftyBoss 1a91c77a64 rename all metaforce related things to Dusk 2026-04-10 21:59:03 -07:00
CraftyBoss 0649aa4828 Merge branch 'main' into android-building 2026-04-10 18:36:37 -07:00
CraftyBoss 4ec4cd363e only check if iso path is not empty on android, split log messages by newlines to prevent truncation
SDL_GetPathInfo doesnt seem able to get valid info from android, or theres some weird permission thing going on
2026-04-10 18:35:40 -07:00
CraftyBoss d01f6bafff use android logging func for aurora_log_callback 2026-04-10 15:13:11 -07:00
CraftyBoss 627cf559c9 building for android impl
currently builds a "Metaforce" apk, will need to change name of course. Running the apk using Android Studio's emulator can get in game, however running on a samsung phone does not seem to work.
2026-04-10 03:53:57 -07:00
PJB3005 7e797b2320 Merge branch 'main' into 26-03-27-better-mapping 2026-04-10 03:50:32 +02:00
PJB3005 228a305110 Show better button names in controller mapping UI
Requires https://github.com/encounter/aurora/pull/68
2026-03-27 19:52:09 +01:00
268 changed files with 13683 additions and 2762 deletions
+49 -41
View File
@@ -8,8 +8,9 @@ on:
pull_request:
env:
SCCACHE_GHA_ENABLED: "true"
# SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
jobs:
build-linux:
@@ -21,13 +22,13 @@ jobs:
matrix:
include:
- name: GCC x86_64
runner: ubuntu-latest
runner: [self-hosted, Linux]
preset: gcc
artifact_arch: x86_64
- name: GCC aarch64
runner: ubuntu-24.04-arm
preset: gcc
artifact_arch: aarch64
# - name: GCC aarch64
# runner: ubuntu-24.04-arm
# preset: gcc
# artifact_arch: aarch64
# - name: Clang x86_64
# runner: ubuntu-latest
# preset: clang
@@ -40,17 +41,22 @@ jobs:
submodules: recursive
- name: Install dependencies
if: 'false' # disabled for self-hosted
run: |
sudo apt-get update
sudo apt-get -y install ninja-build clang lld openssl libcurl4-openssl-dev \
zlib1g-dev libglu1-mesa-dev libdbus-1-dev libvulkan-dev libxi-dev libxrandr-dev libasound2-dev \
libpulse-dev libudev-dev libpng-dev libncurses5-dev libx11-xcb-dev libfreetype-dev \
libxinerama-dev libxcursor-dev python3-markupsafe libgtk-3-dev libssl-dev \
libxss-dev libfuse2
libxss-dev libfuse2 libusb-1.0-0-dev libdecor-0-dev libpipewire-0.3-dev libunwind-dev
- name: Setup sccache
if: 'false' # disabled for self-hosted
uses: mozilla-actions/sccache-action@v0.0.9
- name: Print sccache stats
run: sccache --show-stats
- name: Configure CMake
run: cmake --preset x-linux-ci-${{matrix.preset}}
@@ -61,6 +67,7 @@ jobs:
run: ci/build-appimage.sh
- name: Upload artifacts
if: startsWith(github.event.ref, 'refs/tags/v')
uses: actions/upload-artifact@v7
with:
name: dusk-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}}
@@ -68,23 +75,26 @@ jobs:
build/install/Dusk-*.AppImage
build/install/debug.tar.*
build-apple:
name: Build Apple (${{matrix.name}})
runs-on: macos-latest
runs-on: [self-hosted, macOS]
strategy:
fail-fast: false
matrix:
include:
- name: AppleClang macOS universal
- name: AppleClang macOS arm64
platform: macos
preset: x-macos-ci
artifact_name: macos-appleclang-universal
# - name: AppleClang iOS arm64 # TODO enable when CI is free
preset: x-macos-ci-arm64
artifact_name: macos-appleclang-arm64
# - name: AppleClang macOS x86_64
# platform: macos
# preset: x-macos-ci-x86_64
# artifact_name: macos-appleclang-x86_64
# - name: AppleClang iOS arm64
# platform: ios
# preset: x-ios-ci
# artifact_name: ios-appleclang-arm64
# - name: AppleClang tvOS arm64 # TODO enable when CI is free
# - name: AppleClang tvOS arm64
# platform: tvos
# preset: x-tvos-ci
# artifact_name: tvos-appleclang-arm64
@@ -95,22 +105,15 @@ jobs:
fetch-depth: 0
submodules: recursive
- name: Update Homebrew
if: matrix.platform == 'tvos'
run: |
brew update
brew upgrade --formula
- name: Install dependencies
if: 'false'
run: brew install cmake ninja
- name: Install markupsafe
if: matrix.platform == 'tvos'
run: pip3 install --break-system-packages markupsafe
- name: Install Rust iOS target
if: matrix.platform == 'ios'
run: rustup target add aarch64-apple-ios
run: |
rustup toolchain install stable
rustup target add aarch64-apple-ios
- name: Install Rust tvOS target
if: matrix.platform == 'tvos'
@@ -118,6 +121,12 @@ jobs:
rustup toolchain install nightly
rustup target add --toolchain nightly aarch64-apple-tvos
- name: Install Rust x86_64 macOS target
if: endsWith(matrix.preset, 'x86_64')
run: |
rustup toolchain install stable
rustup target add x86_64-apple-darwin
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
@@ -128,6 +137,7 @@ jobs:
run: cmake --build --preset ${{matrix.preset}}
- name: Upload artifacts
if: startsWith(github.event.ref, 'refs/tags/v')
uses: actions/upload-artifact@v7
with:
name: dusk-${{env.DUSK_VERSION}}-${{matrix.artifact_name}}
@@ -139,26 +149,22 @@ jobs:
name: Build Windows (${{matrix.name}})
runs-on: ${{matrix.runner}}
env:
BUILD_DIR: C:\build
SCCACHE_DIR: C:\sccache
strategy:
fail-fast: false
matrix:
include:
- name: MSVC x86_64
runner: windows-latest
runner: [self-hosted, Windows]
preset: msvc
msvc_arch: amd64
vcpkg_arch: x64
artifact_arch: x86_64
- name: MSVC arm64
runner: windows-11-arm
preset: arm64-msvc
msvc_arch: arm64
vcpkg_arch: arm64
artifact_arch: arm64
# - name: MSVC arm64
# runner: windows-11-arm
# preset: arm64-msvc
# msvc_arch: arm64
# vcpkg_arch: arm64
# artifact_arch: arm64
# - name: Clang x86_64
# runner: windows-latest
# preset: clang
@@ -185,11 +191,10 @@ jobs:
uses: mozilla-actions/sccache-action@v0.0.9
- name: Install dependencies
if: 'false' # disabled for self-hosted
run: |
choco install ninja
vcpkg install zlib:${{matrix.vcpkg_arch}}-windows-static bzip2:${{matrix.vcpkg_arch}}-windows-static `
zstd:${{matrix.vcpkg_arch}}-windows-static liblzma:${{matrix.vcpkg_arch}}-windows-static `
freetype:${{matrix.vcpkg_arch}}-windows-static
vcpkg install freetype:${{matrix.vcpkg_arch}}-windows-static zstd:${{matrix.vcpkg_arch}}-windows-static
- name: Configure CMake
run: cmake --preset x-windows-ci-${{matrix.preset}}
@@ -198,9 +203,12 @@ jobs:
run: cmake --build --preset x-windows-ci-${{matrix.preset}}
- name: Upload artifacts
if: startsWith(github.event.ref, 'refs/tags/v')
uses: actions/upload-artifact@v7
with:
name: dusk-${{env.DUSK_VERSION}}-win32-msvc-${{matrix.artifact_arch}}
path: |
${{env.BUILD_DIR}}/install/*.exe
${{env.BUILD_DIR}}/install/debug.7z
build/install/*.exe
build/install/*.dll
build/install/res/
build/install/debug.7z
+1 -1
View File
@@ -6,7 +6,7 @@
"type": "cppvsdbg",
"request": "launch",
"program": "${command:cmake.launchTargetPath}",
"args": ["-l", "1", "--dvd", "${workspaceRoot}/orig/GZ2E01/GZ2E01.iso"],
"args": ["-l", "1", "--dvd", "${workspaceRoot}/orig/GZ2E01/GZ2E01.iso", "--console"],
"MIMode": "gdb",
"miDebuggerPath": "gdb",
"symbolSearchPath": "${command:cmake.launchTargetPath}",
+284 -90
View File
@@ -48,12 +48,17 @@ else ()
message(STATUS "Unable to find git, commit information will not be available")
endif ()
if (DUSK_WC_DESCRIBE)
string(REGEX REPLACE "v([0-9]+)\.([0-9]+)\.([0-9]+)\-([0-9]+).*" "\\1.\\2.\\3.\\4" DUSK_VERSION_STRING "${DUSK_WC_DESCRIBE}")
string(REGEX REPLACE "v([0-9]+)\.([0-9]+)\.([0-9]+).*" "\\1.\\2.\\3" DUSK_SHORT_VERSION_STRING "${DUSK_WC_DESCRIBE}")
if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)(-([0-9]+).*)?$")
set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
if (CMAKE_MATCH_5)
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${CMAKE_MATCH_5}")
else ()
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.0")
endif ()
else ()
set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION")
set(DUSK_VERSION_STRING "0.0.0")
set(DUSK_VERSION_STRING "0.0.0.0")
set(DUSK_SHORT_VERSION_STRING "0.0.0")
endif ()
# Add version information to CI environment variables
@@ -63,6 +68,9 @@ endif()
message(STATUS "Dusk version set to ${DUSK_WC_DESCRIBE}")
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
project(dusk LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING})
if (APPLE)
enable_language(OBJC)
endif ()
if (APPLE AND NOT TVOS AND CMAKE_SYSTEM_NAME STREQUAL tvOS)
# ios.toolchain.cmake hack for SDL
set(TVOS ON)
@@ -95,8 +103,20 @@ option(DUSK_BUILD_WARNINGS "Enable compiler warnings (off by default)")
option(DUSK_SELECTED_OPT "If on, selected parts of the project will be compiled with optimizations on Debug, intending to make the game run at 30 FPS. Note for MSVC: you will need to remove '/RTC1' from your debug flags in CMake.")
option(DUSK_MOVIE_SUPPORT "If on, compile against libjpeg-turbo to enable THP file decoding" ON)
if(ANDROID)
set(DUSK_MOVIE_SUPPORT OFF)
set(NOD_COMPRESS_BZIP2 OFF CACHE BOOL "" FORCE)
set(NOD_COMPRESS_LZMA OFF CACHE BOOL "" FORCE)
set(NOD_COMPRESS_ZLIB OFF CACHE BOOL "" FORCE)
set(NOD_COMPRESS_ZSTD OFF CACHE BOOL "" FORCE)
endif ()
option(DUSK_ENABLE_SENTRY_NATIVE "Enable sentry-native crash reporting support" OFF)
set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN")
set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment")
if (DUSK_MOVIE_SUPPORT)
find_package(libjpeg-turbo QUIET)
find_package(libjpeg-turbo 3.0 CONFIG QUIET)
if (libjpeg-turbo_FOUND)
message(STATUS "dusk: Using system libjpeg-turbo")
else ()
@@ -108,19 +128,37 @@ if (DUSK_MOVIE_SUPPORT)
else ()
set(_jpeg_lib ${_jpeg_install_dir}/lib/libturbojpeg.a)
endif ()
set(_jpeg_cmake_args
-DCMAKE_INSTALL_PREFIX=${_jpeg_install_dir}
-DENABLE_SHARED=OFF
-DWITH_TURBOJPEG=ON
-DWITH_JAVA=OFF
)
if (CMAKE_TOOLCHAIN_FILE)
get_filename_component(_jpeg_toolchain_file "${CMAKE_TOOLCHAIN_FILE}" ABSOLUTE BASE_DIR "${CMAKE_SOURCE_DIR}")
list(APPEND _jpeg_cmake_args -DCMAKE_TOOLCHAIN_FILE=${_jpeg_toolchain_file})
endif ()
set(_jpeg_passthrough_vars
CMAKE_BUILD_TYPE
CMAKE_C_COMPILER
CMAKE_C_COMPILER_LAUNCHER
CMAKE_MAKE_PROGRAM
CMAKE_MSVC_RUNTIME_LIBRARY
CMAKE_OSX_ARCHITECTURES
DEPLOYMENT_TARGET
ENABLE_ARC
ENABLE_BITCODE
PLATFORM
)
foreach(_var IN LISTS _jpeg_passthrough_vars)
if (DEFINED ${_var})
list(APPEND _jpeg_cmake_args -D${_var}=${${_var}})
endif ()
endforeach ()
ExternalProject_Add(libjpeg-turbo-ext
URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.1.0.tar.gz
URL_HASH SHA256=35fec2e1ddfb05ecf6d93e50bc57c1e54bc81c16d611ddf6eff73fff266d8285
CMAKE_ARGS
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
-DCMAKE_INSTALL_PREFIX=${_jpeg_install_dir}
-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}
-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
-DCMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER}
-DCMAKE_MSVC_RUNTIME_LIBRARY=${CMAKE_MSVC_RUNTIME_LIBRARY}
-DENABLE_SHARED=OFF
-DWITH_TURBOJPEG=ON
-DWITH_JAVA=OFF
CMAKE_ARGS ${_jpeg_cmake_args}
BUILD_BYPRODUCTS ${_jpeg_lib}
)
file(MAKE_DIRECTORY ${_jpeg_install_dir}/include)
@@ -148,21 +186,23 @@ elseif (APPLE)
set(CMAKE_INSTALL_RPATH "$ORIGIN")
set(CMAKE_BUILD_RPATH "$ORIGIN")
elseif (MSVC)
add_compile_options(/bigobj)
add_compile_options(/Zc:strictStrings-)
add_compile_options(/MP)
add_compile_options(/FS)
add_compile_options(
$<$<COMPILE_LANGUAGE:C,CXX>:/bigobj>
$<$<COMPILE_LANGUAGE:C,CXX>:/Zc:strictStrings->
$<$<COMPILE_LANGUAGE:C,CXX>:/MP>
$<$<COMPILE_LANGUAGE:C,CXX>:/FS>
)
if (NOT DUSK_BUILD_WARNINGS)
add_compile_options(/W0)
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:/W0>)
else ()
# Disable warnings
add_compile_options(/wd4068) # unknown pragma
add_compile_options(/wd4291) # no matching delete operator, leaks if exception thrown
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:/wd4068>) # unknown pragma
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:/wd4291>) # no matching delete operator, leaks if exception thrown
# Only show warnings once
add_compile_options(/wo4244) # narrowing conversion, possible data loss
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:/wo4244>) # narrowing conversion, possible data loss
endif ()
add_compile_options(/utf-8)
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:/utf-8>)
endif ()
@@ -183,6 +223,46 @@ FetchContent_Declare(json
)
FetchContent_MakeAvailable(cxxopts json)
if (DUSK_ENABLE_SENTRY_NATIVE)
message(STATUS "dusk: Fetching sentry-native")
set(SENTRY_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(SENTRY_BACKEND crashpad CACHE STRING "" FORCE)
if (WIN32)
set(SENTRY_TRANSPORT winhttp CACHE STRING "" FORCE)
endif ()
set(SENTRY_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(SENTRY_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(SENTRY_BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE)
FetchContent_Declare(sentry_native
GIT_REPOSITORY https://github.com/getsentry/sentry-native.git
GIT_TAG 0.13.6
GIT_SHALLOW TRUE
GIT_PROGRESS TRUE
GIT_SUBMODULES_RECURSE TRUE
)
if (NOT sentry_native_POPULATED)
FetchContent_Populate(sentry_native)
set(_dusk_skip_install_rules ${CMAKE_SKIP_INSTALL_RULES})
set(CMAKE_SKIP_INSTALL_RULES ON)
add_subdirectory(${sentry_native_SOURCE_DIR} ${sentry_native_BINARY_DIR} EXCLUDE_FROM_ALL)
set(CMAKE_SKIP_INSTALL_RULES ${_dusk_skip_install_rules})
endif ()
endif ()
if (CMAKE_SYSTEM_NAME STREQUAL Windows)
set(PLATFORM_NAME win32)
elseif (CMAKE_SYSTEM_NAME STREQUAL Darwin)
if (IOS)
set(PLATFORM_NAME ios)
elseif (TVOS)
set(PLATFORM_NAME tvos)
else ()
set(PLATFORM_NAME macos)
endif ()
else ()
string(TOLOWER CMAKE_SYSTEM_NAME PLATFORM_NAME)
endif ()
configure_file(${CMAKE_SOURCE_DIR}/version.h.in ${CMAKE_BINARY_DIR}/version.h)
include(files.cmake)
@@ -190,7 +270,11 @@ include(files.cmake)
# TODO: version handling for res includes
set(DUSK_BUNDLE_NAME Dusk)
set(DUSK_BUNDLE_IDENTIFIER dev.decomp.dusk)
set(DUSK_BUNDLE_IDENTIFIER dev.twilitrealm.dusk)
set(DUSK_COMPANY_NAME "Twilit Realm")
set(DUSK_FILE_DESCRIPTION "Dusk")
set(DUSK_PRODUCT_NAME "Dusk")
set(DUSK_COPYRIGHT "Copyright (C) Twilit Realm contributors")
set(DUSK_GAME_NAME "GZ2E")
set(DUSK_GAME_VERSION "01")
set(DUSK_TP_VERSION ${DUSK_GAME_NAME}${DUSK_GAME_VERSION})
@@ -214,7 +298,14 @@ set(GAME_INCLUDE_DIRS
${CMAKE_BINARY_DIR})
set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd
aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient)
aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt)
list(APPEND GAME_LIBS libzstd_static)
if (DUSK_ENABLE_SENTRY_NATIVE)
list(APPEND GAME_LIBS sentry)
list(APPEND GAME_COMPILE_DEFS DUSK_ENABLE_SENTRY_NATIVE=1 SENTRY_BUILD_STATIC=1)
endif ()
if (DUSK_MOVIE_SUPPORT)
if (TARGET libjpeg-turbo::turbojpeg-static)
@@ -225,10 +316,60 @@ if (DUSK_MOVIE_SUPPORT)
list(APPEND GAME_COMPILE_DEFS MOVIE_SUPPORT=1)
endif ()
option(DUSK_ENABLE_DISCORD_RPC "Enable Discord Rich Presence support" ON)
if (DUSK_ENABLE_DISCORD_RPC AND NOT ANDROID AND NOT IOS AND NOT TVOS)
FetchContent_Populate(discord_rpc
URL https://github.com/discord/discord-rpc/archive/refs/tags/v3.4.0.tar.gz
URL_HASH SHA256=e13427019027acd187352dacba6c65953af66fdf3c35fcf38fc40b454a9d7855
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
# RapidJSON is a git submodule absent from the discord-rpc tarball; fetch separately.
FetchContent_Populate(rapidjson
URL https://github.com/Tencent/rapidjson/archive/refs/tags/v1.1.0.tar.gz
URL_HASH SHA256=bf7ced29704a1e696fbccf2a2b4ea068e7774fa37f6d7dd4039d0787f8bed98e
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
if (NOT TARGET discord-rpc)
set(_drpc ${discord_rpc_SOURCE_DIR}/src)
set(_drpc_src
${_drpc}/discord_rpc.cpp
${_drpc}/rpc_connection.cpp
${_drpc}/serialization.cpp
)
if (WIN32)
list(APPEND _drpc_src ${_drpc}/connection_win.cpp ${_drpc}/discord_register_win.cpp)
elseif (APPLE)
list(APPEND _drpc_src ${_drpc}/connection_unix.cpp ${_drpc}/discord_register_osx.m)
else ()
list(APPEND _drpc_src ${_drpc}/connection_unix.cpp ${_drpc}/discord_register_linux.cpp)
endif ()
add_library(discord-rpc STATIC ${_drpc_src})
target_include_directories(discord-rpc PUBLIC
${discord_rpc_SOURCE_DIR}/include
${rapidjson_SOURCE_DIR}/include
)
if (UNIX)
target_link_libraries(discord-rpc PUBLIC pthread)
endif ()
endif ()
list(APPEND GAME_LIBS discord-rpc)
list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD_RPC=1)
endif ()
# Edit & Continue
if (MSVC)
add_compile_options("/ZI")
add_link_options("/INCREMENTAL")
if ("${CMAKE_MSVC_DEBUG_INFORMATION_FORMAT}" STREQUAL "" AND CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "EditAndContinue")
endif ()
if (CMAKE_MSVC_DEBUG_INFORMATION_FORMAT STREQUAL "EditAndContinue")
add_link_options("/INCREMENTAL")
endif ()
endif ()
if(ANDROID)
list(APPEND GAME_COMPILE_DEFS TARGET_ANDROID=1)
endif ()
# game_debug is for game code files that we know work when compiled with DEBUG=1
@@ -258,65 +399,110 @@ target_include_directories(game_base PRIVATE ${GAME_INCLUDE_DIRS})
target_link_libraries(game_debug PRIVATE ${GAME_LIBS})
target_link_libraries(game_base PRIVATE ${GAME_LIBS})
# Combined game library
add_library(game STATIC
$<TARGET_OBJECTS:game_base>
$<TARGET_OBJECTS:game_debug>)
target_link_libraries(game PUBLIC ${GAME_LIBS})
if(ANDROID)
add_library(dusk SHARED src/dusk/main.cpp)
set_target_properties(dusk PROPERTIES OUTPUT_NAME main)
else ()
add_executable(dusk src/dusk/main.cpp)
endif ()
add_executable(dusk src/dusk/main.cpp)
target_compile_definitions(dusk PRIVATE TARGET_PC AVOID_UB=1 VERSION=0)
target_include_directories(dusk PRIVATE include)
target_link_libraries(dusk PRIVATE game aurora::main)
target_link_libraries(dusk PRIVATE game_base game_debug aurora::main)
if (TARGET crashpad_handler)
add_dependencies(dusk crashpad_handler)
endif ()
add_custom_command(TARGET dusk POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/res"
"$<TARGET_FILE_DIR:dusk>/res"
COMMENT "Copying resources"
)
if (ANDROID)
# SDLActivity loads SDL_main via dlsym on Android. Since aurora::main is a static
# archive, force an undefined reference so the linker keeps the SDL_main object.
target_link_options(dusk PRIVATE "-Wl,-u,SDL_main")
endif ()
if (NOT APPLE)
add_custom_command(TARGET dusk POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/res"
"$<TARGET_FILE_DIR:dusk>/res"
COMMENT "Copying resources"
)
endif ()
if (WIN32)
set(DUSK_WINDOWS_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/windows)
set(DUSK_WINDOWS_ICON_PNG ${CMAKE_CURRENT_SOURCE_DIR}/res/icon.png)
set(DUSK_WINDOWS_ICON_ICO ${CMAKE_CURRENT_BINARY_DIR}/dusk.ico)
set(DUSK_WINDOWS_RC ${CMAKE_CURRENT_BINARY_DIR}/dusk.rc)
set(DUSK_WINDOWS_MANIFEST ${CMAKE_CURRENT_BINARY_DIR}/dusk.manifest)
add_custom_command(
OUTPUT ${DUSK_WINDOWS_ICON_ICO}
COMMAND powershell -ExecutionPolicy Bypass -File
${DUSK_WINDOWS_RESOURCE_DIR}/Create-IcoFromPng.ps1
-InputPng ${DUSK_WINDOWS_ICON_PNG}
-OutputIco ${DUSK_WINDOWS_ICON_ICO}
DEPENDS ${DUSK_WINDOWS_ICON_PNG} ${DUSK_WINDOWS_RESOURCE_DIR}/Create-IcoFromPng.ps1
VERBATIM
COMMENT "Generating Windows icon"
)
configure_file(${DUSK_WINDOWS_RESOURCE_DIR}/dusk.manifest.in ${DUSK_WINDOWS_MANIFEST} @ONLY)
configure_file(${DUSK_WINDOWS_RESOURCE_DIR}/dusk.rc.in ${DUSK_WINDOWS_RC} @ONLY)
target_sources(dusk PRIVATE ${DUSK_WINDOWS_ICON_ICO} ${DUSK_WINDOWS_RC})
set_target_properties(dusk PROPERTIES WIN32_EXECUTABLE TRUE)
if (MSVC)
target_link_options(dusk PRIVATE /MANIFEST:NO)
endif ()
endif ()
if (APPLE)
if (IOS)
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios)
set(DUSK_INFO_PLIST ${DUSK_RESOURCE_DIR}/Info.plist.in)
file(GLOB_RECURSE DUSK_RESOURCE_FILES "${DUSK_RESOURCE_DIR}/Base.lproj/*")
endif ()
if (IOS OR TVOS)
target_sources(dusk PRIVATE ${DUSK_RESOURCE_FILES})
foreach (FILE ${DUSK_RESOURCE_FILES})
file(RELATIVE_PATH NEW_FILE "${DUSK_RESOURCE_DIR}" ${FILE})
get_filename_component(NEW_FILE_PATH ${NEW_FILE} DIRECTORY)
set_property(SOURCE ${FILE} PROPERTY MACOSX_PACKAGE_LOCATION "Resources/${NEW_FILE_PATH}")
endforeach ()
set_target_properties(
dusk PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_BUNDLE_NAME ${DUSK_BUNDLE_NAME}
MACOSX_BUNDLE_GUI_IDENTIFIER ${DUSK_BUNDLE_IDENTIFIER}
MACOSX_BUNDLE_BUNDLE_VERSION ${DUSK_VERSION_STRING}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${DUSK_SHORT_VERSION_STRING}
OUTPUT_NAME dusk
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "YES"
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "YES"
)
if (CMAKE_GENERATOR STREQUAL "Xcode")
set_target_properties(dusk PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${DUSK_INFO_PLIST})
elseif (DEFINED DUSK_INFO_PLIST)
set(MACOSX_BUNDLE_EXECUTABLE_NAME dusk)
set(MACOSX_BUNDLE_GUI_IDENTIFIER ${DUSK_BUNDLE_IDENTIFIER})
set(MACOSX_BUNDLE_BUNDLE_NAME ${DUSK_BUNDLE_NAME})
set(MACOSX_BUNDLE_BUNDLE_VERSION ${DUSK_VERSION_STRING})
set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${DUSK_SHORT_VERSION_STRING})
set(DUSK_GENERATED_INFO_PLIST ${CMAKE_CURRENT_BINARY_DIR}/dusk.Info.plist)
configure_file(${DUSK_INFO_PLIST} ${DUSK_GENERATED_INFO_PLIST})
add_custom_command(
TARGET dusk POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${DUSK_GENERATED_INFO_PLIST} $<TARGET_FILE_DIR:dusk>/Info.plist
VERBATIM
)
endif ()
elseif (TVOS)
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/tvos)
else ()
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos)
endif ()
set(DUSK_INFO_PLIST ${DUSK_RESOURCE_DIR}/Info.plist.in)
file(GLOB_RECURSE DUSK_RESOURCE_FILES
"${DUSK_RESOURCE_DIR}/Assets.car"
"${DUSK_RESOURCE_DIR}/Base.lproj/*"
"${DUSK_RESOURCE_DIR}/Dusk.icns")
file(GLOB_RECURSE DUSK_APP_RESOURCE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/res/*")
target_sources(dusk PRIVATE ${DUSK_RESOURCE_FILES})
target_sources(dusk PRIVATE ${DUSK_APP_RESOURCE_FILES})
foreach (FILE ${DUSK_RESOURCE_FILES})
file(RELATIVE_PATH NEW_FILE "${DUSK_RESOURCE_DIR}" ${FILE})
get_filename_component(NEW_FILE_PATH ${NEW_FILE} DIRECTORY)
set_property(SOURCE ${FILE} PROPERTY MACOSX_PACKAGE_LOCATION "Resources/${NEW_FILE_PATH}")
endforeach ()
foreach (FILE ${DUSK_APP_RESOURCE_FILES})
file(RELATIVE_PATH NEW_FILE "${CMAKE_CURRENT_SOURCE_DIR}" ${FILE})
get_filename_component(NEW_FILE_PATH ${NEW_FILE} DIRECTORY)
set_property(SOURCE ${FILE} PROPERTY MACOSX_PACKAGE_LOCATION "Resources/${NEW_FILE_PATH}")
endforeach ()
set_target_properties(
dusk PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_BUNDLE_NAME ${DUSK_BUNDLE_NAME}
MACOSX_BUNDLE_GUI_IDENTIFIER ${DUSK_BUNDLE_IDENTIFIER}
MACOSX_BUNDLE_BUNDLE_VERSION ${DUSK_VERSION_STRING}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${DUSK_SHORT_VERSION_STRING}
MACOSX_BUNDLE_INFO_PLIST ${DUSK_INFO_PLIST}
OUTPUT_NAME Dusk
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "YES"
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "YES"
)
endif ()
if (IOS)
find_library(UIKIT_FRAMEWORK UIKit REQUIRED)
find_library(UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK UniformTypeIdentifiers REQUIRED)
target_sources(dusk PRIVATE src/dusk/ios/FileSelectDialog.m)
set_source_files_properties(src/dusk/ios/FileSelectDialog.m PROPERTIES COMPILE_FLAGS -fobjc-arc)
target_link_libraries(dusk PRIVATE ${UIKIT_FRAMEWORK} ${UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK})
endif ()
include(extern/aurora/cmake/AuroraCopyRuntimeDLLs.cmake)
@@ -364,6 +550,10 @@ if (TARGET crashpad_handler)
list(APPEND EXTRA_TARGETS crashpad_handler)
endif ()
install(TARGETS ${BINARY_TARGETS} ${EXTRA_TARGETS} DESTINATION ${CMAKE_INSTALL_PREFIX})
aurora_install_runtime_dlls(dusk ${CMAKE_INSTALL_PREFIX})
if (NOT APPLE)
install(DIRECTORY ${CMAKE_SOURCE_DIR}/res DESTINATION ${CMAKE_INSTALL_PREFIX})
endif ()
if (CMAKE_BUILD_TYPE STREQUAL Debug OR CMAKE_BUILD_TYPE STREQUAL RelWithDebInfo)
set(DEBUG_FILES_LIST "")
foreach (target IN LISTS BINARY_TARGETS EXTRA_TARGETS)
@@ -385,18 +575,22 @@ if (CMAKE_BUILD_TYPE STREQUAL Debug OR CMAKE_BUILD_TYPE STREQUAL RelWithDebInfo)
endif ()
list(APPEND DEBUG_FILES_LIST "${output_name}")
endforeach ()
if (WIN32)
list(TRANSFORM DEBUG_FILES_LIST APPEND ".pdb")
list(JOIN DEBUG_FILES_LIST " " DEBUG_FILES)
install(CODE "execute_process(WORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}\" COMMAND 7z a -t7z \"${CMAKE_INSTALL_PREFIX}/debug.7z\" ${DEBUG_FILES})")
elseif (APPLE)
list(TRANSFORM DEBUG_FILES_LIST APPEND ".dSYM")
list(JOIN DEBUG_FILES_LIST " " DEBUG_FILES)
install(CODE "execute_process(WORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}\" COMMAND tar acfv \"${CMAKE_INSTALL_PREFIX}/debug.tar.xz\" ${DEBUG_FILES})")
elseif (UNIX)
list(TRANSFORM DEBUG_FILES_LIST APPEND ".dbg")
list(JOIN DEBUG_FILES_LIST " " DEBUG_FILES)
install(CODE "execute_process(WORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}\" COMMAND tar -I \"xz -9 -T0\" -cvf \"${CMAKE_INSTALL_PREFIX}/debug.tar.xz\" ${DEBUG_FILES})")
# This is a terrible hack to only run this on CI
# until I turn this into a script or something
if(DEFINED ENV{GITHUB_ENV})
if (WIN32)
list(TRANSFORM DEBUG_FILES_LIST APPEND ".pdb")
list(JOIN DEBUG_FILES_LIST " " DEBUG_FILES)
install(CODE "execute_process(WORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}\" COMMAND 7z a -t7z \"${CMAKE_INSTALL_PREFIX}/debug.7z\" ${DEBUG_FILES})")
elseif (APPLE)
list(TRANSFORM DEBUG_FILES_LIST APPEND ".dSYM")
list(JOIN DEBUG_FILES_LIST " " DEBUG_FILES)
install(CODE "execute_process(WORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}\" COMMAND tar acfv \"${CMAKE_INSTALL_PREFIX}/debug.tar.xz\" ${DEBUG_FILES})")
elseif (UNIX)
list(TRANSFORM DEBUG_FILES_LIST APPEND ".dbg")
list(JOIN DEBUG_FILES_LIST " " DEBUG_FILES)
install(CODE "execute_process(WORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}\" COMMAND tar -I \"xz -9 -T0\" -cvf \"${CMAKE_INSTALL_PREFIX}/debug.tar.xz\" ${DEBUG_FILES})")
endif ()
endif ()
endif ()
foreach (target IN LISTS BINARY_TARGETS)
+89 -28
View File
@@ -22,6 +22,20 @@
"CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreaded"
}
},
{
"name": "ci",
"hidden": true,
"cacheVariables": {
"CMAKE_C_COMPILER_LAUNCHER": "sccache",
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache",
"DUSK_ENABLE_SENTRY_NATIVE": {
"type": "BOOL",
"value": true
},
"DUSK_SENTRY_DSN": "$env{SENTRY_DSN}",
"DUSK_SENTRY_ENVIRONMENT": "production"
}
},
{
"name": "linux-default",
"displayName": "Linux (default)",
@@ -88,7 +102,7 @@
"name": "windows-msvc",
"displayName": "Windows (MSVC)",
"generator": "Ninja",
"binaryDir": "${sourceDir}/out/build/${presetName}",
"binaryDir": "${sourceDir}/build/${presetName}",
"architecture": {
"value": "x64",
"strategy": "external"
@@ -96,7 +110,7 @@
"cacheVariables": {
"CMAKE_C_COMPILER": "cl",
"CMAKE_CXX_COMPILER": "cl",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install"
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install"
},
"vendor": {
"microsoft.com/VisualStudioSettings/CMake/1.0": {
@@ -126,7 +140,7 @@
"name": "windows-arm64-msvc",
"displayName": "Windows ARM64 (MSVC)",
"generator": "Ninja",
"binaryDir": "${sourceDir}/out/build/${presetName}",
"binaryDir": "${sourceDir}/build/${presetName}",
"architecture": {
"value": "arm64",
"strategy": "external"
@@ -134,8 +148,7 @@
"cacheVariables": {
"CMAKE_C_COMPILER": "cl",
"CMAKE_CXX_COMPILER": "cl",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install",
"AURORA_DAWN_PROVIDER": "vendor"
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install"
},
"vendor": {
"microsoft.com/VisualStudioSettings/CMake/1.0": {
@@ -316,7 +329,7 @@
"cacheVariables": {
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
"CMAKE_TOOLCHAIN_FILE": "$env{ANDROID_HOME}/ndk/$env{ANDROID_NDK_VERSION}/build/cmake/android.toolchain.cmake",
"ANDROID_PLATFORM": "android-24"
"ANDROID_PLATFORM": "android-28"
}
},
{
@@ -343,11 +356,11 @@
"name": "x-linux-ci",
"hidden": true,
"inherits": [
"relwithdebinfo"
"relwithdebinfo",
"ci"
],
"cacheVariables": {
"CMAKE_C_COMPILER_LAUNCHER": "sccache",
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache"
"AURORA_SDL3_PROVIDER": "vendor"
}
},
{
@@ -367,11 +380,40 @@
{
"name": "x-macos-ci",
"inherits": [
"macos-default-relwithdebinfo"
"macos-default-relwithdebinfo",
"ci"
],
"cacheVariables": {
"CMAKE_C_COMPILER_LAUNCHER": "sccache",
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache"
"AURORA_NOD_PROVIDER": "vendor",
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
"type": "BOOL",
"value": true
},
"CMAKE_OSX_DEPLOYMENT_TARGET": "11.0",
"CMAKE_IGNORE_PREFIX_PATH": "/opt/homebrew",
"DUSK_MOVIE_SUPPORT": {
"type": "BOOL",
"value": false
}
}
},
{
"name": "x-macos-ci-arm64",
"inherits": [
"x-macos-ci"
],
"cacheVariables": {
"CMAKE_OSX_ARCHITECTURES": "arm64"
}
},
{
"name": "x-macos-ci-x86_64",
"inherits": [
"x-macos-ci"
],
"cacheVariables": {
"CMAKE_OSX_ARCHITECTURES": "x86_64",
"Rust_CARGO_TARGET": "x86_64-apple-darwin"
}
},
{
@@ -381,7 +423,11 @@
],
"cacheVariables": {
"CMAKE_C_COMPILER_LAUNCHER": "sccache",
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache"
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache",
"DUSK_MOVIE_SUPPORT": {
"type": "BOOL",
"value": false
}
}
},
{
@@ -391,21 +437,27 @@
],
"cacheVariables": {
"CMAKE_C_COMPILER_LAUNCHER": "sccache",
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache"
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache",
"DUSK_MOVIE_SUPPORT": {
"type": "BOOL",
"value": false
}
}
},
{
"name": "x-windows-ci",
"hidden": true,
"inherits": [
"relwithdebinfo"
"relwithdebinfo",
"ci"
],
"binaryDir": "$env{BUILD_DIR}",
"cacheVariables": {
"CMAKE_INSTALL_PREFIX": "$env{BUILD_DIR}/install",
"CMAKE_C_COMPILER_LAUNCHER": "sccache",
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache",
"CMAKE_MSVC_DEBUG_INFORMATION_FORMAT": "Embedded"
"CMAKE_MSVC_DEBUG_INFORMATION_FORMAT": "Embedded",
"CMAKE_TOOLCHAIN_FILE": {
"type": "FILEPATH",
"value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
},
"VCPKG_TARGET_TRIPLET": "x64-windows"
}
},
{
@@ -546,10 +598,19 @@
]
},
{
"name": "x-macos-ci",
"configurePreset": "x-macos-ci",
"description": "(Internal) macOS CI",
"displayName": "(Internal) macOS CI",
"name": "x-macos-ci-arm64",
"configurePreset": "x-macos-ci-arm64",
"description": "(Internal) macOS CI arm64",
"displayName": "(Internal) macOS CI arm64",
"targets": [
"install"
]
},
{
"name": "x-macos-ci-x86_64",
"configurePreset": "x-macos-ci-x86_64",
"description": "(Internal) macOS CI x86_64",
"displayName": "(Internal) macOS CI x86_64",
"targets": [
"install"
]
@@ -557,8 +618,8 @@
{
"name": "x-ios-ci",
"configurePreset": "x-ios-ci",
"description": "(Internal) iOS CI",
"displayName": "(Internal) iOS CI",
"description": "(Internal) iOS CI arm64",
"displayName": "(Internal) iOS CI arm64",
"targets": [
"install"
]
@@ -566,8 +627,8 @@
{
"name": "x-tvos-ci",
"configurePreset": "x-tvos-ci",
"description": "(Internal) tvOS CI",
"displayName": "(Internal) tvOS CI",
"description": "(Internal) tvOS CI arm64",
"displayName": "(Internal) tvOS CI arm64",
"targets": [
"install"
]
+46 -90
View File
@@ -1,103 +1,59 @@
## Dusk
![DuskLogo](res/logo-mascot.webp)
### Building
#### Prerequisites
* [CMake 3.25+](https://cmake.org)
* Windows: Install `CMake Tools` in Visual Studio
* macOS: `brew install cmake`
* [Python 3+](https://python.org)
* Windows: [Microsoft Store](https://go.microsoft.com/fwlink?linkID=2082640)
* Verify it's added to `%PATH%` by typing `python` in `cmd`.
* macOS: `brew install python@3`
* **[Windows]** [Visual Studio 2026 Community](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx)
* Select `C++ Development` and verify the following packages are included:
* `Windows 11 SDK`
* `CMake Tools`
* `C++ Clang Compiler`
* `C++ Clang-cl`
* **[macOS]** [Xcode 16.4+](https://developer.apple.com/xcode/download/)
* **[Linux]** Actively tested on Ubuntu 24.04, Arch Linux & derivatives.
* Ubuntu 24.04+ packages
```
build-essential curl git ninja-build clang lld zlib1g-dev libcurl4-openssl-dev \
libglu1-mesa-dev libdbus-1-dev libvulkan-dev libxi-dev libxrandr-dev libasound2-dev libpulse-dev \
libudev-dev libpng-dev libncurses5-dev cmake libx11-xcb-dev python3 python-is-python3 \
libclang-dev libfreetype-dev libxinerama-dev libxcursor-dev python3-markupsafe libgtk-3-dev \
libxss-dev libxtst-dev
```
* Arch Linux packages
```
base-devel cmake ninja llvm vulkan-headers python python-markupsafe clang lld alsa-lib libpulse libxrandr freetype2
```
* Fedora packages
```
cmake vulkan-headers ninja-build clang-devel llvm-devel libpng-devel
```
* It's also important that you install the developer tools and libraries
```
sudo dnf groupinstall "Development Tools" "Development Libraries"
```
#### Setup
Clone and initialize the Dusk repository
```sh
git clone --recursive https://github.com/TwilitRealm/dusk.git
cd dusk
git pull
git submodule update --init --recursive
```
- ### **[Official Website](https://twilitrealm.dev)**
- ### **[Discord](https://discord.gg/QACynxeyna)**
#### Building
# Setup
**⚠️Dusk does NOT provide any copyrighted assets. You must provide your own copy of the game.**
**CLion (Windows / macOS / Linux)**
### 1. Verify your ROM dump
First make sure your dump of the game is clean and supported by Dusk. You can do this by checking the sha1 hash of your dump against this list of supported versions.
Open the project directory in CLion. Enable the appropriate presets for your platform:
| Version | sha1 hash |
| ------------ | ---------------------------------------- |
| GameCube USA | 75edd3ddff41f125d1b4ce1a40378f1b565519e7 |
![CLion](assets/clion.png)
### 2. Download [Dusk](https://github.com/TwilitRealm/dusk/releases)
**Visual Studio (Windows)**
### 3. Setup the game
#### Windows
- Extract the zip folder
- Place your dump of the game into the same folder where you extracted to
- Launch `dusk.exe`
Open the project directory in Visual Studio. The CMake configuration will be loaded automatically.
#### macOS
- TODO
**ninja (macOS)**
#### Linux
- TODO
```sh
cmake --preset macos-default-relwithdebinfo
cmake --build --preset macos-default-relwithdebinfo
```
#### iOS
- TODO
Alternate presets available:
- `macos-default-debug`: Clang, Debug
#### android
- TODO
**ninja (Linux)**
# Building
If you'd like to build Dusk from source, please read the [build instructions](docs/building.md).
```sh
cmake --preset linux-default-relwithdebinfo
cmake --build --preset linux-default-relwithdebinfo
```
# Credits
- Taka
- encounter
- Antidote
- caseif
- CraftyBoss
- crowell
- dooplecks
- gymnast86
- Irastris
- kipcode66
- Lars
- LunarSoap
- Maddie
- MelonSpeedruns
- Pheenoh
- PJB
- Roeming
- YunataSavior
Alternate presets available:
- `linux-default-debug`: GCC, Debug
- `linux-clang-relwithdebinfo`: Clang, RelWithDebInfo
- `linux-clang-debug`: Clang, Debug
**ninja (Windows)**
```sh
cmake --preset windows-msvc-relwithdebinfo
cmake --build --preset windows-msvc-relwithdebinfo
```
Alternate presets available:
- `windows-msvc-debug`: MSVC, Debug
- `windows-clang-relwithdebinfo`: Clang-cl, RelWithDebInfo
- `windows-clang-debug`: Clang-cl, Debug
#### Running
Pass the disc image as a positional argument. Supported formats: ISO (GCM), RVZ, WIA, WBFS, CISO, GCZ
```sh
build/{preset}/dusk /path/to/game.rvz
```
If no path is specified, Dusk defaults to `game.iso` in the current working directory.
#### 30 FPS on Debug
When compiled fully in a Debug the game runs too slowly to hit playable 30 FPS. To avoid this, you can set a CMake cache variable to optimize specific critical files without hampering debuggability in the rest of the program: `-DDUSK_SELECTED_OPT=ON`. When building for MSVC (Windows) you must also modify `CMAKE_CXX_FLAGS_DEBUG` and `CMAKE_C_FLAGS_DEBUG` to remove `/RTC1` from the flags, like so: `-DCMAKE_CXX_FLAGS_DEBUG="/MDd /Zi /Ob0 /Od" -DCMAKE_C_FLAGS_DEBUG="/MDd /Zi /Ob0 /Od"`
Special thanks to the TP Decomp team, the GC/Wii Decomp community, the Aurora developers, and the TP speedrunning community.
-19
View File
@@ -1,19 +0,0 @@
from PIL import Image
FRAME_COUNT = 6572
WIDTH = 48
HEIGHT = 36
out = open("apple.dat", "wb")
for frame in range(1, 6572+1):
print(frame)
img = Image.open(f"apples/{frame}.png")
pixels = img.load()
assert img.width == WIDTH
assert img.height == HEIGHT
for y in range(HEIGHT):
for x in range(WIDTH):
(r, g, b) = pixels[x, y]
if r > 128:
out.write(b"\x01")
else:
out.write(b"\x00")
+2 -2
View File
@@ -9,10 +9,10 @@ chmod +x linuxdeploy-$(uname -m).AppImage
# Build AppImage
cd "$GITHUB_WORKSPACE"
mkdir -p build/appdir/usr/{bin,share/{applications,icons/hicolor}}
cp build/install/!(*.*) build/appdir/usr/bin
cp -r build/install/!(*.*) build/appdir/usr/bin
cp -r platforms/freedesktop/{16x16,32x32,48x48,64x64,128x128,256x256,512x512,1024x1024} build/appdir/usr/share/icons/hicolor
cp platforms/freedesktop/dusk.desktop build/appdir/usr/share/applications
cd build/install
VERSION="$DUSK_VERSION" NO_STRIP=1 "$RUNNER_WORKSPACE"/linuxdeploy-$(uname -m).AppImage \
--appdir "$GITHUB_WORKSPACE"/build/appdir --output appimage
-l /usr/lib/x86_64-linux-gnu/libusb-1.0.so --appdir "$GITHUB_WORKSPACE"/build/appdir --output appimage
+98
View File
@@ -0,0 +1,98 @@
### Building
#### Prerequisites
* [CMake 3.25+](https://cmake.org)
* Windows: Install `CMake Tools` in Visual Studio
* macOS: `brew install cmake`
* [Python 3+](https://python.org)
* Windows: [Microsoft Store](https://go.microsoft.com/fwlink?linkID=2082640)
* Verify it's added to `%PATH%` by typing `python` in `cmd`.
* macOS: `brew install python@3`
* **[Windows]** [Visual Studio 2026 Community](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx)
* Select `C++ Development` and verify the following packages are included:
* `Windows 11 SDK`
* `CMake Tools`
* `C++ Clang Compiler`
* `C++ Clang-cl`
* **[macOS]** [Xcode 16.4+](https://developer.apple.com/xcode/download/)
* **[Linux]** Actively tested on Ubuntu 24.04, Arch Linux & derivatives.
* Ubuntu 24.04+ packages
```
build-essential curl git ninja-build clang lld zlib1g-dev libcurl4-openssl-dev \
libglu1-mesa-dev libdbus-1-dev libvulkan-dev libxi-dev libxrandr-dev libasound2-dev libpulse-dev \
libudev-dev libpng-dev libncurses5-dev cmake libx11-xcb-dev python3 python-is-python3 \
libclang-dev libfreetype-dev libxinerama-dev libxcursor-dev python3-markupsafe libgtk-3-dev \
libxss-dev libxtst-dev
```
* Arch Linux packages
```
base-devel cmake ninja llvm vulkan-headers python python-markupsafe clang lld alsa-lib libpulse libxrandr freetype2
```
* Fedora packages
```
cmake vulkan-headers ninja-build clang-devel llvm-devel libpng-devel
```
* It's also important that you install the developer tools and libraries
```
sudo dnf groupinstall "Development Tools" "Development Libraries"
```
#### Setup
Clone and initialize the Dusk repository
```sh
git clone --recursive https://github.com/TwilitRealm/dusk.git
cd dusk
git pull
git submodule update --init --recursive
```
#### Building
**CLion (Windows / macOS / Linux)**
Open the project directory in CLion. Enable the appropriate presets for your platform:
![CLion](../assets/clion.png)
**Visual Studio (Windows)**
Open the project directory in Visual Studio. The CMake configuration will be loaded automatically.
**ninja (macOS)**
```sh
cmake --preset macos-default-relwithdebinfo
cmake --build --preset macos-default-relwithdebinfo
```
Alternate presets available:
- `macos-default-debug`: Clang, Debug
**ninja (Linux)**
```sh
cmake --preset linux-default-relwithdebinfo
cmake --build --preset linux-default-relwithdebinfo
```
Alternate presets available:
- `linux-default-debug`: GCC, Debug
- `linux-clang-relwithdebinfo`: Clang, RelWithDebInfo
- `linux-clang-debug`: Clang, Debug
**ninja (Windows)**
```sh
cmake --preset windows-msvc-relwithdebinfo
cmake --build --preset windows-msvc-relwithdebinfo
```
Alternate presets available:
- `windows-msvc-debug`: MSVC, Debug
- `windows-clang-relwithdebinfo`: Clang-cl, RelWithDebInfo
- `windows-clang-debug`: Clang-cl, Debug
#### Running
Pass the disc image as a positional argument. Supported formats: ISO (GCM), RVZ, WIA, WBFS, CISO, GCZ
```sh
build/{preset}/dusk /path/to/game.rvz
```
If no path is specified, Dusk defaults to `game.iso` in the current working directory.
+1 -1
+12 -4
View File
@@ -15,7 +15,6 @@ set(DOLZEL_FILES
src/m_Do/m_Do_DVDError.cpp
src/m_Do/m_Do_MemCard.cpp
src/m_Do/m_Do_MemCardRWmng.cpp
src/m_Do/m_Do_machine_exception.cpp
src/m_Do/m_Do_hostIO.cpp
src/c/c_damagereaction.cpp
src/c/c_dylink.cpp
@@ -1334,16 +1333,21 @@ set(DUSK_FILES
include/dusk/endian_gx.hpp
include/dusk/config.hpp
include/dusk/dvd_asset.hpp
include/dusk/scope_guard.hpp
src/dusk/dvd_asset.cpp
src/d/actor/d_a_alink_dusk.cpp
src/dusk/asserts.cpp
src/dusk/config.cpp
src/dusk/crash_reporting.cpp
src/dusk/endian.cpp
src/dusk/extras.c
src/dusk/extras.cpp
src/dusk/file_select.cpp
src/dusk/file_select.hpp
src/dusk/frame_interpolation.cpp
src/dusk/game_clock.cpp
src/dusk/globals.cpp
src/dusk/gyro_aim.cpp
src/dusk/gyro.cpp
src/dusk/io.cpp
src/dusk/layout.cpp
src/dusk/logging.cpp
@@ -1357,10 +1361,10 @@ set(DUSK_FILES
src/dusk/imgui/ImGuiEngine.hpp
src/dusk/imgui/ImGuiMenuGame.cpp
src/dusk/imgui/ImGuiMenuGame.hpp
src/dusk/imgui/ImGuiBloomWindow.cpp
src/dusk/imgui/ImGuiBloomWindow.hpp
src/dusk/imgui/ImGuiMenuTools.cpp
src/dusk/imgui/ImGuiMenuTools.hpp
src/dusk/imgui/ImGuiMenuEnhancements.cpp
src/dusk/imgui/ImGuiMenuEnhancements.hpp
src/dusk/imgui/ImGuiPreLaunchWindow.cpp
src/dusk/imgui/ImGuiPreLaunchWindow.hpp
src/dusk/imgui/ImGuiFirstRunPreset.hpp
@@ -1373,8 +1377,12 @@ set(DUSK_FILES
src/dusk/imgui/ImGuiStubLog.cpp
src/dusk/imgui/ImGuiMapLoader.cpp
src/dusk/imgui/ImGuiSaveEditor.cpp
src/dusk/imgui/ImGuiStateShare.hpp
src/dusk/imgui/ImGuiStateShare.cpp
src/dusk/iso_validate.cpp
src/dusk/offset_ptr.cpp
src/dusk/OSContext.cpp
src/dusk/OSThread.cpp
src/dusk/OSMutex.cpp
src/dusk/discord_presence.cpp
)
Generated
+27
View File
@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1775710090,
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "4c1018dae018162ec878d42fec712642d214fdfa",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
+33
View File
@@ -0,0 +1,33 @@
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
};
outputs = { self, nixpkgs }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
dusk = pkgs.stdenv.mkDerivation {
name = "dusk";
src = ./.;
nativeBuildInputs = [
pkgs.cmake
pkgs.pkg-config
pkgs.wayland
];
buildInputs = [
pkgs.libGL
pkgs.libX11
pkgs.libXcursor
pkgs.libxi
pkgs.libxcb
pkgs.libxrandr
pkgs.libxscrnsaver
pkgs.libxtst
pkgs.libjpeg8
pkgs.libxkbcommon
pkgs.libglvnd
];
};
in {
packages.x86_64-linux.default = dusk;
};
}
+4
View File
@@ -39,6 +39,10 @@ enum Z2WolfHowlCurveID {
Z2WOLFHOWL_NEWSONG2,
Z2WOLFHOWL_NEWSONG3,
#if TARGET_PC
Z2WOLFHOWL_TIMESONG,
#endif
Z2WOLFHOWL_MAX
};
+2 -1
View File
@@ -4549,8 +4549,9 @@ public:
/* 0x03850 */ daAlink_procFunc mpProcFunc;
#if TARGET_PC
void handleWolfHowl();
void handleQuickTransform();
bool checkGyroAimItemContext();
bool checkGyroAimContext();
#endif
}; // Size: 0x385C
+3
View File
@@ -44,6 +44,9 @@ public:
int draw();
int execute();
void drawMeter();
#if TARGET_PC
void updateOnWide();
#endif
void setComboCount(u8, u8);
void setScoreCount(u32);
void addScoreCount(cXyz*, u32, u8);
+9
View File
@@ -220,6 +220,15 @@ public:
/* 0x17E2 */ s16 wait_roll_angle; ///< @brief Roll angle during wait state.
/* 0x17E4 */ u8 field_0x17e4[0x17e8 - 0x17e4];
/* 0x17E8 */ f32 ride_speed_max; ///< @brief Speed rate for riding calculations.
#if TARGET_PC
cXyz himo_mat_interp_prev[2][16];
cXyz himo_mat_interp_curr[2][16];
cXyz himo_tex_interp_prev[2];
cXyz himo_tex_interp_curr[2];
bool himo_interp_prev_valid;
bool himo_interp_curr_valid;
s8 demo_cam_sync_ticks;
#endif
};
STATIC_ASSERT(sizeof(e_wb_class) == 0x17EC);
+3
View File
@@ -25,6 +25,9 @@ public:
/* 0x164 */ cXyz mMinVal;
/* 0x170 */ cXyz mMaxVal;
/* 0x17C */ cXyz mViewScale;
#if TARGET_PC
bool mbReset = false;
#endif
};
/**
+6
View File
@@ -94,6 +94,12 @@ static void __THPAudioInitialize(THPAudioDecodeInfo* info, u8* ptr);
#define THP_TEXTURE_SET_COUNT 3
#endif
#if TARGET_PC
namespace dusk {
void MoviePlayerShutdown();
}
#endif
struct daMP_THPPlayer {
/* 0x000 */ DVDFileInfo fileInfo;
/* 0x03C */ THPHeader header;
+1 -5
View File
@@ -41,7 +41,7 @@ public:
*
*/
class daObj_SMTile_c : public fopAc_ac_c {
public:
private:
/* 0x568 */ mDoExt_brkAnm mBrk;
/* 0x580 */ OBJ_SMTILE_HIO_CLASS* mpHIO;
/* 0x584 */ request_of_phase_process_class mPhase;
@@ -59,10 +59,6 @@ public:
/* 0xB29 */ u8 field_0xb29;
/* 0xB2A */ u8 field_0xb2a;
/* 0xB2B */ u8 field_0xb2b;
bool bad_apple;
u32 bad_apple_frame;
JAISoundHandle apple_sound;
public:
virtual ~daObj_SMTile_c();
int create();
+2 -4
View File
@@ -68,10 +68,8 @@ public:
/* 0x904 */ cXyz field_0x904[2];
/* 0x91C */ int field_0x91c;
/* 0x920 */ cXyz field_0x920[63];
/* 0xC14 */ f32 field_0xc14[4];
/* 0xC24 */ u8 field_0xc24[0xd10 - 0xc24];
/* 0xD10 */ s8 field_0xd10[4];
/* 0xD14 */ u8 field_0xd14[0xd50 - 0xd14];
/* 0xC14 */ f32 field_0xc14[63];
/* 0xD10 */ s8 field_0xd10[64];
/* 0xD50 */ mDoExt_3DlineMat1_c field_0xd50;
/* 0xD8C */ int field_0xd8c;
};
+2 -4
View File
@@ -4834,8 +4834,7 @@ inline void dComIfGd_drawXluListDark() {
inline void dComIfGd_drawXluListInvisible() {
ZoneScoped;
#ifdef TARGET_PC
if (dusk::getSettings().game.enableWaterRefraction &&
!dusk::getSettings().game.enableFrameInterpolation) {
if (!dusk::getSettings().game.disableWaterRefraction) {
#endif
g_dComIfG_gameInfo.drawlist.drawXluListInvisible();
#ifdef TARGET_PC
@@ -4846,8 +4845,7 @@ inline void dComIfGd_drawXluListInvisible() {
inline void dComIfGd_drawOpaListInvisible() {
ZoneScoped;
#ifdef TARGET_PC
if (dusk::getSettings().game.enableWaterRefraction &&
!dusk::getSettings().game.enableFrameInterpolation) {
if (!dusk::getSettings().game.disableWaterRefraction) {
#endif
g_dComIfG_gameInfo.drawlist.drawOpaListInvisible();
#ifdef TARGET_PC
+9 -2
View File
@@ -209,6 +209,10 @@ public:
/* 0x04 */ TGXTexObj* mpTexObj;
/* 0x08 */ Mtx mVolumeMtx;
/* 0x38 */ Mtx mMtx;
#if TARGET_PC
const void* mVolumeMtxKey;
const void* mMtxKey;
#endif
}; // Size: 0x68
struct cBgD_Vtx_t;
@@ -308,8 +312,11 @@ private:
/* 0x0000C */ dDlst_shadowSimple_c mSimple[128];
/* 0x0340C */ int mNextID;
/* 0x03410 */ dDlst_shadowReal_c mReal[8];
/* 0x15EB0 */ TGXTexObj field_0x15eb0[2];
/* 0x15EF0 */ void* field_0x15ef0[2];
/* 0x15EB0 */ TGXTexObj mShadowTexObj[2];
/* 0x15EF0 */ void* mShadowTexData[2];
#if TARGET_PC
int mTexResScale;
#endif
};
class dDlst_window_c {
+16
View File
@@ -10,6 +10,7 @@
#include "JSystem/J3DGraphLoader/J3DAnmLoader.h"
class dFile_info_c;
class J2DPicture;
class dDlst_FileSel_c : public dDlst_base_c {
public:
@@ -113,6 +114,14 @@ public:
/* 0x04 */ J2DScreen* Scr3m;
};
class dDlst_FileSelFade_c : public dDlst_base_c {
public:
void draw();
virtual ~dDlst_FileSelFade_c() {}
/* 0x04 */ J2DPicture* mpPict;
};
class dFs_HIO_c : public JORReflexible {
public:
dFs_HIO_c();
@@ -676,6 +685,9 @@ public:
#if PLATFORM_GCN
/* 0x2378 */ J2DPicture* mpFadePict;
#endif
#ifdef TARGET_PC
dDlst_FileSelFade_c mFadeDlst;
#endif
#if PLATFORM_WII || PLATFORM_SHIELD
/* 0x2376 */ u8 field_0x2376[SAVEFILE_SIZE];
@@ -684,6 +696,10 @@ public:
#endif
};
#ifdef TARGET_PC
STATIC_ASSERT(sizeof(dFile_select_c) == 0x237C + sizeof(dDlst_FileSelFade_c));
#else
STATIC_ASSERT(sizeof(dFile_select_c) == 0x237C);
#endif
#endif /* D_FILE_D_FILE_SELECT_H */
+3 -1
View File
@@ -75,7 +75,9 @@ public:
/* 0x8 */ BE(u16) mAreaName;
/* 0xA */ u8 mCount;
#ifdef _MSVC_LANG
u8* __get_mRoomNos() const { return (u8*)(this + 1); }
// Room numbers start at offset 0xB (right after mCount), NOT at sizeof(data)=12.
// (u8*)(this+1) would give offset 12 because MSVC sizeof=12; use &mCount+1 instead.
u8* __get_mRoomNos() const { return (u8*)&mCount + 1; }
__declspec(property(get = __get_mRoomNos)) u8* mRoomNos;
#else
/* 0xB */ u8 mRoomNos[0];
+3
View File
@@ -204,6 +204,9 @@ private:
/* 0x6D1 */ u8 field_0x6d1;
/* 0x6D2 */ u8 field_0x6d2;
/* 0x6D3 */ u8 field_0x6d3;
#if TARGET_PC
f32 mSelectItemSlideElapsed[4];
#endif
};
#endif /* D_MENU_D_MENU_RING_H */
+4
View File
@@ -110,6 +110,10 @@ struct dMsgScrnHowl_c : public dMsgScrnBase_c {
/* 0x27A0 */ f32 field_0x27a0;
/* 0x27A4 */ f32 field_0x27a4;
/* 0x27A8 */ f32 field_0x27a8;
#if TARGET_PC
u8 showCursor;
#endif
};
#endif /* MSG_SCRN_D_MSG_SCRN_HOWL_H */
+18 -6
View File
@@ -487,19 +487,31 @@ public:
}
}
char* getPlayerName() const { return const_cast<char*>(mPlayerName); }
void setPlayerName(const char* i_name) { strcpy(mPlayerName, i_name); }
void setPlayerName(const char* i_name) {
#if AVOID_UB
strncpy(mPlayerName, i_name, sizeof(mPlayerName) - 1);
mPlayerName[sizeof(mPlayerName) - 1] = '\0';
#else
strcpy(mPlayerName, i_name);
#endif
}
char* getHorseName() const { return const_cast<char*>(mHorseName); }
void setHorseName(const char* i_name) { strcpy(mHorseName, i_name); }
void setHorseName(const char* i_name) {
#if AVOID_UB
strncpy(mHorseName, i_name, sizeof(mHorseName) - 1);
mHorseName[sizeof(mHorseName) - 1] = '\0';
#else
strcpy(mHorseName, i_name);
#endif
}
u8 getClearCount() const { return mClearCount; }
/* 0x00 */ BE(u64) unk0;
/* 0x08 */ BE(s64) mTotalTime;
/* 0x10 */ BE(u16) unk16;
/* 0x12 */ BE(u16) mDeathCount;
/* 0x14 */ char mPlayerName[16];
/* 0x24 */ u8 unk36;
/* 0x25 */ char mHorseName[16];
/* 0x35 */ u8 unk53;
/* 0x14 */ char mPlayerName[17];
/* 0x25 */ char mHorseName[17];
/* 0x36 */ u8 mClearCount;
/* 0x37 */ u8 unk55[5];
}; // Size: 0x40
+7
View File
@@ -47,6 +47,10 @@ public:
mPositionY = y;
}
#ifdef TARGET_PC
void refreshAspectScale();
#endif
void onUpdateFlag() { mUpdateFlag = true; }
void resetUpdateFlag() { mUpdateFlag = false; }
@@ -79,6 +83,9 @@ private:
/* 0x58 */ f32 mPositionX;
/* 0x5C */ f32 mPositionY;
/* 0x60 */ f32 mParam1;
#ifdef TARGET_PC
f32 mBaseParam1;
#endif
/* 0x64 */ f32 mParam2;
/* 0x68 */ f32 mParam3;
/* 0x6C */ f32 mParam4;
+2
View File
@@ -12,6 +12,8 @@ namespace dusk::audio {
void SetMasterVolume(f32 value);
void SetPaused(bool paused);
u32 GetResetCount(int channelIdx);
f32 VolumeFromU16(u16 value);
+11
View File
@@ -146,6 +146,12 @@ concept ConfigValue =
template <ConfigValue T>
const ConfigImplBase* GetConfigImpl();
template <typename T>
struct ConfigEnumRange {
static constexpr auto min = std::numeric_limits<std::underlying_type_t<T>>::min();
static constexpr auto max = std::numeric_limits<std::underlying_type_t<T>>::max();
};
/**
* \brief A CVar storing values.
*
@@ -189,6 +195,11 @@ public:
}
}
[[nodiscard]] constexpr const T& getDefaultValue() const noexcept {
checkRegistered();
return defaultValue;
}
/**
* \brief Change the value of a CVar.
*
+8
View File
@@ -0,0 +1,8 @@
#pragma once
namespace dusk {
void InitializeCrashReporting();
void ShutdownCrashReporting();
} // namespace dusk
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#ifdef DUSK_DISCORD_RPC
namespace dusk {
namespace discord {
void Initialize();
void RunCallbacks();
void UpdatePresence();
void Shutdown();
}
}
#endif // DUSK_DISCORD_RPC
-1
View File
@@ -6,7 +6,6 @@
#include "aurora/gfx.h"
extern AuroraInfo auroraInfo;
extern const char* configPath;
namespace dusk {
extern AuroraStats lastFrameAuroraStats;
+25 -17
View File
@@ -1,13 +1,12 @@
#ifndef DUSK_FRAME_INTERP_H
#define DUSK_FRAME_INTERP_H
#pragma once
#include <dolphin/mtx.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
struct cXyz;
class camera_process_class;
class view_class;
#ifdef __cplusplus
namespace dusk {
@@ -15,29 +14,38 @@ namespace frame_interp {
void ensure_initialized();
void begin_record_camera();
void begin_record();
void end_record();
void interpolate(float step);
void begin_frame(bool enabled, bool is_sim_frame, float step);
void interpolate();
float get_interpolation_step();
void notify_sim_tick_complete();
uint32_t begin_presentation_ui_pass();
uint32_t get_presentation_ui_advance_ticks();
void end_presentation_ui_pass();
void open_child(const void* key, int32_t id);
void close_child();
void request_presentation_sync();
bool presentation_sync_active();
bool is_enabled();
// TODO: These should be phased out as UI is progressively updated to use game_clock
void set_ui_tick_pending(bool value);
bool get_ui_tick_pending();
bool is_sim_frame();
void record_camera(::camera_process_class* cam, int camera_id);
void record_final_mtx_raw(const Mtx* dest, const Mtx src);
void interp_view(::view_class* view);
void record_final_mtx(Mtx m, const void *key);
void record_final_mtx(Mtx m);
bool lookup_replacement(const void* source, Mtx out);
bool lookup_replacement(const void* key, Mtx out);
bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out);
void camera_eye_from_view_mtx(MtxP view_mtx, cXyz* o_eye);
bool build_star_view(Mtx o_view, Mtx o_cam_billboard_base, cXyz* o_anchor_eye, float* o_fovy);
typedef void (*InterpolationCallBack)(bool isSimFrame, void* pUserWork);
// call on a sim tick, will get called during presentation
void add_interpolation_callback(InterpolationCallBack pCallBack, void* pUserWork);
void begin_presentation_camera();
void end_presentation_camera();
} // namespace frame_interp
} // namespace dusk
#endif
#endif
+33
View File
@@ -0,0 +1,33 @@
#ifndef DUSK_GAME_CLOCK_H
#define DUSK_GAME_CLOCK_H
#include <stddef.h>
namespace dusk {
namespace game_clock {
void ensure_initialized();
void reset_accumulator();
void reset_frame_timer();
constexpr float sim_pace() { return 1.0f / 30.0f; }
constexpr float period_for_original_frames(float frame_count) { return frame_count * sim_pace(); }
constexpr float ui_maximum_dt() { return 0.05f; }
constexpr float ui_initial_dt() { return 1.0f / 60.0f; }
struct MainLoopPacer {
float presentation_dt_seconds;
bool is_interpolating;
bool do_sim_tick;
float interpolation_step;
float sim_pace;
};
MainLoopPacer advance_main_loop();
float consume_interval(const void* consumer);
} // namespace game_clock
} // namespace dusk
#endif // DUSK_GAME_CLOCK_H
+17
View File
@@ -0,0 +1,17 @@
#ifndef DUSK_GYRO_H
#define DUSK_GYRO_H
namespace dusk::gyro {
void read(float dt);
void getAimDeltas(float& out_yaw, float& out_pitch);
bool queryGyroAimContext();
void rollgoalTick(bool play_active, s16 camera_yaw);
void rollgoalTableOffset(s16& out_ax, s16& out_az);
extern bool s_sensor_keep_alive;
bool get_sensor_keep_alive();
void set_sensor_keep_alive(bool value);
} // namespace dusk::gyro
#endif
-10
View File
@@ -1,10 +0,0 @@
#ifndef DUSK_GYRO_AIM_H
#define DUSK_GYRO_AIM_H
namespace dusk::gyro_aim {
void read(float dt, bool context_active);
void consumeAimDeltas(float& out_yaw_rad, float& out_pitch_rad);
bool queryGyroAimItemContext();
} // namespace dusk::gyro_aim
#endif
+8 -5
View File
@@ -9,14 +9,17 @@ constexpr const char* DO_RESET = "Cmd+R";
constexpr const char* DO_RESET = "Ctrl+R";
#endif
constexpr const char* TOGGLE_FULLSCREEN = "F11";
constexpr const char* SHOW_PROCESS_MANAGEMENT = "F2";
constexpr const char* SHOW_DEBUG_OVERLAY = "F3";
constexpr const char* SHOW_HEAP_VIEWER = "F4";
constexpr const char* SHOW_STUB_LOG = "F5";
constexpr const char* SHOW_CAMERA_DEBUG = "F6";
constexpr const char* SHOW_AUDIO_DEBUG = "F7";
constexpr const char* SHOW_PLAYER_INFO = "F5";
constexpr const char* SHOW_SAVE_EDITOR = "F6";
constexpr const char* SHOW_MAP_LOADER = "F7";
constexpr const char* SHOW_STATE_SHARE = "F8";
constexpr const char* SHOW_DEBUG_CAMERA = "F9";
constexpr const char* SHOW_AUDIO_DEBUG = "F10";
constexpr const char* TOGGLE_FULLSCREEN = "F11";
constexpr const char* TURBO = "Tab";
+5
View File
@@ -4,9 +4,14 @@
#include <aurora/aurora.h>
#include <aurora/lib/logging.hpp>
#include <filesystem>
void aurora_log_callback(AuroraLogLevel level, const char* module, const char* message, unsigned int len);
namespace dusk {
void InitializeFileLogging(const std::filesystem::path& configDir, AuroraLogLevel logLevel);
void ShutdownFileLogging();
const char* GetLogFilePath();
void SendToStubLog(AuroraLogLevel level, const char* module, const char* message);
}
+4
View File
@@ -1,10 +1,14 @@
#ifndef DUSK_MAIN_H
#define DUSK_MAIN_H
#include <filesystem>
namespace dusk {
extern bool IsRunning;
extern bool IsShuttingDown;
extern bool IsGameLaunched;
extern bool IsFocusPaused;
extern std::filesystem::path ConfigPath;
}
#endif // DUSK_MAIN_H
+20
View File
@@ -0,0 +1,20 @@
#ifndef DUSK_SCOPE_GUARD_HPP
#define DUSK_SCOPE_GUARD_HPP
#include <functional>
class SimpleScopeGuard {
public:
// Store the function in the constructor
explicit SimpleScopeGuard(const std::function<void()>& func) : m_func(func) {}
// Run the function when the object goes out of scope
~SimpleScopeGuard() {
if (m_func) m_func();
}
private:
std::function<void()> m_func;
};
#endif //DUSK_SCOPE_GUARD_HPP
+42 -6
View File
@@ -7,6 +7,20 @@ namespace dusk {
using namespace config;
enum class BloomMode : int {
Off = 0,
Classic = 1,
Dusk = 2,
};
namespace config {
template <>
struct ConfigEnumRange<BloomMode> {
static constexpr auto min = BloomMode::Off;
static constexpr auto max = BloomMode::Dusk;
};
}
// Persistent user settings
struct UserSettings {
@@ -47,16 +61,23 @@ struct UserSettings {
ConfigVar<bool> noMissClimbing;
ConfigVar<bool> fastTears;
ConfigVar<bool> instantSaves;
ConfigVar<bool> instantText;
ConfigVar<bool> sunsSong;
// Preferences
ConfigVar<bool> enableMirrorMode;
ConfigVar<bool> invertCameraXAxis;
ConfigVar<bool> disableMainHUD;
ConfigVar<bool> pauseOnFocusLost;
// Graphics
ConfigVar<bool> enableBloom;
ConfigVar<bool> enableWaterRefraction;
ConfigVar<BloomMode> bloomMode;
ConfigVar<float> bloomMultiplier;
ConfigVar<bool> disableWaterRefraction;
ConfigVar<bool> enableFrameInterpolation;
ConfigVar<int> internalResolutionScale;
ConfigVar<int> shadowResolutionMultiplier;
ConfigVar<bool> enableDepthOfField;
// Audio
ConfigVar<bool> noLowHpSound;
@@ -64,12 +85,25 @@ struct UserSettings {
// Input
ConfigVar<bool> enableGyroAim;
ConfigVar<float> gyroAimSensitivityX;
ConfigVar<float> gyroAimSensitivityY;
ConfigVar<bool> gyroAimInvertPitch;
ConfigVar<bool> gyroAimInvertYaw;
ConfigVar<bool> enableGyroRollgoal;
ConfigVar<float> gyroSensitivityX;
ConfigVar<float> gyroSensitivityY;
ConfigVar<float> gyroSensitivityRollgoal;
ConfigVar<float> gyroSmoothing;
ConfigVar<float> gyroDeadband;
ConfigVar<bool> gyroInvertPitch;
ConfigVar<bool> gyroInvertYaw;
// Cheats
ConfigVar<bool> infiniteHearts;
ConfigVar<bool> infiniteArrows;
ConfigVar<bool> infiniteBombs;
ConfigVar<bool> infiniteOil;
ConfigVar<bool> infiniteOxygen;
ConfigVar<bool> infiniteRupees;
ConfigVar<bool> moonJump;
ConfigVar<bool> superClawshot;
ConfigVar<bool> alwaysGreatspin;
ConfigVar<bool> enableFastIronBoots;
ConfigVar<bool> canTransformAnywhere;
ConfigVar<bool> fastSpinner;
@@ -88,6 +122,8 @@ struct UserSettings {
ConfigVar<bool> skipPreLaunchUI;
ConfigVar<bool> showPipelineCompilation;
ConfigVar<bool> wasPresetChosen;
ConfigVar<bool> enableCrashReporting;
ConfigVar<bool> duskMenuOpen;
} backend;
};
+1 -1
View File
@@ -25,7 +25,7 @@ typedef struct leafdraw_class : base_process_class {
#endif
/* 0xB8 */ leafdraw_method_class* leaf_methods;
/* 0xBC */ s8 unk_0xBC;
/* 0xBD */ u8 unk_0xBD;
/* 0xBD */ u8 draw_interp_frame;
/* 0xBE */ draw_priority_class draw_priority;
} leafdraw_class;
+1
View File
@@ -18,6 +18,7 @@ typedef struct process_node_class {
/* 0x0BC */ layer_class layer;
/* 0x0E8 */ node_list_class layer_nodelist[16];
/* 0x1A8 */ s8 unk_0x1A8;
/* 0x1A9 */ s8 draw_interp_frame;
} process_node_class;
typedef struct node_process_profile_definition {
+2
View File
@@ -220,10 +220,12 @@ using std::isnan;
// Some basic macros that are more convenient than putting down #if blocks for one-line changes.
#if TARGET_PC
#define IF_DUSK(statement) statement
#define IF_DUSK_ARG(expr) , expr
#define IF_NOT_DUSK(statement)
#define DUSK_IF_ELSE(dusk, orig) dusk
#else
#define IF_DUSK(statement)
#define IF_DUSK_ARG(expr)
#define IF_NOT_DUSK(statement) statement
#define DUSK_IF_ELSE(dusk, orig) orig
#endif
+19 -8
View File
@@ -125,8 +125,15 @@ public:
#if TARGET_PC
static f32 hudAspectScaleDown;
static f32 hudAspectScaleUp;
static f32 ScaleHUDXLeft(f32 baseX) { return getMinXF() + baseX; }
static f32 ScaleHUDXRight(f32 baseX) { return -getMinXF() + baseX; }
static void updateSafeAreaBounds();
static f32 getSafeMinXF() { return m_safeMinXF; }
static f32 getSafeMinYF() { return m_safeMinYF; }
static f32 getSafeWidthF() { return m_safeWidthF; }
static f32 getSafeHeightF() { return m_safeHeightF; }
static f32 getSafeMaxXF() { return m_safeMaxXF; }
static f32 getSafeMaxYF() { return m_safeMaxYF; }
static f32 ScaleHUDXLeft(f32 baseX) { return getSafeMinXF() + baseX; }
static f32 ScaleHUDXRight(f32 baseX) { return getSafeMaxXF() - FB_WIDTH_BASE + baseX; }
#endif
static void setBlureMtx(const Mtx m) {
@@ -279,12 +286,7 @@ public:
#if WIDESCREEN_SUPPORT
static void setTvSize();
#if TARGET_PC
static void onWide(f32 width, f32 height);
#else
static void onWide();
#endif
static void offWide();
static u8 isWide();
@@ -297,7 +299,7 @@ public:
#endif
#if TARGET_PC
static void setWindowSize(AuroraWindowSize const& size);
static void updateRenderSize();
#endif
static TGXTexObj mFrameBufferTexObj;
@@ -369,6 +371,15 @@ public:
static int m_height;
static f32 m_heightF;
static f32 m_widthF;
#if TARGET_PC
static f32 m_safeMinXF;
static f32 m_safeMinYF;
static f32 m_safeMaxXF;
static f32 m_safeMaxYF;
static f32 m_safeWidthF;
static f32 m_safeHeightF;
#endif
#endif
};
-4
View File
@@ -43,10 +43,6 @@ struct mDoLib_clipper {
};
void mDoLib_project(Vec* src, Vec* dst);
#if TARGET_PC
void mDoLib_project(Vec* src, Vec* dst, JGeometry::TBox2<f32> viewport);
#endif
u32 mDoLib_setResTimgObj(ResTIMG const* res, TGXTexObj* o_texObj, u32 tlut_name,
GXTlutObj* o_tlutObj);
void mDoLib_pos2camera(Vec* src, Vec* dst);
@@ -79,6 +79,10 @@ public:
virtual void viewCalc();
virtual ~J3DModel() {}
#if TARGET_PC
static void interp_callback(bool isSimFrame, void* pUserWork);
#endif
J3DModelData* getModelData() { return mModelData; }
void onFlag(u32 flag) { mFlags |= flag; }
@@ -105,9 +109,7 @@ public:
void setAnmMtx(int jointNo, Mtx m) {
mMtxBuffer->setAnmMtx(jointNo, m);
#ifdef TARGET_PC
dusk::frame_interp::record_final_mtx_raw(
reinterpret_cast<const Mtx*>(mMtxBuffer->getAnmMtx(jointNo)),
mMtxBuffer->getAnmMtx(jointNo));
dusk::frame_interp::record_final_mtx(mMtxBuffer->getAnmMtx(jointNo));
#endif
}
MtxP getAnmMtx(int jointNo) { return mMtxBuffer->getAnmMtx(jointNo); }
@@ -23,6 +23,10 @@ public:
void syncJ3DSysPointers() const;
void syncJ3DSysFlags() const;
#if TARGET_PC
bool needsInterpCallBack() const;
#endif
virtual ~J3DModelData() {}
void simpleCalcMaterial(Mtx mtx) { simpleCalcMaterial(0, mtx); }
@@ -33,6 +33,9 @@ public:
void copy(J3DMaterial*);
s32 newSharedDisplayList(u32);
s32 newSingleSharedDisplayList(u32);
#if TARGET_PC
bool needsInterpCallBack() const;
#endif
virtual void calc(f32 const (*)[4]);
virtual void calcDiffTexMtx(f32 const (*)[4]);
@@ -46,7 +49,6 @@ public:
virtual void change();
J3DMaterial() { initialize(); }
~J3DMaterial() {}
J3DMaterial* getNext() { return mNext; }
J3DShape* getShape() { return mShape; }
J3DTevBlock* getTevBlock() { return mTevBlock; }
@@ -11,9 +11,17 @@
struct JASCalc {
static void imixcopy(const s16*, const s16*, s16*, u32);
static void bcopyfast(const void* src, void* dest, u32 size);
#if TARGET_ANDROID
static void _bcopy(const void* src, void* dest, u32 size);
#else
static void bcopy(const void* src, void* dest, u32 size);
#endif
static void bzerofast(void* dest, u32 size);
#if TARGET_ANDROID
static void _bzero(void* dest, u32 size);
#else
static void bzero(void* dest, u32 size);
#endif
static f32 pow2(f32);
template <typename A, typename B>
@@ -101,10 +101,6 @@ public:
void setDrawDoneMethod(EDrawDone drawDone) { mDrawDoneMethod = drawDone; }
void setFader(JUTFader* fader) { mFader = fader; }
#ifdef TARGET_PC
// For frame interpolation
void setFaderSimSteps(u32 steps);
#endif
void resetFader() { setFader(NULL); }
JUTFader* getFader() const { return mFader; }
void setClearColor(JUtility::TColor color) { mClearColor = color; }
@@ -11,8 +11,10 @@
class JUTFader {
public:
enum EStatus {
UNKSTATUS_M1 = -1,
UNKSTATUS_0 = 0,
None,
Wait,
FadeIn,
FadeOut,
};
JUTFader(int, int, int, int, JUtility::TColor);
@@ -29,12 +31,12 @@ public:
void setColor(JUtility::TColor color) { mColor.set(color); }
/* 0x04 */ s32 mStatus;
/* 0x08 */ u16 field_0x8;
/* 0x0A */ u16 field_0xa;
/* 0x08 */ u16 mDuration;
/* 0x0A */ u16 mTimer;
/* 0x0C */ JUtility::TColor mColor;
/* 0x10 */ JGeometry::TBox2<f32> mBox;
/* 0x20 */ int mEStatus;
/* 0x24 */ u32 field_0x24;
/* 0x20 */ int mStatusTimer;
/* 0x24 */ u32 mNextStatus;
};
#endif /* JUTFADER_H */
@@ -5,6 +5,17 @@
#include <cstring>
#include "dusk/endian.h"
#if TARGET_PC
struct FontDrawContext {
bool isTextureLoaded = false;
};
#define FONT_DRAW_CTX , FontDrawContext* context
#define FONT_DRAW_CTX_ARG , context
#else
#define FONT_DRAW_CTX
#define FONT_DRAW_CTX_ARG
#endif
/**
* @ingroup jsystem-jutility
*
@@ -84,7 +95,12 @@ public:
/* 0x0C */ virtual void setGX() = 0;
/* 0x10 */ virtual void setGX(JUtility::TColor col1, JUtility::TColor col2) { setGX(); }
/* 0x14 */ virtual f32 drawChar_scale(f32 a1, f32 a2, f32 a3, f32 a4, int a5, bool a6) = 0;
/* 0x14 */ virtual f32 drawChar_scale(f32 a1, f32 a2, f32 a3, f32 a4, int a5, bool a6 FONT_DRAW_CTX) = 0;
#if TARGET_PC
f32 drawChar_scale(f32 a1, f32 a2, f32 a3, f32 a4, int a5, bool a6) {
return drawChar_scale(a1, a2, a3, a4, a5, a6, nullptr);
}
#endif
/* 0x18 */ virtual int getLeading() const = 0;
/* 0x1C */ virtual s32 getAscent() const = 0;
/* 0x20 */ virtual s32 getDescent() const = 0;
@@ -97,6 +113,11 @@ public:
/* 0x3C */ virtual ResFONT* getResFont() const = 0;
/* 0x40 */ virtual bool isLeadByte(int a1) const = 0;
#if TARGET_PC
virtual void pushDrawState() = 0;
virtual void popDrawState() = 0;
#endif
static bool isLeadByte_1Byte(int b) {
return false;
}
@@ -18,10 +18,6 @@ struct BlockHeader {
BE(u32) size;
};
#if TARGET_PC
struct GlyphTextures;
#endif
/**
* @ingroup jsystem-jutility
*
@@ -31,7 +27,7 @@ public:
virtual ~JUTResFont();
virtual void setGX();
virtual void setGX(JUtility::TColor, JUtility::TColor);
virtual f32 drawChar_scale(f32, f32, f32, f32, int, bool);
virtual f32 drawChar_scale(f32, f32, f32, f32, int, bool FONT_DRAW_CTX);
virtual int getLeading() const;
virtual s32 getAscent() const;
virtual s32 getDescent() const;
@@ -43,7 +39,7 @@ public:
virtual int getFontType() const;
virtual ResFONT* getResFont() const;
virtual bool isLeadByte(int) const;
virtual void loadImage(int, GXTexMapID);
virtual void loadImage(int, GXTexMapID FONT_DRAW_CTX);
virtual void setBlock();
JUTResFont(ResFONT const*, JKRHeap*);
@@ -53,10 +49,15 @@ public:
bool initiate(ResFONT const*, JKRHeap*);
bool protected_initiate(ResFONT const*, JKRHeap*);
void countBlock();
void loadFont(int, GXTexMapID, JUTFont::TWidth*);
void loadFont(int, GXTexMapID, JUTFont::TWidth* FONT_DRAW_CTX);
int getFontCode(int) const;
int convertSjis(int, BE(u16)*) const;
#if TARGET_PC
void pushDrawState() override;
void popDrawState() override;
#endif
inline void delete_and_initialize() {
deleteMemBlocks_ResFont();
initialize_state();
@@ -68,11 +69,7 @@ public:
// some types uncertain, may need to be fixed
/* 0x1C */ int mWidth;
/* 0x20 */ int mHeight;
#if TARGET_PC
GlyphTextures* mGlyphTextures;
#else
/* 0x24 */ TGXTexObj mTexObj;
#endif
/* 0x44 */ int mTexPageIdx;
/* 0x48 */ const ResFONT* mResFont;
/* 0x4C */ ResFONT::INF1* mInf1Ptr;
@@ -86,6 +83,16 @@ public:
/* 0x66 */ u16 field_0x66;
/* 0x68 */ u16 mMaxCode;
/* 0x6C */ const IsLeadByte_func* mIsLeadByte;
#if TARGET_PC
// Dusk change: we use a single large texture for all characters.
// This enables better draw call merging, ideally enabling entire blocks of
// text to be one draw call.
TGXTexObj mJoinedTextureObject;
u16 mJoinedTextureHeight;
void initJoinedTexture();
#endif
};
extern u8 const JUTResFONT_Ascfont_fix12[];
@@ -33,24 +33,16 @@ public:
static void postRetraceProc(u32);
static void drawDoneCallback();
u16 getFbWidth() const {
#if TARGET_PC
return m_WindowSize.fb_width;
#else
return mRenderObj->fbWidth;
#endif
}
u16 getEfbHeight() const {
#if TARGET_PC
return m_WindowSize.fb_height;
#else
return mRenderObj->efbHeight;
#endif
}
u16 getFbWidth() const { return mRenderObj->fbWidth; }
u16 getEfbHeight() const { return mRenderObj->efbHeight; }
void getBounds(u16& width, u16& height) const {
width = (u16)getFbWidth();
height = (u16)getEfbHeight();
}
#ifdef TARGET_PC
u32 getRenderWidth() const { return mRenderWidth; }
u32 getRenderHeight() const { return mRenderHeight; }
#endif
u16 getXfbHeight() const { return u16(mRenderObj->xfbHeight); }
u8 isAntiAliasing() const { return u8(mRenderObj->aa); }
Pattern getSamplePattern() const { return mRenderObj->sample_pattern; }
@@ -63,7 +55,7 @@ public:
GXRenderModeObj* getRenderMode() const { return mRenderObj; }
#if TARGET_PC
void setWindowSize(AuroraWindowSize const& size);
void setRenderSize(u32 width, u32 height);
#endif
private:
@@ -89,7 +81,8 @@ private:
#if TARGET_PC
public:
AuroraWindowSize m_WindowSize;
u32 mRenderWidth;
u32 mRenderHeight;
#endif
};
@@ -64,10 +64,6 @@ void J2DGrafContext::setup2D() {
}
void J2DGrafContext::setScissor() {
#if TARGET_PC
GXSetScissor(mScissorBounds.i.x, mScissorBounds.i.y, mScissorBounds.getWidth(),
mScissorBounds.getHeight());
#else
JGeometry::TBox2<f32> bounds(0, 0, 1024, 1024);
JGeometry::TBox2<f32> curBounds(mScissorBounds);
mScissorBounds.intersect(bounds);
@@ -81,7 +77,6 @@ void J2DGrafContext::setScissor() {
} else {
GXSetScissor(0, 0, 0, 0);
}
#endif
}
void J2DGrafContext::scissor(JGeometry::TBox2<f32> const& bounds) {
+9 -2
View File
@@ -211,6 +211,11 @@ f32 J2DPrint::parse(const u8* pString, int length, int param_2, u16* param_3,
local_bc.a = local_bc.a * alpha / 0xFF;
mFont->setGradColor(local_b8, field_0x22 ? local_bc : local_b8);
#if TARGET_PC
FontDrawContext context;
mFont->pushDrawState();
#endif
do {
bool b2ByteCharacter = false;
bool r25;
@@ -312,9 +317,9 @@ f32 J2DPrint::parse(const u8* pString, int length, int param_2, u16* param_3,
} else {
if (param_6) {
if (param_3 != NULL) {
mFont->drawChar_scale(mCursorH + (s16)param_3[someIndex], mCursorV, (s32)mScaleX, (s32)mScaleY, iCharacter, true);
mFont->drawChar_scale(mCursorH + (s16)param_3[someIndex], mCursorV, (s32)mScaleX, (s32)mScaleY, iCharacter, true IF_DUSK_ARG(&context));
} else {
mFont->drawChar_scale(mCursorH, mCursorV, (s32)mScaleX, (s32)mScaleY, iCharacter, true);
mFont->drawChar_scale(mCursorH, mCursorV, (s32)mScaleX, (s32)mScaleY, iCharacter, true IF_DUSK_ARG(&context));
}
}
mCursorH += field_0x34;
@@ -353,6 +358,8 @@ f32 J2DPrint::parse(const u8* pString, int length, int param_2, u16* param_3,
iCharacter = *(pString++);
} while (true);
IF_DUSK(mFont->popDrawState());
if (param_3 != NULL) {
param_3[someIndex] = 0xFFFF;
}
+20 -5
View File
@@ -97,6 +97,16 @@ s32 J3DModel::entryModelData(J3DModelData* pModelData, u32 mdlFlags, u32 mtxNum)
return kJ3DError_Success;
}
#if TARGET_PC
void J3DModel::interp_callback(bool isSimFrame, void* pUserWork) {
J3DModel* i_this = static_cast<J3DModel*>(pUserWork);
if (!isSimFrame) {
i_this->calcMaterial();
i_this->diff();
}
}
#endif
s32 J3DModel::createShapePacket(J3DModelData* pModelData) {
J3D_ASSERTMSG(173, pModelData != NULL, "Error : null pointer.");
@@ -452,11 +462,11 @@ void J3DModel::calc() {
#ifdef TARGET_PC
for (u16 i = 0; i < mModelData->getJointNum(); ++i) {
dusk::frame_interp::record_final_mtx_raw(reinterpret_cast<const Mtx*>(getAnmMtx(i)), getAnmMtx(i));
dusk::frame_interp::record_final_mtx(getAnmMtx(i));
}
for (u16 i = 0; i < mModelData->getWEvlpMtxNum(); ++i) {
dusk::frame_interp::record_final_mtx_raw(reinterpret_cast<const Mtx*>(getWeightAnmMtx(i)), getWeightAnmMtx(i));
dusk::frame_interp::record_final_mtx(getWeightAnmMtx(i));
}
#endif
}
@@ -485,6 +495,11 @@ void J3DModel::entry() {
joint->entryIn();
}
}
#if TARGET_PC
if (mModelData->needsInterpCallBack())
dusk::frame_interp::add_interpolation_callback(&J3DModel::interp_callback, this);
#endif
}
void J3DModel::viewCalc() {
@@ -496,7 +511,7 @@ void J3DModel::viewCalc() {
J3DCalcViewBaseMtx(j3dSys.getViewMtx(), mBaseScale, mBaseTransformMtx,
(MtxP)&mInternalView);
#ifdef TARGET_PC
dusk::frame_interp::record_final_mtx_raw(&mInternalView, mInternalView);
dusk::frame_interp::record_final_mtx(mInternalView);
#endif
}
} else if (isCpuSkinningOn()) {
@@ -504,7 +519,7 @@ void J3DModel::viewCalc() {
J3DCalcViewBaseMtx(j3dSys.getViewMtx(), mBaseScale, mBaseTransformMtx,
(MtxP)&mInternalView);
#ifdef TARGET_PC
dusk::frame_interp::record_final_mtx_raw(&mInternalView, mInternalView);
dusk::frame_interp::record_final_mtx(mInternalView);
#endif
}
} else if (checkFlag(J3DMdlFlag_SkinPosCpu)) {
@@ -528,7 +543,7 @@ void J3DModel::viewCalc() {
#ifdef TARGET_PC
for (u16 i = 0; i < mModelData->getDrawMtxNum(); ++i) {
dusk::frame_interp::record_final_mtx_raw(&getDrawMtxPtr()[i], getDrawMtxPtr()[i]);
dusk::frame_interp::record_final_mtx(getDrawMtxPtr()[i]);
}
#endif
@@ -84,6 +84,15 @@ void J3DModelData::simpleCalcMaterial(u16 idx, Mtx param_1) {
}
}
#if TARGET_PC
bool J3DModelData::needsInterpCallBack() const {
for (u16 i = 0, n = getMaterialNum(); i < n; i++)
if (getMaterialNodePointer(i)->needsInterpCallBack())
return true;
return false;
}
#endif
void J3DModelData::syncJ3DSysPointers() const {
j3dSys.setTexture(getTexture());
j3dSys.setVtxPos(getVtxPosArray(), getVtxNum());
+27 -3
View File
@@ -265,7 +265,7 @@ void J3DMaterial::diff(u32 diffFlags) {
}
void J3DMaterial::calc(f32 const (*param_0)[4]) {
if (j3dSys.checkFlag(0x40000000)) {
if (j3dSys.checkFlag(J3DSysFlag_PostTexMtx)) {
mTexGenBlock->calcPostTexMtx(param_0);
} else {
mTexGenBlock->calc(param_0);
@@ -276,7 +276,7 @@ void J3DMaterial::calc(f32 const (*param_0)[4]) {
}
void J3DMaterial::calcDiffTexMtx(f32 const (*param_0)[4]) {
if (j3dSys.checkFlag(0x40000000)) {
if (j3dSys.checkFlag(J3DSysFlag_PostTexMtx)) {
mTexGenBlock->calcPostTexMtxWithoutViewMtx(param_0);
} else {
mTexGenBlock->calcWithoutViewMtx(param_0);
@@ -288,7 +288,7 @@ void J3DMaterial::setCurrentMtx() {
}
void J3DMaterial::calcCurrentMtx() {
if (!j3dSys.checkFlag(0x40000000)) {
if (!j3dSys.checkFlag(J3DSysFlag_PostTexMtx)) {
mCurrentMtx.setCurrentTexMtx(
getTexCoord(0)->getTexGenMtx(),
getTexCoord(1)->getTexGenMtx(),
@@ -371,6 +371,30 @@ s32 J3DMaterial::newSingleSharedDisplayList(u32 dlSize) {
return kJ3DError_Success;
}
#if TARGET_PC
bool J3DMaterial::needsInterpCallBack() const {
for (int i = 0, n = getTexGenNum(); i < n; i++) {
J3DTexMtx* pTexMtx = mTexGenBlock->getTexMtx(i);
if (pTexMtx != NULL) {
u32 texMtxMode = pTexMtx->getTexMtxInfo().mInfo & 0x3f;
// uses j3dSys.getViewMtx()
switch (texMtxMode) {
case J3DTexMtxMode_EnvmapBasic:
case J3DTexMtxMode_EnvmapOld:
case J3DTexMtxMode_Envmap:
case J3DTexMtxMode_ProjmapBasic:
case J3DTexMtxMode_Projmap:
case J3DTexMtxMode_ViewProjmap:
case J3DTexMtxMode_ViewProjmapBasic:
return true;
}
}
}
return false;
}
#endif
void J3DPatchedMaterial::initialize() {
J3DMaterial::initialize();
}
+2 -2
View File
@@ -37,9 +37,9 @@ void loadTexCoordGens(u32 texGenNum, J3DTexCoord* texCoords) {
var_r28 = 61;
J3DGDWriteXFCmdHdr(GX_XF_REG_DUALTEX0, texGenNum);
if (j3dSys.checkFlag(0x40000000)) {
if (j3dSys.checkFlag(J3DSysFlag_PostTexMtx)) {
for (int i = 0; i < texGenNum; i++) {
if (texCoords[i].getTexGenMtx() != 60) {
if (texCoords[i].getTexGenMtx() != GX_IDENTITY) {
var_r28 = i * 3;
} else {
var_r28 = 61;
+9 -1
View File
@@ -120,11 +120,19 @@ void JAISeqMgr::mixOut() {
}
JAISeq* JAISeqMgr::beginStartSeq_() {
JAISeq* seq = JKR_NEW JAISeq(this, field_0x10);
#ifdef TARGET_PC
if (JAISeq::getFreeMemCount() == 0) {
JUT_WARN(273, "%s", "JASPoolAllocObject::<JAISeq>::operator new failed .\n");
return NULL;
}
return JKR_NEW JAISeq(this, field_0x10);
#else
JAISeq* seq = new JAISeq(this, field_0x10);
if (seq == NULL) {
JUT_WARN(273, "%s", "JASPoolAllocObject::<JAISeq>::operator new failed .\n");
}
return seq;
#endif
}
bool JAISeqMgr::endStartSeq_(JAISeq* seq, JAISoundHandle* handle) {
+8
View File
@@ -55,7 +55,11 @@ void JASDriver::initAI(void (*param_0)(void)) {
for (int i = 0; i < 3; i++) {
sDmaDacBuffer[i] = JKR_NEW_ARRAY_ARGS(s16, dacSize, JASDram, 0x20);
JUT_ASSERT(102, sDmaDacBuffer[i])
#if TARGET_ANDROID
JASCalc::_bzero(sDmaDacBuffer[i], size);
#else
JASCalc::bzero(sDmaDacBuffer[i], size);
#endif
DCStoreRange(sDmaDacBuffer[i], size);
}
sDspDacBuffer = JKR_NEW_ARRAY_ARGS(s16*, data_804507A8, JASDram, 0);
@@ -63,7 +67,11 @@ void JASDriver::initAI(void (*param_0)(void)) {
for (int i = 0; i < data_804507A8; i++) {
sDspDacBuffer[i] = JKR_NEW_ARRAY_ARGS(s16, getDacSize(), JASDram, 0x20);
JUT_ASSERT(119, sDspDacBuffer[i]);
#if TARGET_ANDROID
JASCalc::_bzero(sDspDacBuffer[i], size);
#else
JASCalc::bzero(sDspDacBuffer[i], size);
#endif
DCStoreRange(sDspDacBuffer[i], size);
}
sDspDacWriteBuffer = data_804507A8 - 1;
+12
View File
@@ -69,7 +69,11 @@ JASBasicBank* JASBNKParser::Ver1::createBasicBank(void const* stream, JKRHeap* h
JUT_ASSERT(145, list_chunk);
u8* envt = JKR_NEW_ARRAY_ARGS(u8, envt_chunk->mSize, heap, 2);
#if TARGET_ANDROID
JASCalc::_bcopy(envt_chunk->mData, envt, envt_chunk->mSize);
#else
JASCalc::bcopy(envt_chunk->mData, envt, envt_chunk->mSize);
#endif
BE(u32)* ptr = &osc_chunk->mCount;
u32 count = *ptr++;
@@ -215,7 +219,11 @@ JASBasicBank* JASBNKParser::Ver0::createBasicBank(void const* stream, JKRHeap* h
int size = endPtr - points;
JASOscillator::Point* table = JKR_NEW_ARRAY_ARGS(JASOscillator::Point, size, heap, 0);
JUT_ASSERT(396, table != NULL);
#if TARGET_ANDROID
JASCalc::_bcopy(points, table, size * sizeof(JASOscillator::Point));
#else
JASCalc::bcopy(points, table, size * sizeof(JASOscillator::Point));
#endif
osc->mTable = table;
} else {
osc->mTable = NULL;
@@ -227,7 +235,11 @@ JASBasicBank* JASBNKParser::Ver0::createBasicBank(void const* stream, JKRHeap* h
int size = endPtr - points;
JASOscillator::Point* table = JKR_NEW_ARRAY_ARGS(JASOscillator::Point, size, heap, 0);
JUT_ASSERT(409, table != NULL);
#if TARGET_ANDROID
JASCalc::_bcopy(points, table, size * sizeof(JASOscillator::Point));
#else
JASCalc::bcopy(points, table, size * sizeof(JASOscillator::Point));
#endif
osc->rel_table = table;
} else {
osc->rel_table = NULL;
@@ -13,7 +13,11 @@ void JASBasicBank::newInstTable(u8 num, JKRHeap* heap) {
JUT_ASSERT(31, num <= JASBank::PRG_OSC);
mInstNumMax = num;
mInstTable = JKR_NEW_ARRAY_ARGS(JASInst*, mInstNumMax, heap, 0);
#if TARGET_ANDROID
JASCalc::_bzero(mInstTable, mInstNumMax * 4);
#else
JASCalc::bzero(mInstTable, mInstNumMax * 4);
#endif
}
}
@@ -13,7 +13,11 @@ JASBasicInst::JASBasicInst() {
mPitch = 1.0;
mKeymapCount = 0;
mKeymap = NULL;
#if TARGET_ANDROID
JASCalc::_bzero(field_0xc, sizeof(field_0xc));
#else
JASCalc::bzero(field_0xc, sizeof(field_0xc));
#endif
}
JASBasicInst::~JASBasicInst() {
+8
View File
@@ -33,7 +33,11 @@ void JASCalc::bcopyfast(const void* src, void* dest, u32 size) {
}
}
#if TARGET_ANDROID
void JASCalc::_bcopy(const void* src, void* dest, u32 size) {
#else
void JASCalc::bcopy(const void* src, void* dest, u32 size) {
#endif
u32* usrc;
u32* udest;
@@ -90,7 +94,11 @@ void JASCalc::bzerofast(void* dest, u32 size) {
}
}
#if TARGET_ANDROID
void JASCalc::_bzero(void* dest, u32 size) {
#else
void JASCalc::bzero(void* dest, u32 size) {
#endif
u32* udest;
u8* bdest = (u8*)dest;
if ((size & 0x1f) == 0 && (reinterpret_cast<uintptr_t>(dest) & 0x1f) == 0) {
@@ -434,8 +434,14 @@ void JASDsp::initBuffer() {
JUT_ASSERT(354, CH_BUF);
FX_BUF = JKR_NEW_ARRAY_ARGS(FxBuf, 4, JASDram, 0x20);
JUT_ASSERT(356, FX_BUF);
#if TARGET_ANDROID
JASCalc::_bzero(CH_BUF, sizeof(TChannel) * DSP_CHANNELS);
JASCalc::_bzero(FX_BUF, sizeof(FxBuf) * 4);
#else
JASCalc::bzero(CH_BUF, sizeof(TChannel) * DSP_CHANNELS);
JASCalc::bzero(FX_BUF, sizeof(FxBuf) * 4);
#endif
for (u8 i = 0; i < 4; i++) {
setFXLine(i, NULL, NULL);
}
@@ -459,7 +465,11 @@ int JASDsp::setFXLine(u8 param_0, s16* buffer, JASDsp::FxlineConfig_* param_2) {
if (buffer != NULL && param_2 != NULL) {
u32 bufsize = param_2->field_0xc * 0xa0;
puVar3->field_0x4 = buffer;
#if TARGET_ANDROID
JASCalc::_bzero(buffer, bufsize);
#else
JASCalc::bzero(buffer, bufsize);
#endif
JUT_ASSERT(420, ((u32)((uintptr_t)buffer) & 0x1f) == 0);
JUT_ASSERT(421, (bufsize & 0x1f) == 0);
DCFlushRange(buffer, bufsize);
+4
View File
@@ -18,7 +18,11 @@ void JASDrumSet::newPercArray(u8 num, JKRHeap* heap) {
JUT_ASSERT(39, num <= 128);
mPercNumMax = num;
mPercArray = JKR_NEW_ARRAY_ARGS(TPerc*, mPercNumMax, heap, 0);
#if TARGET_ANDROID
JASCalc::_bzero(mPercArray, mPercNumMax * sizeof(TPerc*));
#else
JASCalc::bzero(mPercArray, mPercNumMax * sizeof(TPerc*));
#endif
}
}
+4
View File
@@ -53,7 +53,11 @@ int JASReportCopyBuffer(char *param_1,int lines) {
r30 = sLineMax - 1;
}
src = &sBuffer[r30 * 64];
#if TARGET_ANDROID
JASCalc::_bcopy(src, dest, 64);
#else
JASCalc::bcopy(src, dest, 64);
#endif
r30--;
dest += 64;
}
@@ -33,7 +33,11 @@ void* JASTaskThread::allocCallStack(JASThreadCallback callback, const void* msg,
}
callStack->msgType_ = 1;
#if TARGET_ANDROID
JASCalc::_bcopy(msg, callStack->msg.buffer, msgSize);
#else
JASCalc::bcopy(msg, callStack->msg.buffer, msgSize);
#endif
callStack->callback_ = callback;
return callStack;
}
+4 -18
View File
@@ -205,14 +205,6 @@ void JFWDisplay::preGX() {
}
}
#ifdef TARGET_PC
static s32 s_faderSimSteps = -1;
void JFWDisplay::setFaderSimSteps(u32 steps) {
s_faderSimSteps = static_cast<s32>(steps);
}
#endif
void JFWDisplay::endGX() {
s32 bufferNum = JUTXfb::getManager()->getBufferNum();
u16 width = JUTVideo::getManager()->getFbWidth();
@@ -224,17 +216,10 @@ void JFWDisplay::endGX() {
if (mFader != NULL) {
ortho.setPort();
#ifdef TARGET_PC
u32 advance_count = 1;
if (dusk::getSettings().game.enableFrameInterpolation && s_faderSimSteps >= 0) {
advance_count = static_cast<u32>(s_faderSimSteps);
s_faderSimSteps = -1;
} else {
s_faderSimSteps = -1;
}
for (u32 i = 0; i < advance_count; i++) {
if (dusk::frame_interp::get_ui_tick_pending()) {
mFader->advance();
}
if (mFader->getStatus() != 1) {
if (mFader->getStatus() != JUTFader::Wait) {
mFader->draw();
}
#else
@@ -393,7 +378,8 @@ static void waitPrecise(Limiter& limiter, Uint64 targetNs) {
static void waitForTick(u32 p1, u16 p2) {
#if TARGET_PC
if (dusk::getSettings().game.enableFrameInterpolation) {
if (dusk::getSettings().game.enableFrameInterpolation && !dusk::getTransientSettings().skipFrameRateLimit) {
dusk::frameUsagePct = 0.f;
return;
}
if (dusk::getTransientSettings().skipFrameRateLimit) {
@@ -2,7 +2,11 @@
#include "JSystem/JStudio/JStudio/jstudio-object.h"
#if TARGET_PC
#include "dusk/audio.h"
#include "dusk/frame_interpolation.h"
#include "dusk/settings.h"
#endif
namespace JStudio {
namespace {
@@ -650,10 +654,25 @@ value_or_fun:
return;
value:
#if TARGET_PC
if (dusk::getSettings().game.enableFrameInterpolation && u <= 5 &&
(operation == data::UNK_0x2 || operation == data::UNK_0x3 || operation == data::UNK_0x12))
{
dusk::frame_interp::request_presentation_sync();
}
#endif
adaptor->adaptor_setVariableValue(control, u, operation, param_2, param_3);
return;
value_n:
#if TARGET_PC
if (dusk::getSettings().game.enableFrameInterpolation &&
(pN == TAdaptor_camera::sauVariableValue_3_POSITION_XYZ || pN == TAdaptor_camera::sauVariableValue_3_TARGET_POSITION_XYZ) &&
(operation == data::UNK_0x2 || operation == data::UNK_0x3 || operation == data::UNK_0x12))
{
dusk::frame_interp::request_presentation_sync();
}
#endif
adaptor->adaptor_setVariableValue_n(control, pN, u, operation, param_2, param_3);
return;
@@ -314,10 +314,17 @@ void JStudio_JStage::TAdaptor_actor::getJSG_SRT_(JStudio::TControl const* pContr
}
void JStudio_JStage::TAdaptor_actor::TVVOutput_ANIMATION_FRAME_::operator()(
f32 param_1, JStudio::TAdaptor* adaptor) const {
f32 param_1, JStudio::TAdaptor* adaptor) const {
#if TARGET_PC
TAdaptor_actor* actor_adaptor = static_cast<TAdaptor_actor*>(adaptor);
JStage::TActor* actor = actor_adaptor->get_pJSG_();
// field_0x8 is always hardcoded to either 305 or 309
u32 idx = (field_0x8 == 305) ? actor_adaptor->field_0x130 : actor_adaptor->field_0x134;
#else
JStage::TActor* actor = static_cast<TAdaptor_actor*>(adaptor)->get_pJSG_();
// not sure what this bit is
u32 idx = *(u32*)(((uintptr_t)adaptor - 1) + field_0x8);
#endif
u8 idx_lowBytes = idx;
u8 idx_highBytes = idx >> 8;
+44 -44
View File
@@ -10,51 +10,51 @@
JUTFader::JUTFader(int x, int y, int width, int height, JUtility::TColor pColor)
: mColor(pColor), mBox(x, y, x + width, y + height) {
mStatus = 0;
field_0x8 = 0;
field_0xa = 0;
field_0x24 = 0;
mEStatus = UNKSTATUS_M1;
mStatus = None;
mDuration = 0;
mTimer = 0;
mNextStatus = 0;
mStatusTimer = -1;
}
void JUTFader::advance() {
if (0 <= mEStatus && mEStatus-- == 0) {
mStatus = field_0x24;
if (0 <= mStatusTimer && mStatusTimer-- == 0) {
mStatus = mNextStatus;
}
if (mStatus == 1) {
if (mStatus == Wait) {
return;
}
switch (mStatus) {
case 0:
case None:
mColor.a = 0xFF;
break;
case 2:
case FadeIn:
#if AVOID_UB
if (field_0x8 == 0) {
mStatus = 1;
if (mDuration == 0) {
mStatus = Wait;
break;
}
#endif
mColor.a = 0xFF - ((++field_0xa * 0xFF) / field_0x8);
mColor.a = 0xFF - ((++mTimer * 0xFF) / mDuration);
if (field_0xa >= field_0x8) {
mStatus = 1;
if (mTimer >= mDuration) {
mStatus = Wait;
}
break;
case 3:
case FadeOut:
#if AVOID_UB
if (field_0x8 == 0) {
mStatus = 0;
if (mDuration == 0) {
mStatus = None;
break;
}
#endif
mColor.a = ((++field_0xa * 0xFF) / field_0x8);
mColor.a = ((++mTimer * 0xFF) / mDuration);
if (field_0xa >= field_0x8) {
mStatus = 0;
if (mTimer >= mDuration) {
mStatus = None;
}
break;
@@ -77,53 +77,53 @@ void JUTFader::draw() {
}
}
bool JUTFader::startFadeIn(int param_0) {
bool JUTFader::startFadeIn(int duration) {
bool statusCheck = mStatus == 0;
if (statusCheck) {
mStatus = 2;
field_0xa = 0;
field_0x8 = param_0;
mStatus = FadeIn;
mTimer = 0;
mDuration = duration;
}
return statusCheck;
}
bool JUTFader::startFadeOut(int param_0) {
bool JUTFader::startFadeOut(int duration) {
bool statusCheck = mStatus == 1;
if (statusCheck) {
mStatus = 3;
field_0xa = 0;
field_0x8 = param_0;
mStatus = FadeOut;
mTimer = 0;
mDuration = duration;
}
return statusCheck;
}
void JUTFader::setStatus(JUTFader::EStatus i_status, int param_1) {
void JUTFader::setStatus(JUTFader::EStatus i_status, int timer) {
switch (i_status) {
case 0:
if (param_1 != 0) {
field_0x24 = 0;
mEStatus = param_1;
case None:
if (timer != 0) {
mNextStatus = None;
mStatusTimer = timer;
break;
}
mStatus = 0;
field_0x24 = 0;
mEStatus = 0;
mStatus = None;
mNextStatus = None;
mStatusTimer = 0;
break;
case 1:
if (param_1 != 0) {
field_0x24 = 1;
mEStatus = param_1;
case Wait:
if (timer != 0) {
mNextStatus = Wait;
mStatusTimer = timer;
break;
}
mStatus = 1;
field_0x24 = 1;
mEStatus = 0;
mStatus = Wait;
mNextStatus = Wait;
mStatusTimer = 0;
break;
}
}
+69 -37
View File
@@ -6,26 +6,13 @@
#include "JSystem/JUtility/JUTAssert.h"
#include "JSystem/JUtility/JUTConsole.h"
#include <gx.h>
#include "absl/container/node_hash_map.h"
#if TARGET_PC
struct GlyphTextures {
absl::node_hash_map<int, TGXTexObj> textures;
};
#endif
JUTResFont::JUTResFont() {
#if TARGET_PC
mGlyphTextures = new GlyphTextures();
#endif
initialize_state();
JUTFont::initialize_state();
}
JUTResFont::JUTResFont(const ResFONT* pFont, JKRHeap* pHeap) {
#if TARGET_PC
mGlyphTextures = new GlyphTextures();
#endif
initialize_state();
JUTFont::initialize_state();
initiate(pFont, pHeap);
@@ -33,10 +20,7 @@ JUTResFont::JUTResFont(const ResFONT* pFont, JKRHeap* pHeap) {
JUTResFont::~JUTResFont() {
#if TARGET_PC
for (auto& pair : mGlyphTextures->textures) {
pair.second.reset();
}
delete mGlyphTextures;
mJoinedTextureObject.reset();
#endif
if (mValid) {
@@ -70,6 +54,33 @@ bool JUTResFont::initiate(const ResFONT* pFont, JKRHeap* pHeap) {
return true;
}
#if TARGET_PC
void JUTResFont::initJoinedTexture() {
if (mGly1BlockNum != 1) {
CRASH("mGly1BlockNum must be 1!");
}
const auto& block = *mpGlyphBlocks[0];
if (block.textureWidth % 8 != 0 || block.textureHeight % 8 != 0) {
// Idk how the GameCube's tiling texture format works so this is a safety check.
CRASH("Texture size not divisible!");
}
int pageCount = 0;
u32 pageNumCells = block.numRows * block.numColumns;
for (u32 code = block.startCode; code < block.endCode; code += pageNumCells) {
pageCount += 1;
}
mJoinedTextureHeight = block.textureHeight * pageCount;
GXInitTexObj(&mJoinedTextureObject, block.data, block.textureWidth,
mJoinedTextureHeight, static_cast<GXTexFmt>(block.textureFormat.host()),
GX_CLAMP, GX_CLAMP, false);
GXInitTexObjLOD(&mJoinedTextureObject, GX_LINEAR, GX_LINEAR, 0.0f, 0.0f, 0.0f, 0U, 0U, GX_ANISO_1);
}
#endif
bool JUTResFont::protected_initiate(const ResFONT* pFont, JKRHeap* pHeap) {
void** p;
delete_and_initialize();
@@ -100,8 +111,10 @@ bool JUTResFont::protected_initiate(const ResFONT* pFont, JKRHeap* pHeap) {
mpMapBlocks = JKR_NEW_ARRAY_ARGS(ResFONT::MAP1*, mMap1BlockNum, p);
}
setBlock();
return true;
IF_DUSK(initJoinedTexture());
return true;
}
void JUTResFont::countBlock() {
@@ -231,14 +244,14 @@ void JUTResFont::setGX(JUtility::TColor col1, JUtility::TColor col2) {
}
f32 JUTResFont::drawChar_scale(f32 pos_x, f32 pos_y, f32 scale_x, f32 scale_y, int str_int,
bool flag) {
bool flag FONT_DRAW_CTX) {
f32 x1;
f32 x2;
f32 y1;
JUT_ASSERT(378, mValid);
JUTFont::TWidth width;
loadFont(str_int, GX_TEXMAP0, &width);
loadFont(str_int, GX_TEXMAP0, &width FONT_DRAW_CTX_ARG);
if ((mFixed) || (!flag)) {
x1 = pos_x;
@@ -258,15 +271,26 @@ f32 JUTResFont::drawChar_scale(f32 pos_x, f32 pos_y, f32 scale_x, f32 scale_y, i
f32 y2 = getDescent() * (scale_y / getHeight()) + pos_y;
u16 texW = mpGlyphBlocks[field_0x66]->textureWidth;
#if TARGET_PC
u16 texH = mJoinedTextureHeight;
#else
u16 texH = mpGlyphBlocks[field_0x66]->textureHeight;
#endif
u16 cellW = mpGlyphBlocks[field_0x66]->cellWidth;
u16 cellH = mpGlyphBlocks[field_0x66]->cellHeight;
s32 u1 = (mWidth * 0x8000) / texW;
s32 v1 = (mHeight * 0x8000) / texH;
s32 u2 = ((mWidth + cellW) * 0x8000) / texW;
s32 v2 = ((mHeight + cellH) * 0x8000) / texH;
#if TARGET_PC
if (!context) {
pushDrawState();
}
#else
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0);
#endif
GXBegin(GX_QUADS, GX_VTXFMT0, 4);
// Bottom Left
@@ -290,18 +314,33 @@ f32 JUTResFont::drawChar_scale(f32 pos_x, f32 pos_y, f32 scale_x, f32 scale_y, i
GXTexCoord2u16(u1, v2);
GXEnd();
#if TARGET_PC
if (!context) {
popDrawState();
}
#else
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_S16, 0);
#endif
return retval;
}
void JUTResFont::loadFont(int code, GXTexMapID texMapID, JUTFont::TWidth* pDstWidth) {
#if TARGET_PC
void JUTResFont::pushDrawState() {
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0);
}
void JUTResFont::popDrawState() {
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_S16, 0);
}
#endif
void JUTResFont::loadFont(int code, GXTexMapID texMapID, JUTFont::TWidth* pDstWidth FONT_DRAW_CTX) {
if (pDstWidth != 0) {
getWidthEntry(code, pDstWidth);
}
int fontCode = getFontCode(code);
loadImage(fontCode, texMapID);
loadImage(fontCode, texMapID FONT_DRAW_CTX_ARG);
}
void JUTResFont::getWidthEntry(int code, JUTFont::TWidth* i_width) const {
@@ -403,7 +442,7 @@ int JUTResFont::getFontCode(int chr) const {
return ret;
}
void JUTResFont::loadImage(int code, GXTexMapID id){
void JUTResFont::loadImage(int code, GXTexMapID id FONT_DRAW_CTX){
int i = 0;
for (; i < mGly1BlockNum; i++)
{
@@ -435,22 +474,15 @@ void JUTResFont::loadImage(int code, GXTexMapID id){
mHeight = cellRow * cellH;
#if TARGET_PC
const auto found = mGlyphTextures->textures.find(pageIdx);
GXTexObj* texObj;
if (found == mGlyphTextures->textures.end()) {
texObj = &mGlyphTextures->textures[pageIdx];
void* pImg = &mpGlyphBlocks[i]->data[pageIdx * mpGlyphBlocks[i]->textureSize];
GXInitTexObj(texObj, pImg, mpGlyphBlocks[i]->textureWidth,
mpGlyphBlocks[i]->textureHeight, (GXTexFmt)(u16)mpGlyphBlocks[i]->textureFormat,
GX_CLAMP, GX_CLAMP, 0);
mHeight += texH * pageIdx;
GXInitTexObjLOD(texObj, GX_LINEAR, GX_LINEAR, 0.0f, 0.0f, 0.0f, 0U, 0U, GX_ANISO_1);
} else {
texObj = &found->second;
if (!context || !context->isTextureLoaded) {
GXLoadTexObj(&mJoinedTextureObject, id);
if (context) {
context->isTextureLoaded = true;
}
}
GXLoadTexObj(texObj, id);
// Gets used by some other code.
mTexPageIdx = pageIdx;
field_0x66 = i;
+3 -2
View File
@@ -205,7 +205,8 @@ void JUTVideo::setRenderMode(GXRenderModeObj const* pObj) {
void JUTVideo::waitRetraceIfNeed() {}
#if TARGET_PC
void JUTVideo::setWindowSize(AuroraWindowSize const& size) {
m_WindowSize = size;
void JUTVideo::setRenderSize(u32 width, u32 height) {
mRenderWidth = width;
mRenderHeight = height;
}
#endif
+5
View File
@@ -0,0 +1,5 @@
.gradle/
build/
app/build/
local.properties
app/src/main/jniLibs/*/*.so
+77
View File
@@ -0,0 +1,77 @@
# Android Shell
This directory contains a minimal SDLActivity-based Android app wrapper for Dusk.
## Prerequisites
- Android SDK installed (`ANDROID_HOME`)
- Android NDK version used by CMake presets (`ANDROID_NDK_VERSION`)
- JDK 17+
Example:
```bash
export ANDROID_HOME="$HOME/Android/Sdk"
export ANDROID_NDK_VERSION="29.0.14206865"
export JAVA_HOME="/usr/lib/jvm/java-17-openjdk"
```
## Build Native Libraries
```bash
cmake --preset android-arm64
cmake --build --preset android-arm64
cmake --preset android-x86_64
cmake --build --preset android-x86_64
```
These builds produce:
- `build/android-arm64/Binaries/libmain.so`
- `build/android-x86_64/Binaries/libmain.so`
## Stage Libraries Into APK Project
```bash
./android/scripts/stage-jni-libs.sh
```
This copies:
- `libmain.so` -> `android/app/src/main/jniLibs/arm64-v8a/`
- `libmain.so` -> `android/app/src/main/jniLibs/x86_64/`
## Refresh SDL Java Shim (Optional)
If you update SDL and want to refresh the embedded Java shim files:
```bash
./android/scripts/sync-sdl-java.sh
```
## Build APK
```bash
cd android
./gradlew :app:assembleDebug
```
Output APK:
- `android/app/build/outputs/apk/debug/app-debug.apk`
## Launch With Runtime Args (adb)
You can pass command-line args through the activity intent:
```bash
adb shell am start -n com.twilitrealm.dusk/.DuskActivity \
--es dusk_args "'/sdcard/Download/The Legend of Zelda: Twilight Princess (USA).iso'"
```
Supported extras:
- `dusk_args`: single shell-like argument string
- `dusk_argv`: string-array argv
- `dusk_disc`: compatibility shortcut (single ISO path)
+50
View File
@@ -0,0 +1,50 @@
plugins {
id 'com.android.application'
}
android {
namespace 'com.twilitrealm.dusk'
compileSdk 36
defaultConfig {
applicationId 'com.twilitrealm.dusk'
minSdk 26
targetSdk 36
versionCode 1
versionName '0.1.0'
}
buildTypes {
debug {
minifyEnabled false
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main {
jniLibs.srcDirs = ['src/main/jniLibs']
assets.srcDirs = ['../../assets']
}
}
splits {
abi {
enable true
reset()
include 'arm64-v8a', 'x86_64'
universalApk false
}
}
lint {
abortOnError false
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
+2
View File
@@ -0,0 +1,2 @@
# Keep SDL activity and related JNI bridge methods.
-keep class org.libsdl.app.** { *; }
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:installLocation="auto">
<uses-feature android:glEsVersion="0x00020000" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
<uses-feature android:name="android.hardware.type.pc" android:required="false" />
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:allowBackup="true"
android:hardwareAccelerated="true"
android:appCategory="game"
android:icon="@mipmap/icon"
android:label="@string/app_name"
android:theme="@android:style/Theme.NoTitleBar"
android:enableOnBackInvokedCallback="false">
<meta-data android:name="android.game_mode_config"
android:resource="@xml/game_mode_config" />
<activity
android:name="com.twilitrealm.dusk.DuskActivity"
android:alwaysRetainTaskState="true"
android:configChanges="layoutDirection|locale|grammaticalGender|fontScale|fontWeightAdjustment|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:preferMinimalPostProcessing="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,111 @@
package com.twilitrealm.dusk;
import android.app.ActionBar;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.WindowInsets;
import org.libsdl.app.SDLActivity;
import java.util.ArrayList;
import java.util.List;
public class DuskActivity extends SDLActivity {
private static String[] splitArgs(String raw) {
List<String> out = new ArrayList<>();
StringBuilder current = new StringBuilder();
boolean inSingle = false;
boolean inDouble = false;
boolean escaped = false;
for (int i = 0; i < raw.length(); ++i) {
char c = raw.charAt(i);
if (escaped) {
current.append(c);
escaped = false;
continue;
}
if (c == '\\' && !inSingle) {
escaped = true;
continue;
}
if (c == '"' && !inSingle) {
inDouble = !inDouble;
continue;
}
if (c == '\'' && !inDouble) {
inSingle = !inSingle;
continue;
}
if (!inSingle && !inDouble && Character.isWhitespace(c)) {
if (current.length() > 0) {
out.add(current.toString());
current.setLength(0);
}
continue;
}
current.append(c);
}
if (escaped) {
current.append('\\');
}
if (current.length() > 0) {
out.add(current.toString());
}
return out.toArray(new String[0]);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
getWindow().getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars());
}else {
View decorView = getWindow().getDecorView();
// Hide the status bar.
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);
// Remember that you should never show the action bar if the
// status bar is hidden, so hide that too if necessary.
ActionBar actionBar = getActionBar();
actionBar.hide();
}
}
@Override
protected String[] getLibraries() {
// SDL3 is statically linked into libmain.so in this build.
return new String[] {
"main"
};
}
@Override
protected String[] getArguments() {
Intent intent = getIntent();
if (intent != null) {
String[] argv = intent.getStringArrayExtra("dusk_argv");
if (argv != null && argv.length > 0) {
return argv;
}
String rawArgs = intent.getStringExtra("dusk_args");
if (rawArgs != null) {
String trimmed = rawArgs.trim();
if (!trimmed.isEmpty()) {
return splitArgs(trimmed);
}
}
String discPath = intent.getStringExtra("dusk_disc");
if (discPath != null && !discPath.isEmpty()) {
return new String[] { discPath };
}
}
return new String[0];
}
}
@@ -0,0 +1,21 @@
package org.libsdl.app;
import android.hardware.usb.UsbDevice;
interface HIDDevice
{
public int getId();
public int getVendorId();
public int getProductId();
public String getSerialNumber();
public int getVersion();
public String getManufacturerName();
public String getProductName();
public UsbDevice getDevice();
public boolean open();
public int writeReport(byte[] report, boolean feature);
public boolean readReport(byte[] report, boolean feature);
public void setFrozen(boolean frozen);
public void close();
public void shutdown();
}
@@ -0,0 +1,655 @@
package org.libsdl.app;
import android.content.Context;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothGattService;
import android.hardware.usb.UsbDevice;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.os.*;
//import com.android.internal.util.HexDump;
import java.lang.Runnable;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.UUID;
class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice {
private static final String TAG = "hidapi";
private HIDDeviceManager mManager;
private BluetoothDevice mDevice;
private int mDeviceId;
private BluetoothGatt mGatt;
private boolean mIsRegistered = false;
private boolean mIsConnected = false;
private boolean mIsChromebook = false;
private boolean mIsReconnecting = false;
private boolean mFrozen = false;
private LinkedList<GattOperation> mOperations;
GattOperation mCurrentOperation = null;
private Handler mHandler;
private static final int TRANSPORT_AUTO = 0;
private static final int TRANSPORT_BREDR = 1;
private static final int TRANSPORT_LE = 2;
private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000;
static final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3");
static final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3");
static final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3");
static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 };
static class GattOperation {
private enum Operation {
CHR_READ,
CHR_WRITE,
ENABLE_NOTIFICATION
}
Operation mOp;
UUID mUuid;
byte[] mValue;
BluetoothGatt mGatt;
boolean mResult = true;
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
mGatt = gatt;
mOp = operation;
mUuid = uuid;
}
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
mGatt = gatt;
mOp = operation;
mUuid = uuid;
mValue = value;
}
public void run() {
// This is executed in main thread
BluetoothGattCharacteristic chr;
switch (mOp) {
case CHR_READ:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Reading characteristic " + chr.getUuid());
if (!mGatt.readCharacteristic(chr)) {
Log.e(TAG, "Unable to read characteristic " + mUuid.toString());
mResult = false;
break;
}
mResult = true;
break;
case CHR_WRITE:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
chr.setValue(mValue);
if (!mGatt.writeCharacteristic(chr)) {
Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
mResult = false;
break;
}
mResult = true;
break;
case ENABLE_NOTIFICATION:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Writing descriptor of " + chr.getUuid());
if (chr != null) {
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if (cccd != null) {
int properties = chr.getProperties();
byte[] value;
if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
} else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) {
value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
} else {
Log.e(TAG, "Unable to start notifications on input characteristic");
mResult = false;
return;
}
mGatt.setCharacteristicNotification(chr, true);
cccd.setValue(value);
if (!mGatt.writeDescriptor(cccd)) {
Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
mResult = false;
return;
}
mResult = true;
}
}
}
}
public boolean finish() {
return mResult;
}
private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
BluetoothGattService valveService = mGatt.getService(steamControllerService);
if (valveService == null)
return null;
return valveService.getCharacteristic(uuid);
}
static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) {
return new GattOperation(gatt, Operation.CHR_READ, uuid);
}
static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) {
return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value);
}
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid);
}
}
HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) {
mManager = manager;
mDevice = device;
mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier());
mIsRegistered = false;
mIsChromebook = SDLActivity.isChromebook();
mOperations = new LinkedList<GattOperation>();
mHandler = new Handler(Looper.getMainLooper());
mGatt = connectGatt();
// final HIDDeviceBLESteamController finalThis = this;
// mHandler.postDelayed(new Runnable() {
// @Override
// void run() {
// finalThis.checkConnectionForChromebookIssue();
// }
// }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
}
String getIdentifier() {
return String.format("SteamController.%s", mDevice.getAddress());
}
BluetoothGatt getGatt() {
return mGatt;
}
// Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
// of TRANSPORT_LE. Let's force ourselves to connect low energy.
private BluetoothGatt connectGatt(boolean managed) {
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
try {
return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
} catch (Exception e) {
return mDevice.connectGatt(mManager.getContext(), managed, this);
}
} else {
return mDevice.connectGatt(mManager.getContext(), managed, this);
}
}
private BluetoothGatt connectGatt() {
return connectGatt(false);
}
protected int getConnectionState() {
Context context = mManager.getContext();
if (context == null) {
// We are lacking any context to get our Bluetooth information. We'll just assume disconnected.
return BluetoothProfile.STATE_DISCONNECTED;
}
BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
if (btManager == null) {
// This device doesn't support Bluetooth. We should never be here, because how did
// we instantiate a device to start with?
return BluetoothProfile.STATE_DISCONNECTED;
}
return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
}
void reconnect() {
if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
mGatt.disconnect();
mGatt = connectGatt();
}
}
protected void checkConnectionForChromebookIssue() {
if (!mIsChromebook) {
// We only do this on Chromebooks, because otherwise it's really annoying to just attempt
// over and over.
return;
}
int connectionState = getConnectionState();
switch (connectionState) {
case BluetoothProfile.STATE_CONNECTED:
if (!mIsConnected) {
// We are in the Bad Chromebook Place. We can force a disconnect
// to try to recover.
Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
}
else if (!isRegistered()) {
if (mGatt.getServices().size() > 0) {
Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover.");
probeService(this);
}
else {
Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
}
}
else {
Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!");
return;
}
break;
case BluetoothProfile.STATE_DISCONNECTED:
Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
case BluetoothProfile.STATE_CONNECTING:
Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer.");
break;
}
final HIDDeviceBLESteamController finalThis = this;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
finalThis.checkConnectionForChromebookIssue();
}
}, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
}
private boolean isRegistered() {
return mIsRegistered;
}
private void setRegistered() {
mIsRegistered = true;
}
private boolean probeService(HIDDeviceBLESteamController controller) {
if (isRegistered()) {
return true;
}
if (!mIsConnected) {
return false;
}
Log.v(TAG, "probeService controller=" + controller);
for (BluetoothGattService service : mGatt.getServices()) {
if (service.getUuid().equals(steamControllerService)) {
Log.v(TAG, "Found Valve steam controller service " + service.getUuid());
for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
if (chr.getUuid().equals(inputCharacteristic)) {
Log.v(TAG, "Found input characteristic");
// Start notifications
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if (cccd != null) {
enableNotification(chr.getUuid());
}
}
}
return true;
}
}
if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) {
Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us.");
mIsConnected = false;
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
}
return false;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
private void finishCurrentGattOperation() {
GattOperation op = null;
synchronized (mOperations) {
if (mCurrentOperation != null) {
op = mCurrentOperation;
mCurrentOperation = null;
}
}
if (op != null) {
boolean result = op.finish(); // TODO: Maybe in main thread as well?
// Our operation failed, let's add it back to the beginning of our queue.
if (!result) {
mOperations.addFirst(op);
}
}
executeNextGattOperation();
}
private void executeNextGattOperation() {
synchronized (mOperations) {
if (mCurrentOperation != null)
return;
if (mOperations.isEmpty())
return;
mCurrentOperation = mOperations.removeFirst();
}
// Run in main thread
mHandler.post(new Runnable() {
@Override
public void run() {
synchronized (mOperations) {
if (mCurrentOperation == null) {
Log.e(TAG, "Current operation null in executor?");
return;
}
mCurrentOperation.run();
// now wait for the GATT callback and when it comes, finish this operation
}
}
});
}
private void queueGattOperation(GattOperation op) {
synchronized (mOperations) {
mOperations.add(op);
}
executeNextGattOperation();
}
private void enableNotification(UUID chrUuid) {
GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid);
queueGattOperation(op);
}
void writeCharacteristic(UUID uuid, byte[] value) {
GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value);
queueGattOperation(op);
}
void readCharacteristic(UUID uuid) {
GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid);
queueGattOperation(op);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
////////////// BluetoothGattCallback overridden methods
//////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onConnectionStateChange(BluetoothGatt g, int status, int newState) {
//Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
mIsReconnecting = false;
if (newState == 2) {
mIsConnected = true;
// Run directly, without GattOperation
if (!isRegistered()) {
mHandler.post(new Runnable() {
@Override
public void run() {
mGatt.discoverServices();
}
});
}
}
else if (newState == 0) {
mIsConnected = false;
}
// Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
//Log.v(TAG, "onServicesDiscovered status=" + status);
if (status == 0) {
if (gatt.getServices().size() == 0) {
Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack.");
mIsReconnecting = true;
mIsConnected = false;
gatt.disconnect();
mGatt = connectGatt(false);
}
else {
probeService(this);
}
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
//Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid());
if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) {
mManager.HIDDeviceReportResponse(getId(), characteristic.getValue());
}
finishCurrentGattOperation();
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
//Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
if (characteristic.getUuid().equals(reportCharacteristic)) {
// Only register controller with the native side once it has been fully configured
if (!isRegistered()) {
Log.v(TAG, "Registering Steam Controller with ID: " + getId());
mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true);
setRegistered();
}
}
finishCurrentGattOperation();
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
// Enable this for verbose logging of controller input reports
//Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue()));
if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) {
mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
}
}
@Override
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
//Log.v(TAG, "onDescriptorRead status=" + status);
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
BluetoothGattCharacteristic chr = descriptor.getCharacteristic();
//Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid());
if (chr.getUuid().equals(inputCharacteristic)) {
boolean hasWrittenInputDescriptor = true;
BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic);
if (reportChr != null) {
Log.v(TAG, "Writing report characteristic to enter valve mode");
reportChr.setValue(enterValveMode);
gatt.writeCharacteristic(reportChr);
}
}
finishCurrentGattOperation();
}
@Override
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
//Log.v(TAG, "onReliableWriteCompleted status=" + status);
}
@Override
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
//Log.v(TAG, "onReadRemoteRssi status=" + status);
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
//Log.v(TAG, "onMtuChanged status=" + status);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////// Public API
//////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public int getId() {
return mDeviceId;
}
@Override
public int getVendorId() {
// Valve Corporation
final int VALVE_USB_VID = 0x28DE;
return VALVE_USB_VID;
}
@Override
public int getProductId() {
// We don't have an easy way to query from the Bluetooth device, but we know what it is
final int D0G_BLE2_PID = 0x1106;
return D0G_BLE2_PID;
}
@Override
public String getSerialNumber() {
// This will be read later via feature report by Steam
return "12345";
}
@Override
public int getVersion() {
return 0;
}
@Override
public String getManufacturerName() {
return "Valve Corporation";
}
@Override
public String getProductName() {
return "Steam Controller";
}
@Override
public UsbDevice getDevice() {
return null;
}
@Override
public boolean open() {
return true;
}
@Override
public int writeReport(byte[] report, boolean feature) {
if (!isRegistered()) {
Log.e(TAG, "Attempted writeReport before Steam Controller is registered!");
if (mIsConnected) {
probeService(this);
}
return -1;
}
if (feature) {
// We need to skip the first byte, as that doesn't go over the air
byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
//Log.v(TAG, "writeFeatureReport " + HexDump.dumpHexString(actual_report));
writeCharacteristic(reportCharacteristic, actual_report);
return report.length;
} else {
//Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report));
writeCharacteristic(reportCharacteristic, report);
return report.length;
}
}
@Override
public boolean readReport(byte[] report, boolean feature) {
if (!isRegistered()) {
Log.e(TAG, "Attempted readReport before Steam Controller is registered!");
if (mIsConnected) {
probeService(this);
}
return false;
}
if (feature) {
readCharacteristic(reportCharacteristic);
return true;
} else {
// Not implemented
return false;
}
}
@Override
public void close() {
}
@Override
public void setFrozen(boolean frozen) {
mFrozen = frozen;
}
@Override
public void shutdown() {
close();
BluetoothGatt g = mGatt;
if (g != null) {
g.disconnect();
g.close();
mGatt = null;
}
mManager = null;
mIsRegistered = false;
mIsConnected = false;
mOperations.clear();
}
}
@@ -0,0 +1,690 @@
package org.libsdl.app;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.os.Build;
import android.util.Log;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.hardware.usb.*;
import android.os.Handler;
import android.os.Looper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
public class HIDDeviceManager {
private static final String TAG = "hidapi";
private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION";
private static HIDDeviceManager sManager;
private static int sManagerRefCount = 0;
static public HIDDeviceManager acquire(Context context) {
if (sManagerRefCount == 0) {
sManager = new HIDDeviceManager(context);
}
++sManagerRefCount;
return sManager;
}
static public void release(HIDDeviceManager manager) {
if (manager == sManager) {
--sManagerRefCount;
if (sManagerRefCount == 0) {
sManager.close();
sManager = null;
}
}
}
private Context mContext;
private HashMap<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>();
private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>();
private int mNextDeviceId = 0;
private SharedPreferences mSharedPreferences = null;
private boolean mIsChromebook = false;
private UsbManager mUsbManager;
private Handler mHandler;
private BluetoothManager mBluetoothManager;
private List<BluetoothDevice> mLastBluetoothDevices;
private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDeviceAttached(usbDevice);
} else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDeviceDetached(usbDevice);
} else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false));
}
}
};
private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// Bluetooth device was connected. If it was a Steam Controller, handle it
if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Log.d(TAG, "Bluetooth device connected: " + device);
if (isSteamController(device)) {
connectBluetoothDevice(device);
}
}
// Bluetooth device was disconnected, remove from controller manager (if any)
if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Log.d(TAG, "Bluetooth device disconnected: " + device);
disconnectBluetoothDevice(device);
}
}
};
private HIDDeviceManager(final Context context) {
mContext = context;
HIDDeviceRegisterCallback();
mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE);
mIsChromebook = SDLActivity.isChromebook();
// if (shouldClear) {
// SharedPreferences.Editor spedit = mSharedPreferences.edit();
// spedit.clear();
// spedit.apply();
// }
// else
{
mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0);
}
}
Context getContext() {
return mContext;
}
int getDeviceIDForIdentifier(String identifier) {
SharedPreferences.Editor spedit = mSharedPreferences.edit();
int result = mSharedPreferences.getInt(identifier, 0);
if (result == 0) {
result = mNextDeviceId++;
spedit.putInt("next_device_id", mNextDeviceId);
}
spedit.putInt(identifier, result);
spedit.apply();
return result;
}
private void initializeUSB() {
mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE);
if (mUsbManager == null) {
return;
}
/*
// Logging
for (UsbDevice device : mUsbManager.getDeviceList().values()) {
Log.i(TAG,"Path: " + device.getDeviceName());
Log.i(TAG,"Manufacturer: " + device.getManufacturerName());
Log.i(TAG,"Product: " + device.getProductName());
Log.i(TAG,"ID: " + device.getDeviceId());
Log.i(TAG,"Class: " + device.getDeviceClass());
Log.i(TAG,"Protocol: " + device.getDeviceProtocol());
Log.i(TAG,"Vendor ID " + device.getVendorId());
Log.i(TAG,"Product ID: " + device.getProductId());
Log.i(TAG,"Interface count: " + device.getInterfaceCount());
Log.i(TAG,"---------------------------------------");
// Get interface details
for (int index = 0; index < device.getInterfaceCount(); index++) {
UsbInterface mUsbInterface = device.getInterface(index);
Log.i(TAG," ***** *****");
Log.i(TAG," Interface index: " + index);
Log.i(TAG," Interface ID: " + mUsbInterface.getId());
Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass());
Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass());
Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol());
Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount());
// Get endpoint details
for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++)
{
UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi);
Log.i(TAG," ++++ ++++ ++++");
Log.i(TAG," Endpoint index: " + epi);
Log.i(TAG," Attributes: " + mEndpoint.getAttributes());
Log.i(TAG," Direction: " + mEndpoint.getDirection());
Log.i(TAG," Number: " + mEndpoint.getEndpointNumber());
Log.i(TAG," Interval: " + mEndpoint.getInterval());
Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize());
Log.i(TAG," Type: " + mEndpoint.getType());
}
}
}
Log.i(TAG," No more devices connected.");
*/
// Register for USB broadcasts and permission completions
IntentFilter filter = new IntentFilter();
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION);
if (Build.VERSION.SDK_INT >= 33) { /* Android 13.0 (TIRAMISU) */
mContext.registerReceiver(mUsbBroadcast, filter, Context.RECEIVER_EXPORTED);
} else {
mContext.registerReceiver(mUsbBroadcast, filter);
}
for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
handleUsbDeviceAttached(usbDevice);
}
}
UsbManager getUSBManager() {
return mUsbManager;
}
private void shutdownUSB() {
try {
mContext.unregisterReceiver(mUsbBroadcast);
} catch (Exception e) {
// We may not have registered, that's okay
}
}
private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) {
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
return true;
}
if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) {
return true;
}
return false;
}
private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) {
final int XB360_IFACE_SUBCLASS = 93;
final int XB360_IFACE_PROTOCOL = 1; // Wired
final int XB360W_IFACE_PROTOCOL = 129; // Wireless
final int[] SUPPORTED_VENDORS = {
0x0079, // GPD Win 2
0x044f, // Thrustmaster
0x045e, // Microsoft
0x046d, // Logitech
0x056e, // Elecom
0x06a3, // Saitek
0x0738, // Mad Catz
0x07ff, // Mad Catz
0x0e6f, // PDP
0x0f0d, // Hori
0x1038, // SteelSeries
0x11c9, // Nacon
0x12ab, // Unknown
0x1430, // RedOctane
0x146b, // BigBen
0x1532, // Razer Sabertooth
0x15e4, // Numark
0x162e, // Joytech
0x1689, // Razer Onza
0x1949, // Lab126, Inc.
0x1bad, // Harmonix
0x20d6, // PowerA
0x24c6, // PowerA
0x2c22, // Qanba
0x2dc8, // 8BitDo
0x9886, // ASTRO Gaming
};
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
(usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL ||
usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) {
int vendor_id = usbDevice.getVendorId();
for (int supportedVid : SUPPORTED_VENDORS) {
if (vendor_id == supportedVid) {
return true;
}
}
}
return false;
}
private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) {
final int XB1_IFACE_SUBCLASS = 71;
final int XB1_IFACE_PROTOCOL = 208;
final int[] SUPPORTED_VENDORS = {
0x03f0, // HP
0x044f, // Thrustmaster
0x045e, // Microsoft
0x0738, // Mad Catz
0x0b05, // ASUS
0x0e6f, // PDP
0x0f0d, // Hori
0x10f5, // Turtle Beach
0x1532, // Razer Wildcat
0x20d6, // PowerA
0x24c6, // PowerA
0x294b, // Snakebyte
0x2dc8, // 8BitDo
0x2e24, // Hyperkin
0x2e95, // SCUF
0x3285, // Nacon
0x3537, // GameSir
0x366c, // ByoWave
};
if (usbInterface.getId() == 0 &&
usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
int vendor_id = usbDevice.getVendorId();
for (int supportedVid : SUPPORTED_VENDORS) {
if (vendor_id == supportedVid) {
return true;
}
}
}
return false;
}
private void handleUsbDeviceAttached(UsbDevice usbDevice) {
connectHIDDeviceUSB(usbDevice);
}
private void handleUsbDeviceDetached(UsbDevice usbDevice) {
List<Integer> devices = new ArrayList<Integer>();
for (HIDDevice device : mDevicesById.values()) {
if (usbDevice.equals(device.getDevice())) {
devices.add(device.getId());
}
}
for (int id : devices) {
HIDDevice device = mDevicesById.get(id);
mDevicesById.remove(id);
device.shutdown();
HIDDeviceDisconnected(id);
}
}
private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) {
for (HIDDevice device : mDevicesById.values()) {
if (usbDevice.equals(device.getDevice())) {
boolean opened = false;
if (permission_granted) {
opened = device.open();
}
HIDDeviceOpenResult(device.getId(), opened);
}
}
}
private void connectHIDDeviceUSB(UsbDevice usbDevice) {
synchronized (this) {
int interface_mask = 0;
for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) {
UsbInterface usbInterface = usbDevice.getInterface(interface_index);
if (isHIDDeviceInterface(usbDevice, usbInterface)) {
// Check to see if we've already added this interface
// This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive
int interface_id = usbInterface.getId();
if ((interface_mask & (1 << interface_id)) != 0) {
continue;
}
interface_mask |= (1 << interface_id);
HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index);
int id = device.getId();
mDevicesById.put(id, device);
HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol(), false);
}
}
}
}
private void initializeBluetooth() {
Log.d(TAG, "Initializing Bluetooth");
if (Build.VERSION.SDK_INT >= 31 /* Android 12 */ &&
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH_CONNECT");
return;
}
if (Build.VERSION.SDK_INT <= 30 /* Android 11.0 (R) */ &&
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH");
return;
}
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE");
return;
}
// Find bonded bluetooth controllers and create SteamControllers for them
mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
if (mBluetoothManager == null) {
// This device doesn't support Bluetooth.
return;
}
BluetoothAdapter btAdapter = mBluetoothManager.getAdapter();
if (btAdapter == null) {
// This device has Bluetooth support in the codebase, but has no available adapters.
return;
}
// Get our bonded devices.
for (BluetoothDevice device : btAdapter.getBondedDevices()) {
Log.d(TAG, "Bluetooth device available: " + device);
if (isSteamController(device)) {
connectBluetoothDevice(device);
}
}
// NOTE: These don't work on Chromebooks, to my undying dismay.
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
if (Build.VERSION.SDK_INT >= 33) { /* Android 13.0 (TIRAMISU) */
mContext.registerReceiver(mBluetoothBroadcast, filter, Context.RECEIVER_EXPORTED);
} else {
mContext.registerReceiver(mBluetoothBroadcast, filter);
}
if (mIsChromebook) {
mHandler = new Handler(Looper.getMainLooper());
mLastBluetoothDevices = new ArrayList<BluetoothDevice>();
// final HIDDeviceManager finalThis = this;
// mHandler.postDelayed(new Runnable() {
// @Override
// public void run() {
// finalThis.chromebookConnectionHandler();
// }
// }, 5000);
}
}
private void shutdownBluetooth() {
try {
mContext.unregisterReceiver(mBluetoothBroadcast);
} catch (Exception e) {
// We may not have registered, that's okay
}
}
// Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly.
// This function provides a sort of dummy version of that, watching for changes in the
// connected devices and attempting to add controllers as things change.
void chromebookConnectionHandler() {
if (!mIsChromebook) {
return;
}
ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>();
ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>();
List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
for (BluetoothDevice bluetoothDevice : currentConnected) {
if (!mLastBluetoothDevices.contains(bluetoothDevice)) {
connected.add(bluetoothDevice);
}
}
for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) {
if (!currentConnected.contains(bluetoothDevice)) {
disconnected.add(bluetoothDevice);
}
}
mLastBluetoothDevices = currentConnected;
for (BluetoothDevice bluetoothDevice : disconnected) {
disconnectBluetoothDevice(bluetoothDevice);
}
for (BluetoothDevice bluetoothDevice : connected) {
connectBluetoothDevice(bluetoothDevice);
}
final HIDDeviceManager finalThis = this;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
finalThis.chromebookConnectionHandler();
}
}, 10000);
}
boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) {
Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice);
synchronized (this) {
if (mBluetoothDevices.containsKey(bluetoothDevice)) {
Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect");
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
device.reconnect();
return false;
}
HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice);
int id = device.getId();
mBluetoothDevices.put(bluetoothDevice, device);
mDevicesById.put(id, device);
// The Steam Controller will mark itself connected once initialization is complete
}
return true;
}
void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) {
synchronized (this) {
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
if (device == null)
return;
int id = device.getId();
mBluetoothDevices.remove(bluetoothDevice);
mDevicesById.remove(id);
device.shutdown();
HIDDeviceDisconnected(id);
}
}
boolean isSteamController(BluetoothDevice bluetoothDevice) {
// Sanity check. If you pass in a null device, by definition it is never a Steam Controller.
if (bluetoothDevice == null) {
return false;
}
// If the device has no local name, we really don't want to try an equality check against it.
if (bluetoothDevice.getName() == null) {
return false;
}
return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0);
}
private void close() {
shutdownUSB();
shutdownBluetooth();
synchronized (this) {
for (HIDDevice device : mDevicesById.values()) {
device.shutdown();
}
mDevicesById.clear();
mBluetoothDevices.clear();
HIDDeviceReleaseCallback();
}
}
public void setFrozen(boolean frozen) {
synchronized (this) {
for (HIDDevice device : mDevicesById.values()) {
device.setFrozen(frozen);
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
private HIDDevice getDevice(int id) {
synchronized (this) {
HIDDevice result = mDevicesById.get(id);
if (result == null) {
Log.v(TAG, "No device for id: " + id);
Log.v(TAG, "Available devices: " + mDevicesById.keySet());
}
return result;
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
////////// JNI interface functions
//////////////////////////////////////////////////////////////////////////////////////////////////////
boolean initialize(boolean usb, boolean bluetooth) {
Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")");
if (usb) {
initializeUSB();
}
if (bluetooth) {
initializeBluetooth();
}
return true;
}
boolean openDevice(int deviceID) {
Log.v(TAG, "openDevice deviceID=" + deviceID);
HIDDevice device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return false;
}
// Look to see if this is a USB device and we have permission to access it
UsbDevice usbDevice = device.getDevice();
if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) {
HIDDeviceOpenPending(deviceID);
try {
final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31
int flags;
if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
flags = FLAG_MUTABLE;
} else {
flags = 0;
}
Intent intent = new Intent(HIDDeviceManager.ACTION_USB_PERMISSION);
intent.setPackage(mContext.getPackageName());
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, intent, flags));
} catch (Exception e) {
Log.v(TAG, "Couldn't request permission for USB device " + usbDevice);
HIDDeviceOpenResult(deviceID, false);
}
return false;
}
try {
return device.open();
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return false;
}
int writeReport(int deviceID, byte[] report, boolean feature) {
try {
//Log.v(TAG, "writeReport deviceID=" + deviceID + " length=" + report.length);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return -1;
}
return device.writeReport(report, feature);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return -1;
}
boolean readReport(int deviceID, byte[] report, boolean feature) {
try {
//Log.v(TAG, "readReport deviceID=" + deviceID);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return false;
}
return device.readReport(report, feature);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return false;
}
void closeDevice(int deviceID) {
try {
Log.v(TAG, "closeDevice deviceID=" + deviceID);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return;
}
device.close();
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////// Native methods
//////////////////////////////////////////////////////////////////////////////////////////////////////
private native void HIDDeviceRegisterCallback();
private native void HIDDeviceReleaseCallback();
native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol, boolean bBluetooth);
native void HIDDeviceOpenPending(int deviceID);
native void HIDDeviceOpenResult(int deviceID, boolean opened);
native void HIDDeviceDisconnected(int deviceID);
native void HIDDeviceInputReport(int deviceID, byte[] report);
native void HIDDeviceReportResponse(int deviceID, byte[] report);
}
@@ -0,0 +1,313 @@
package org.libsdl.app;
import android.hardware.usb.*;
import android.os.Build;
import android.util.Log;
import java.util.Arrays;
import java.util.Locale;
class HIDDeviceUSB implements HIDDevice {
private static final String TAG = "hidapi";
protected HIDDeviceManager mManager;
protected UsbDevice mDevice;
protected int mInterfaceIndex;
protected int mInterface;
protected int mDeviceId;
protected UsbDeviceConnection mConnection;
protected UsbEndpoint mInputEndpoint;
protected UsbEndpoint mOutputEndpoint;
protected InputThread mInputThread;
protected boolean mRunning;
protected boolean mFrozen;
public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) {
mManager = manager;
mDevice = usbDevice;
mInterfaceIndex = interface_index;
mInterface = mDevice.getInterface(mInterfaceIndex).getId();
mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier());
mRunning = false;
}
String getIdentifier() {
return String.format(Locale.ENGLISH, "%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex);
}
@Override
public int getId() {
return mDeviceId;
}
@Override
public int getVendorId() {
return mDevice.getVendorId();
}
@Override
public int getProductId() {
return mDevice.getProductId();
}
@Override
public String getSerialNumber() {
String result = null;
try {
result = mDevice.getSerialNumber();
}
catch (SecurityException exception) {
//Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage());
}
if (result == null) {
result = "";
}
return result;
}
@Override
public int getVersion() {
return 0;
}
@Override
public String getManufacturerName() {
String result;
result = mDevice.getManufacturerName();
if (result == null) {
result = String.format("%x", getVendorId());
}
return result;
}
@Override
public String getProductName() {
String result;
result = mDevice.getProductName();
if (result == null) {
result = String.format("%x", getProductId());
}
return result;
}
@Override
public UsbDevice getDevice() {
return mDevice;
}
String getDeviceName() {
return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")";
}
@Override
public boolean open() {
mConnection = mManager.getUSBManager().openDevice(mDevice);
if (mConnection == null) {
Log.w(TAG, "Unable to open USB device " + getDeviceName());
return false;
}
// Force claim our interface
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
if (!mConnection.claimInterface(iface, true)) {
Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName());
close();
return false;
}
// Find the endpoints
for (int j = 0; j < iface.getEndpointCount(); j++) {
UsbEndpoint endpt = iface.getEndpoint(j);
switch (endpt.getDirection()) {
case UsbConstants.USB_DIR_IN:
if (mInputEndpoint == null) {
mInputEndpoint = endpt;
}
break;
case UsbConstants.USB_DIR_OUT:
if (mOutputEndpoint == null) {
mOutputEndpoint = endpt;
}
break;
}
}
// Make sure the required endpoints were present
if (mInputEndpoint == null || mOutputEndpoint == null) {
Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName());
close();
return false;
}
// Start listening for input
mRunning = true;
mInputThread = new InputThread();
mInputThread.start();
return true;
}
@Override
public int writeReport(byte[] report, boolean feature) {
if (mConnection == null) {
Log.w(TAG, "writeReport() called with no device connection");
return -1;
}
if (feature) {
int res = -1;
int offset = 0;
int length = report.length;
boolean skipped_report_id = false;
byte report_number = report[0];
if (report_number == 0x0) {
++offset;
--length;
skipped_report_id = true;
}
res = mConnection.controlTransfer(
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT,
0x09/*HID set_report*/,
(3/*HID feature*/ << 8) | report_number,
mInterface,
report, offset, length,
1000/*timeout millis*/);
if (res < 0) {
Log.w(TAG, "writeFeatureReport() returned " + res + " on device " + getDeviceName());
return -1;
}
if (skipped_report_id) {
++length;
}
return length;
} else {
int res = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000);
if (res != report.length) {
Log.w(TAG, "writeOutputReport() returned " + res + " on device " + getDeviceName());
}
return res;
}
}
@Override
public boolean readReport(byte[] report, boolean feature) {
int res = -1;
int offset = 0;
int length = report.length;
boolean skipped_report_id = false;
byte report_number = report[0];
if (mConnection == null) {
Log.w(TAG, "readReport() called with no device connection");
return false;
}
if (report_number == 0x0) {
/* Offset the return buffer by 1, so that the report ID
will remain in byte 0. */
++offset;
--length;
skipped_report_id = true;
}
res = mConnection.controlTransfer(
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN,
0x01/*HID get_report*/,
((feature ? 3/*HID feature*/ : 1/*HID Input*/) << 8) | report_number,
mInterface,
report, offset, length,
1000/*timeout millis*/);
if (res < 0) {
Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName());
return false;
}
if (skipped_report_id) {
++res;
++length;
}
byte[] data;
if (res == length) {
data = report;
} else {
data = Arrays.copyOfRange(report, 0, res);
}
mManager.HIDDeviceReportResponse(mDeviceId, data);
return true;
}
@Override
public void close() {
mRunning = false;
if (mInputThread != null) {
while (mInputThread.isAlive()) {
mInputThread.interrupt();
try {
mInputThread.join();
} catch (InterruptedException e) {
// Keep trying until we're done
}
}
mInputThread = null;
}
if (mConnection != null) {
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
mConnection.releaseInterface(iface);
mConnection.close();
mConnection = null;
}
}
@Override
public void shutdown() {
close();
mManager = null;
}
@Override
public void setFrozen(boolean frozen) {
mFrozen = frozen;
}
protected class InputThread extends Thread {
@Override
public void run() {
int packetSize = mInputEndpoint.getMaxPacketSize();
byte[] packet = new byte[packetSize];
while (mRunning) {
int r;
try
{
r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000);
}
catch (Exception e)
{
Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e);
break;
}
if (r < 0) {
// Could be a timeout or an I/O error
}
if (r > 0) {
byte[] data;
if (r == packetSize) {
data = packet;
} else {
data = Arrays.copyOfRange(packet, 0, r);
}
if (!mFrozen) {
mManager.HIDDeviceInputReport(mDeviceId, data);
}
}
}
}
}
}
@@ -0,0 +1,90 @@
package org.libsdl.app;
import android.app.Activity;
import android.content.Context;
import java.lang.reflect.Method;
/**
SDL library initialization
*/
public class SDL {
// This function should be called first and sets up the native code
// so it can call into the Java classes
static public void setupJNI() {
SDLActivity.nativeSetupJNI();
SDLAudioManager.nativeSetupJNI();
SDLControllerManager.nativeSetupJNI();
}
// This function should be called each time the activity is started
static public void initialize() {
setContext(null);
SDLActivity.initialize();
SDLAudioManager.initialize();
SDLControllerManager.initialize();
}
// This function stores the current activity (SDL or not)
static public void setContext(Activity context) {
SDLAudioManager.setContext(context);
mContext = context;
}
static public Activity getContext() {
return mContext;
}
static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
loadLibrary(libraryName, mContext);
}
static void loadLibrary(String libraryName, Context context) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
if (libraryName == null) {
throw new NullPointerException("No library name provided.");
}
try {
// Let's see if we have ReLinker available in the project. This is necessary for
// some projects that have huge numbers of local libraries bundled, and thus may
// trip a bug in Android's native library loader which ReLinker works around. (If
// loadLibrary works properly, ReLinker will simply use the normal Android method
// internally.)
//
// To use ReLinker, just add it as a dependency. For more information, see
// https://github.com/KeepSafe/ReLinker for ReLinker's repository.
//
Class<?> relinkClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
Class<?> relinkListenerClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
Class<?> contextClass = context.getClassLoader().loadClass("android.content.Context");
Class<?> stringClass = context.getClassLoader().loadClass("java.lang.String");
// Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if
// they've changed during updates.
Method forceMethod = relinkClass.getDeclaredMethod("force");
Object relinkInstance = forceMethod.invoke(null);
Class<?> relinkInstanceClass = relinkInstance.getClass();
// Actually load the library!
Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass);
loadMethod.invoke(relinkInstance, context, libraryName, null, null);
}
catch (final Throwable e) {
// Fall back
try {
System.loadLibrary(libraryName);
}
catch (final UnsatisfiedLinkError ule) {
throw ule;
}
catch (final SecurityException se) {
throw se;
}
}
}
protected static Activity mContext;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,126 @@
package org.libsdl.app;
import android.content.Context;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Build;
import android.util.Log;
import java.util.Arrays;
import java.util.ArrayList;
class SDLAudioManager {
protected static final String TAG = "SDLAudio";
protected static Context mContext;
private static AudioDeviceCallback mAudioDeviceCallback;
static void initialize() {
mAudioDeviceCallback = null;
if(Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */)
{
mAudioDeviceCallback = new AudioDeviceCallback() {
@Override
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
for (AudioDeviceInfo deviceInfo : addedDevices) {
nativeAddAudioDevice(deviceInfo.isSink(), deviceInfo.getProductName().toString(), deviceInfo.getId());
}
}
@Override
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
for (AudioDeviceInfo deviceInfo : removedDevices) {
nativeRemoveAudioDevice(deviceInfo.isSink(), deviceInfo.getId());
}
}
};
}
}
static void setContext(Context context) {
mContext = context;
}
static void release(Context context) {
// no-op atm
}
// Audio
private static AudioDeviceInfo getInputAudioDeviceInfo(int deviceId) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) {
if (deviceInfo.getId() == deviceId) {
return deviceInfo;
}
}
}
return null;
}
private static AudioDeviceInfo getPlaybackAudioDeviceInfo(int deviceId) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
if (deviceInfo.getId() == deviceId) {
return deviceInfo;
}
}
}
return null;
}
static void registerAudioDeviceCallback() {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
// get an initial list now, before hotplug callbacks fire.
for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
if (dev.getType() == AudioDeviceInfo.TYPE_TELEPHONY) {
continue; // Device cannot be opened
}
nativeAddAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId());
}
for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) {
nativeAddAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId());
}
audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null);
}
}
static void unregisterAudioDeviceCallback() {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback);
}
}
/** This method is called by SDL using JNI. */
static void audioSetThreadPriority(boolean recording, int device_id) {
try {
/* Set thread name */
if (recording) {
Thread.currentThread().setName("SDLAudioC" + device_id);
} else {
Thread.currentThread().setName("SDLAudioP" + device_id);
}
/* Set thread priority */
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
} catch (Exception e) {
Log.v(TAG, "modify thread properties failed " + e.toString());
}
}
static native void nativeSetupJNI();
static native void nativeRemoveAudioDevice(boolean recording, int deviceId);
static native void nativeAddAudioDevice(boolean recording, String name, int deviceId);
}
@@ -0,0 +1,935 @@
package org.libsdl.app;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import android.content.Context;
import android.hardware.lights.Light;
import android.hardware.lights.LightsRequest;
import android.hardware.lights.LightsManager;
import android.hardware.lights.LightState;
import android.graphics.Color;
import android.os.Build;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.os.VibratorManager;
import android.util.Log;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
public class SDLControllerManager
{
static native void nativeSetupJNI();
static native void nativeAddJoystick(int device_id, String name, String desc,
int vendor_id, int product_id,
int button_mask,
int naxes, int axis_mask, int nhats, boolean can_rumble, boolean has_rgb_led);
static native void nativeRemoveJoystick(int device_id);
static native void nativeAddHaptic(int device_id, String name);
static native void nativeRemoveHaptic(int device_id);
static public native boolean onNativePadDown(int device_id, int keycode);
static public native boolean onNativePadUp(int device_id, int keycode);
static native void onNativeJoy(int device_id, int axis,
float value);
static native void onNativeHat(int device_id, int hat_id,
int x, int y);
protected static SDLJoystickHandler mJoystickHandler;
protected static SDLHapticHandler mHapticHandler;
private static final String TAG = "SDLControllerManager";
static void initialize() {
if (mJoystickHandler == null) {
mJoystickHandler = new SDLJoystickHandler();
}
if (mHapticHandler == null) {
if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
mHapticHandler = new SDLHapticHandler_API31();
} else if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) {
mHapticHandler = new SDLHapticHandler_API26();
} else {
mHapticHandler = new SDLHapticHandler();
}
}
}
// Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
static public boolean handleJoystickMotionEvent(MotionEvent event) {
return mJoystickHandler.handleMotionEvent(event);
}
/**
* This method is called by SDL using JNI.
*/
static void pollInputDevices() {
mJoystickHandler.pollInputDevices();
}
/**
* This method is called by SDL using JNI.
*/
static void joystickSetLED(int device_id, int red, int green, int blue) {
mJoystickHandler.setLED(device_id, red, green, blue);
}
/**
* This method is called by SDL using JNI.
*/
static void pollHapticDevices() {
mHapticHandler.pollHapticDevices();
}
/**
* This method is called by SDL using JNI.
*/
static void hapticRun(int device_id, float intensity, int length) {
mHapticHandler.run(device_id, intensity, length);
}
/**
* This method is called by SDL using JNI.
*/
static void hapticRumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) {
mHapticHandler.rumble(device_id, low_frequency_intensity, high_frequency_intensity, length);
}
/**
* This method is called by SDL using JNI.
*/
static void hapticStop(int device_id)
{
mHapticHandler.stop(device_id);
}
// Check if a given device is considered a possible SDL joystick
static public boolean isDeviceSDLJoystick(int deviceId) {
InputDevice device = InputDevice.getDevice(deviceId);
// We cannot use InputDevice.isVirtual before API 16, so let's accept
// only nonnegative device ids (VIRTUAL_KEYBOARD equals -1)
if ((device == null) || (deviceId < 0)) {
return false;
}
int sources = device.getSources();
/* This is called for every button press, so let's not spam the logs */
/*
if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
Log.v(TAG, "Input device " + device.getName() + " has class joystick.");
}
if ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) {
Log.v(TAG, "Input device " + device.getName() + " is a dpad.");
}
if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
Log.v(TAG, "Input device " + device.getName() + " is a gamepad.");
}
*/
return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 ||
((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) ||
((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
);
}
}
/* Actual joystick functionality available for API >= 19 devices */
class SDLJoystickHandler {
static class SDLJoystick {
int device_id;
String name;
String desc;
ArrayList<InputDevice.MotionRange> axes;
ArrayList<InputDevice.MotionRange> hats;
ArrayList<Light> lights;
LightsManager.LightsSession lightsSession;
}
static class RangeComparator implements Comparator<InputDevice.MotionRange> {
@Override
public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
// Some controllers, like the Moga Pro 2, return AXIS_GAS (22) for right trigger and AXIS_BRAKE (23) for left trigger - swap them so they're sorted in the right order for SDL
int arg0Axis = arg0.getAxis();
int arg1Axis = arg1.getAxis();
if (arg0Axis == MotionEvent.AXIS_GAS) {
arg0Axis = MotionEvent.AXIS_BRAKE;
} else if (arg0Axis == MotionEvent.AXIS_BRAKE) {
arg0Axis = MotionEvent.AXIS_GAS;
}
if (arg1Axis == MotionEvent.AXIS_GAS) {
arg1Axis = MotionEvent.AXIS_BRAKE;
} else if (arg1Axis == MotionEvent.AXIS_BRAKE) {
arg1Axis = MotionEvent.AXIS_GAS;
}
// Make sure the AXIS_Z is sorted between AXIS_RY and AXIS_RZ.
// This is because the usual pairing are:
// - AXIS_X + AXIS_Y (left stick).
// - AXIS_RX, AXIS_RY (sometimes the right stick, sometimes triggers).
// - AXIS_Z, AXIS_RZ (sometimes the right stick, sometimes triggers).
// This sorts the axes in the above order, which tends to be correct
// for Xbox-ish game pads that have the right stick on RX/RY and the
// triggers on Z/RZ.
//
// Gamepads that don't have AXIS_Z/AXIS_RZ but use
// AXIS_LTRIGGER/AXIS_RTRIGGER are unaffected by this.
//
// References:
// - https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input
// - https://www.kernel.org/doc/html/latest/input/gamepad.html
if (arg0Axis == MotionEvent.AXIS_Z) {
arg0Axis = MotionEvent.AXIS_RZ - 1;
} else if (arg0Axis > MotionEvent.AXIS_Z && arg0Axis < MotionEvent.AXIS_RZ) {
--arg0Axis;
}
if (arg1Axis == MotionEvent.AXIS_Z) {
arg1Axis = MotionEvent.AXIS_RZ - 1;
} else if (arg1Axis > MotionEvent.AXIS_Z && arg1Axis < MotionEvent.AXIS_RZ) {
--arg1Axis;
}
return arg0Axis - arg1Axis;
}
}
private final ArrayList<SDLJoystick> mJoysticks;
SDLJoystickHandler() {
mJoysticks = new ArrayList<SDLJoystick>();
}
/**
* Handles adding and removing of input devices.
*/
synchronized void pollInputDevices() {
int[] deviceIds = InputDevice.getDeviceIds();
for (int device_id : deviceIds) {
if (SDLControllerManager.isDeviceSDLJoystick(device_id)) {
SDLJoystick joystick = getJoystick(device_id);
if (joystick == null) {
InputDevice joystickDevice = InputDevice.getDevice(device_id);
joystick = new SDLJoystick();
joystick.device_id = device_id;
joystick.name = joystickDevice.getName();
joystick.desc = getJoystickDescriptor(joystickDevice);
joystick.axes = new ArrayList<InputDevice.MotionRange>();
joystick.hats = new ArrayList<InputDevice.MotionRange>();
joystick.lights = new ArrayList<Light>();
List<InputDevice.MotionRange> ranges = joystickDevice.getMotionRanges();
Collections.sort(ranges, new RangeComparator());
for (InputDevice.MotionRange range : ranges) {
if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {
joystick.hats.add(range);
} else {
joystick.axes.add(range);
}
}
}
boolean can_rumble = false;
boolean has_rgb_led = false;
if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
VibratorManager vibratorManager = joystickDevice.getVibratorManager();
int[] vibrators = vibratorManager.getVibratorIds();
if (vibrators.length > 0) {
can_rumble = true;
}
LightsManager lightsManager = joystickDevice.getLightsManager();
List<Light> lights = lightsManager.getLights();
for (Light light : lights) {
if (light.hasRgbControl()) {
joystick.lights.add(light);
}
}
if (!joystick.lights.isEmpty()) {
joystick.lightsSession = lightsManager.openSession();
has_rgb_led = true;
}
}
mJoysticks.add(joystick);
SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc,
getVendorId(joystickDevice), getProductId(joystickDevice),
getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, can_rumble, has_rgb_led);
}
}
}
/* Check removed devices */
ArrayList<Integer> removedDevices = null;
for (SDLJoystick joystick : mJoysticks) {
int device_id = joystick.device_id;
int i;
for (i = 0; i < deviceIds.length; i++) {
if (device_id == deviceIds[i]) break;
}
if (i == deviceIds.length) {
if (removedDevices == null) {
removedDevices = new ArrayList<Integer>();
}
removedDevices.add(device_id);
}
}
if (removedDevices != null) {
for (int device_id : removedDevices) {
SDLControllerManager.nativeRemoveJoystick(device_id);
for (int i = 0; i < mJoysticks.size(); i++) {
if (mJoysticks.get(i).device_id == device_id) {
if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
if (mJoysticks.get(i).lightsSession != null) {
try {
mJoysticks.get(i).lightsSession.close();
} catch (Exception e) {
// Session may already be unregistered when device disconnects
}
mJoysticks.get(i).lightsSession = null;
}
}
mJoysticks.remove(i);
break;
}
}
}
}
}
synchronized protected SDLJoystick getJoystick(int device_id) {
for (SDLJoystick joystick : mJoysticks) {
if (joystick.device_id == device_id) {
return joystick;
}
}
return null;
}
/**
* Handles given MotionEvent.
* @param event the event to be handled.
* @return if given event was processed.
*/
boolean handleMotionEvent(MotionEvent event) {
int actionPointerIndex = event.getActionIndex();
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_MOVE) {
SDLJoystick joystick = getJoystick(event.getDeviceId());
if (joystick != null) {
for (int i = 0; i < joystick.axes.size(); i++) {
InputDevice.MotionRange range = joystick.axes.get(i);
/* Normalize the value to -1...1 */
float value = (event.getAxisValue(range.getAxis(), actionPointerIndex) - range.getMin()) / range.getRange() * 2.0f - 1.0f;
SDLControllerManager.onNativeJoy(joystick.device_id, i, value);
}
for (int i = 0; i < joystick.hats.size() / 2; i++) {
int hatX = Math.round(event.getAxisValue(joystick.hats.get(2 * i).getAxis(), actionPointerIndex));
int hatY = Math.round(event.getAxisValue(joystick.hats.get(2 * i + 1).getAxis(), actionPointerIndex));
SDLControllerManager.onNativeHat(joystick.device_id, i, hatX, hatY);
}
}
}
return true;
}
String getJoystickDescriptor(InputDevice joystickDevice) {
String desc = joystickDevice.getDescriptor();
if (desc != null && !desc.isEmpty()) {
return desc;
}
return joystickDevice.getName();
}
int getProductId(InputDevice joystickDevice) {
return joystickDevice.getProductId();
}
int getVendorId(InputDevice joystickDevice) {
return joystickDevice.getVendorId();
}
int getAxisMask(List<InputDevice.MotionRange> ranges) {
// For compatibility, keep computing the axis mask like before,
// only really distinguishing 2, 4 and 6 axes.
int axis_mask = 0;
if (ranges.size() >= 2) {
// ((1 << SDL_GAMEPAD_AXIS_LEFTX) | (1 << SDL_GAMEPAD_AXIS_LEFTY))
axis_mask |= 0x0003;
}
if (ranges.size() >= 4) {
// ((1 << SDL_GAMEPAD_AXIS_RIGHTX) | (1 << SDL_GAMEPAD_AXIS_RIGHTY))
axis_mask |= 0x000c;
}
if (ranges.size() >= 6) {
// ((1 << SDL_GAMEPAD_AXIS_LEFT_TRIGGER) | (1 << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER))
axis_mask |= 0x0030;
}
// Also add an indicator bit for whether the sorting order has changed.
// This serves to disable outdated gamecontrollerdb.txt mappings.
boolean have_z = false;
boolean have_past_z_before_rz = false;
for (InputDevice.MotionRange range : ranges) {
int axis = range.getAxis();
if (axis == MotionEvent.AXIS_Z) {
have_z = true;
} else if (axis > MotionEvent.AXIS_Z && axis < MotionEvent.AXIS_RZ) {
have_past_z_before_rz = true;
}
}
if (have_z && have_past_z_before_rz) {
// If both these exist, the compare() function changed sorting order.
// Set a bit to indicate this fact.
axis_mask |= 0x8000;
}
return axis_mask;
}
int getButtonMask(InputDevice joystickDevice) {
int button_mask = 0;
int[] keys = new int[] {
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_B,
KeyEvent.KEYCODE_BUTTON_X,
KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BACK,
KeyEvent.KEYCODE_MENU,
KeyEvent.KEYCODE_BUTTON_MODE,
KeyEvent.KEYCODE_BUTTON_START,
KeyEvent.KEYCODE_BUTTON_THUMBL,
KeyEvent.KEYCODE_BUTTON_THUMBR,
KeyEvent.KEYCODE_BUTTON_L1,
KeyEvent.KEYCODE_BUTTON_R1,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_BUTTON_SELECT,
KeyEvent.KEYCODE_DPAD_CENTER,
// These don't map into any SDL controller buttons directly
KeyEvent.KEYCODE_BUTTON_L2,
KeyEvent.KEYCODE_BUTTON_R2,
KeyEvent.KEYCODE_BUTTON_C,
KeyEvent.KEYCODE_BUTTON_Z,
KeyEvent.KEYCODE_BUTTON_1,
KeyEvent.KEYCODE_BUTTON_2,
KeyEvent.KEYCODE_BUTTON_3,
KeyEvent.KEYCODE_BUTTON_4,
KeyEvent.KEYCODE_BUTTON_5,
KeyEvent.KEYCODE_BUTTON_6,
KeyEvent.KEYCODE_BUTTON_7,
KeyEvent.KEYCODE_BUTTON_8,
KeyEvent.KEYCODE_BUTTON_9,
KeyEvent.KEYCODE_BUTTON_10,
KeyEvent.KEYCODE_BUTTON_11,
KeyEvent.KEYCODE_BUTTON_12,
KeyEvent.KEYCODE_BUTTON_13,
KeyEvent.KEYCODE_BUTTON_14,
KeyEvent.KEYCODE_BUTTON_15,
KeyEvent.KEYCODE_BUTTON_16,
};
int[] masks = new int[] {
(1 << 0), // A -> A
(1 << 1), // B -> B
(1 << 2), // X -> X
(1 << 3), // Y -> Y
(1 << 4), // BACK -> BACK
(1 << 6), // MENU -> START
(1 << 5), // MODE -> GUIDE
(1 << 6), // START -> START
(1 << 7), // THUMBL -> LEFTSTICK
(1 << 8), // THUMBR -> RIGHTSTICK
(1 << 9), // L1 -> LEFTSHOULDER
(1 << 10), // R1 -> RIGHTSHOULDER
(1 << 11), // DPAD_UP -> DPAD_UP
(1 << 12), // DPAD_DOWN -> DPAD_DOWN
(1 << 13), // DPAD_LEFT -> DPAD_LEFT
(1 << 14), // DPAD_RIGHT -> DPAD_RIGHT
(1 << 4), // SELECT -> BACK
(1 << 0), // DPAD_CENTER -> A
(1 << 15), // L2 -> ??
(1 << 16), // R2 -> ??
(1 << 17), // C -> ??
(1 << 18), // Z -> ??
(1 << 20), // 1 -> ??
(1 << 21), // 2 -> ??
(1 << 22), // 3 -> ??
(1 << 23), // 4 -> ??
(1 << 24), // 5 -> ??
(1 << 25), // 6 -> ??
(1 << 26), // 7 -> ??
(1 << 27), // 8 -> ??
(1 << 28), // 9 -> ??
(1 << 29), // 10 -> ??
(1 << 30), // 11 -> ??
(1 << 31), // 12 -> ??
// We're out of room...
0xFFFFFFFF, // 13 -> ??
0xFFFFFFFF, // 14 -> ??
0xFFFFFFFF, // 15 -> ??
0xFFFFFFFF, // 16 -> ??
};
boolean[] has_keys = joystickDevice.hasKeys(keys);
for (int i = 0; i < keys.length; ++i) {
if (has_keys[i]) {
button_mask |= masks[i];
}
}
return button_mask;
}
void setLED(int device_id, int red, int green, int blue) {
if (Build.VERSION.SDK_INT < 31 /* Android 12.0 (S) */) {
return;
}
SDLJoystick joystick = getJoystick(device_id);
if (joystick == null || joystick.lights.isEmpty()) {
return;
}
LightsRequest.Builder lightsRequest = new LightsRequest.Builder();
LightState lightState = new LightState.Builder().setColor(Color.rgb(red, green, blue)).build();
for (Light light : joystick.lights) {
if (light.hasRgbControl()) {
lightsRequest.addLight(light, lightState);
}
}
joystick.lightsSession.requestLights(lightsRequest.build());
}
}
class SDLHapticHandler_API31 extends SDLHapticHandler {
@Override
void run(int device_id, float intensity, int length) {
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
vibrate(haptic.vib, intensity, length);
}
}
@Override
void rumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) {
InputDevice device = InputDevice.getDevice(device_id);
if (device == null) {
return;
}
if (Build.VERSION.SDK_INT < 31 /* Android 12.0 (S) */) {
/* Silence 'lint' warning */
return;
}
VibratorManager manager = device.getVibratorManager();
int[] vibrators = manager.getVibratorIds();
if (vibrators.length >= 2) {
vibrate(manager.getVibrator(vibrators[0]), low_frequency_intensity, length);
vibrate(manager.getVibrator(vibrators[1]), high_frequency_intensity, length);
} else if (vibrators.length == 1) {
float intensity = (low_frequency_intensity * 0.6f) + (high_frequency_intensity * 0.4f);
vibrate(manager.getVibrator(vibrators[0]), intensity, length);
}
}
private void vibrate(Vibrator vibrator, float intensity, int length) {
if (Build.VERSION.SDK_INT < 31 /* Android 12.0 (S) */) {
/* Silence 'lint' warning */
return;
}
if (intensity == 0.0f) {
vibrator.cancel();
return;
}
int value = Math.round(intensity * 255);
if (value > 255) {
value = 255;
}
if (value < 1) {
vibrator.cancel();
return;
}
try {
vibrator.vibrate(VibrationEffect.createOneShot(length, value));
}
catch (Exception e) {
// Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if
// something went horribly wrong with the Android 8.0 APIs.
vibrator.vibrate(length);
}
}
}
class SDLHapticHandler_API26 extends SDLHapticHandler {
@Override
void run(int device_id, float intensity, int length) {
if (Build.VERSION.SDK_INT < 26 /* Android 8.0 (O) */) {
/* Silence 'lint' warning */
return;
}
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
if (intensity == 0.0f) {
stop(device_id);
return;
}
int vibeValue = Math.round(intensity * 255);
if (vibeValue > 255) {
vibeValue = 255;
}
if (vibeValue < 1) {
stop(device_id);
return;
}
try {
haptic.vib.vibrate(VibrationEffect.createOneShot(length, vibeValue));
}
catch (Exception e) {
// Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if
// something went horribly wrong with the Android 8.0 APIs.
haptic.vib.vibrate(length);
}
}
}
}
class SDLHapticHandler {
static class SDLHaptic {
int device_id;
String name;
Vibrator vib;
}
private final ArrayList<SDLHaptic> mHaptics;
SDLHapticHandler() {
mHaptics = new ArrayList<SDLHaptic>();
}
void run(int device_id, float intensity, int length) {
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
haptic.vib.vibrate(length);
}
}
void rumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) {
// Not supported in older APIs
}
void stop(int device_id) {
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
haptic.vib.cancel();
}
}
synchronized void pollHapticDevices() {
final int deviceId_VIBRATOR_SERVICE = 999999;
boolean hasVibratorService = false;
/* Check VIBRATOR_SERVICE */
Vibrator vib = (Vibrator) SDL.getContext().getSystemService(Context.VIBRATOR_SERVICE);
if (vib != null) {
hasVibratorService = vib.hasVibrator();
if (hasVibratorService) {
SDLHaptic haptic = getHaptic(deviceId_VIBRATOR_SERVICE);
if (haptic == null) {
haptic = new SDLHaptic();
haptic.device_id = deviceId_VIBRATOR_SERVICE;
haptic.name = "VIBRATOR_SERVICE";
haptic.vib = vib;
mHaptics.add(haptic);
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
}
}
}
/* Check removed devices */
ArrayList<Integer> removedDevices = null;
for (SDLHaptic haptic : mHaptics) {
int device_id = haptic.device_id;
if (device_id != deviceId_VIBRATOR_SERVICE || !hasVibratorService) {
if (removedDevices == null) {
removedDevices = new ArrayList<Integer>();
}
removedDevices.add(device_id);
} // else: don't remove the vibrator if it is still present
}
if (removedDevices != null) {
for (int device_id : removedDevices) {
SDLControllerManager.nativeRemoveHaptic(device_id);
for (int i = 0; i < mHaptics.size(); i++) {
if (mHaptics.get(i).device_id == device_id) {
mHaptics.remove(i);
break;
}
}
}
}
}
synchronized protected SDLHaptic getHaptic(int device_id) {
for (SDLHaptic haptic : mHaptics) {
if (haptic.device_id == device_id) {
return haptic;
}
}
return null;
}
}
class SDLGenericMotionListener_API14 implements View.OnGenericMotionListener {
protected static final int SDL_PEN_DEVICE_TYPE_UNKNOWN = 0;
protected static final int SDL_PEN_DEVICE_TYPE_DIRECT = 1;
protected static final int SDL_PEN_DEVICE_TYPE_INDIRECT = 2;
// Generic Motion (mouse hover, joystick...) events go here
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
if (event.getSource() == InputDevice.SOURCE_JOYSTICK)
return SDLControllerManager.handleJoystickMotionEvent(event);
float x, y;
int action = event.getActionMasked();
int pointerCount = event.getPointerCount();
boolean consumed = false;
for (int i = 0; i < pointerCount; i++) {
int toolType = event.getToolType(i);
if (toolType == MotionEvent.TOOL_TYPE_MOUSE) {
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, i);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, i);
SDLActivity.onNativeMouse(0, action, x, y, false);
consumed = true;
break;
case MotionEvent.ACTION_HOVER_MOVE:
x = getEventX(event, i);
y = getEventY(event, i);
SDLActivity.onNativeMouse(0, action, x, y, checkRelativeEvent(event));
consumed = true;
break;
default:
break;
}
} else if (toolType == MotionEvent.TOOL_TYPE_STYLUS || toolType == MotionEvent.TOOL_TYPE_ERASER) {
switch (action) {
case MotionEvent.ACTION_HOVER_ENTER:
case MotionEvent.ACTION_HOVER_MOVE:
case MotionEvent.ACTION_HOVER_EXIT:
x = event.getX(i);
y = event.getY(i);
float p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
// BUTTON_STYLUS_PRIMARY is 2^5, so shift by 4, and apply SDL_PEN_INPUT_DOWN/SDL_PEN_INPUT_ERASER_TIP
int buttons = (event.getButtonState() >> 4) | (1 << (toolType == MotionEvent.TOOL_TYPE_STYLUS ? 0 : 30));
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
buttons |= 0x08;
}
SDLActivity.onNativePen(event.getPointerId(i), getPenDeviceType(event.getDevice()), buttons, action, x, y, p);
consumed = true;
break;
}
}
}
return consumed;
}
boolean supportsRelativeMouse() {
return false;
}
boolean inRelativeMode() {
return false;
}
boolean setRelativeMouseEnabled(boolean enabled) {
return false;
}
void reclaimRelativeMouseModeIfNeeded() {
}
boolean checkRelativeEvent(MotionEvent event) {
return inRelativeMode();
}
float getEventX(MotionEvent event, int pointerIndex) {
return event.getX(pointerIndex);
}
float getEventY(MotionEvent event, int pointerIndex) {
return event.getY(pointerIndex);
}
int getPenDeviceType(InputDevice penDevice) {
return SDL_PEN_DEVICE_TYPE_UNKNOWN;
}
}
class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API14 {
// Generic Motion (mouse hover, joystick...) events go here
private boolean mRelativeModeEnabled;
@Override
boolean supportsRelativeMouse() {
return true;
}
@Override
boolean inRelativeMode() {
return mRelativeModeEnabled;
}
@Override
boolean setRelativeMouseEnabled(boolean enabled) {
mRelativeModeEnabled = enabled;
return true;
}
@Override
float getEventX(MotionEvent event, int pointerIndex) {
if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) {
/* Silence 'lint' warning */
return 0;
}
if (mRelativeModeEnabled && event.getToolType(pointerIndex) == MotionEvent.TOOL_TYPE_MOUSE) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X, pointerIndex);
} else {
return event.getX(pointerIndex);
}
}
@Override
float getEventY(MotionEvent event, int pointerIndex) {
if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) {
/* Silence 'lint' warning */
return 0;
}
if (mRelativeModeEnabled && event.getToolType(pointerIndex) == MotionEvent.TOOL_TYPE_MOUSE) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y, pointerIndex);
} else {
return event.getY(pointerIndex);
}
}
}
class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 {
// Generic Motion (mouse hover, joystick...) events go here
private boolean mRelativeModeEnabled;
@Override
boolean supportsRelativeMouse() {
return (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */);
}
@Override
boolean inRelativeMode() {
return mRelativeModeEnabled;
}
@Override
boolean setRelativeMouseEnabled(boolean enabled) {
if (Build.VERSION.SDK_INT < 26 /* Android 8.0 (O) */) {
/* Silence 'lint' warning */
return false;
}
if (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */) {
if (enabled) {
SDLActivity.getContentView().requestPointerCapture();
} else {
SDLActivity.getContentView().releasePointerCapture();
}
mRelativeModeEnabled = enabled;
return true;
} else {
return false;
}
}
@Override
void reclaimRelativeMouseModeIfNeeded() {
if (Build.VERSION.SDK_INT < 26 /* Android 8.0 (O) */) {
/* Silence 'lint' warning */
return;
}
if (mRelativeModeEnabled && !SDLActivity.isDeXMode()) {
SDLActivity.getContentView().requestPointerCapture();
}
}
@Override
boolean checkRelativeEvent(MotionEvent event) {
if (Build.VERSION.SDK_INT < 26 /* Android 8.0 (O) */) {
/* Silence 'lint' warning */
return false;
}
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
}
@Override
float getEventX(MotionEvent event, int pointerIndex) {
// Relative mouse in capture mode will only have relative for X/Y
return event.getX(pointerIndex);
}
@Override
float getEventY(MotionEvent event, int pointerIndex) {
// Relative mouse in capture mode will only have relative for X/Y
return event.getY(pointerIndex);
}
}
class SDLGenericMotionListener_API29 extends SDLGenericMotionListener_API26 {
@Override
int getPenDeviceType(InputDevice penDevice)
{
if (penDevice == null) {
return SDL_PEN_DEVICE_TYPE_UNKNOWN;
}
return penDevice.isExternal() ? SDL_PEN_DEVICE_TYPE_INDIRECT : SDL_PEN_DEVICE_TYPE_DIRECT;
}
}
@@ -0,0 +1,66 @@
package org.libsdl.app;
import android.content.*;
import android.text.InputType;
import android.view.*;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
/* This is a fake invisible editor view that receives the input and defines the
* pan&scan region
*/
public class SDLDummyEdit extends View implements View.OnKeyListener
{
InputConnection ic;
int input_type;
SDLDummyEdit(Context context) {
super(context);
setFocusableInTouchMode(true);
setFocusable(true);
setOnKeyListener(this);
}
void setInputType(int input_type) {
this.input_type = input_type;
}
@Override
public boolean onCheckIsTextEditor() {
return true;
}
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
return SDLActivity.handleKeyEvent(v, keyCode, event, ic);
}
//
@Override
public boolean onKeyPreIme (int keyCode, KeyEvent event) {
// As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event
// FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639
// FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not
// FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout
// FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
// FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :)
if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) {
SDLActivity.onNativeKeyboardFocusLost();
}
}
return super.onKeyPreIme(keyCode, event);
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
ic = new SDLInputConnection(this, true);
outAttrs.inputType = input_type;
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI |
EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */;
return ic;
}
}
@@ -0,0 +1,138 @@
package org.libsdl.app;
import android.content.*;
import android.os.Build;
import android.text.Editable;
import android.view.*;
import android.view.inputmethod.BaseInputConnection;
import android.widget.EditText;
class SDLInputConnection extends BaseInputConnection
{
protected EditText mEditText;
protected String mCommittedText = "";
SDLInputConnection(View targetView, boolean fullEditor) {
super(targetView, fullEditor);
mEditText = new EditText(SDL.getContext());
}
@Override
public Editable getEditable() {
return mEditText.getEditableText();
}
@Override
public boolean sendKeyEvent(KeyEvent event) {
/*
* This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard)
* However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses
* and so we need to generate them ourselves in commitText. To avoid duplicates on the handful of keys
* that still do, we empty this out.
*/
/*
* Return DOES still generate a key event, however. So rather than using it as the 'click a button' key
* as we do with physical keyboards, let's just use it to hide the keyboard.
*/
if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
if (SDLActivity.onNativeSoftReturnKey()) {
return true;
}
}
return super.sendKeyEvent(event);
}
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
if (!super.commitText(text, newCursorPosition)) {
return false;
}
updateText();
return true;
}
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
if (!super.setComposingText(text, newCursorPosition)) {
return false;
}
updateText();
return true;
}
@Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
if (Build.VERSION.SDK_INT <= 29 /* Android 10.0 (Q) */) {
// Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection
// and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
if (beforeLength > 0 && afterLength == 0) {
// backspace(s)
while (beforeLength-- > 0) {
nativeGenerateScancodeForUnichar('\b');
}
return true;
}
}
if (!super.deleteSurroundingText(beforeLength, afterLength)) {
return false;
}
updateText();
return true;
}
protected void updateText() {
final Editable content = getEditable();
if (content == null) {
return;
}
String text = content.toString();
int compareLength = Math.min(text.length(), mCommittedText.length());
int matchLength, offset;
/* Backspace over characters that are no longer in the string */
for (matchLength = 0; matchLength < compareLength; ) {
int codePoint = mCommittedText.codePointAt(matchLength);
if (codePoint != text.codePointAt(matchLength)) {
break;
}
matchLength += Character.charCount(codePoint);
}
/* FIXME: This doesn't handle graphemes, like '🌬️' */
for (offset = matchLength; offset < mCommittedText.length(); ) {
int codePoint = mCommittedText.codePointAt(offset);
nativeGenerateScancodeForUnichar('\b');
offset += Character.charCount(codePoint);
}
if (matchLength < text.length()) {
String pendingText = text.subSequence(matchLength, text.length()).toString();
if (!SDLActivity.dispatchingKeyEvent()) {
for (offset = 0; offset < pendingText.length(); ) {
int codePoint = pendingText.codePointAt(offset);
if (codePoint == '\n') {
if (SDLActivity.onNativeSoftReturnKey()) {
return;
}
}
/* Higher code points don't generate simulated scancodes */
if (codePoint > 0 && codePoint < 128) {
nativeGenerateScancodeForUnichar((char)codePoint);
}
offset += Character.charCount(codePoint);
}
}
SDLInputConnection.nativeCommitText(pendingText, 0);
}
mCommittedText = text;
}
public static native void nativeCommitText(String text, int newCursorPosition);
public static native void nativeGenerateScancodeForUnichar(char c);
}
@@ -0,0 +1,449 @@
package org.libsdl.app;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.graphics.Insets;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.ScaleGestureDetector;
/**
SDLSurface. This is what we draw on, so we need to know when it's created
in order to do anything useful.
Because of this, that's where we set up the SDL thread
*/
public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
View.OnApplyWindowInsetsListener, View.OnKeyListener, View.OnTouchListener,
SensorEventListener, ScaleGestureDetector.OnScaleGestureListener {
// Sensors
protected SensorManager mSensorManager;
protected Display mDisplay;
// Keep track of the surface size to normalize touch events
protected float mWidth, mHeight;
// Is SurfaceView ready for rendering
protected boolean mIsSurfaceReady;
// Pinch events
private final ScaleGestureDetector scaleGestureDetector;
// Startup
protected SDLSurface(Context context) {
super(context);
getHolder().addCallback(this);
scaleGestureDetector = new ScaleGestureDetector(context, this);
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
setOnApplyWindowInsetsListener(this);
setOnKeyListener(this);
setOnTouchListener(this);
mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
setOnGenericMotionListener(SDLActivity.getMotionListener());
// Some arbitrary defaults to avoid a potential division by zero
mWidth = 1.0f;
mHeight = 1.0f;
mIsSurfaceReady = false;
}
protected void handlePause() {
enableSensor(Sensor.TYPE_ACCELEROMETER, false);
}
protected void handleResume() {
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
setOnApplyWindowInsetsListener(this);
setOnKeyListener(this);
setOnTouchListener(this);
enableSensor(Sensor.TYPE_ACCELEROMETER, true);
}
protected Surface getNativeSurface() {
return getHolder().getSurface();
}
// Called when we have a valid drawing surface
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.v("SDL", "surfaceCreated()");
SDLActivity.onNativeSurfaceCreated();
}
// Called when we lose the surface
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.v("SDL", "surfaceDestroyed()");
// Transition to pause, if needed
SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
SDLActivity.handleNativeState();
mIsSurfaceReady = false;
SDLActivity.onNativeSurfaceDestroyed();
}
// Called when the surface is resized
@Override
public void surfaceChanged(SurfaceHolder holder,
int format, int width, int height) {
Log.v("SDL", "surfaceChanged()");
if (SDLActivity.mSingleton == null) {
return;
}
mWidth = width;
mHeight = height;
int nDeviceWidth = width;
int nDeviceHeight = height;
float density = 1.0f;
try
{
DisplayMetrics realMetrics = new DisplayMetrics();
mDisplay.getRealMetrics( realMetrics );
nDeviceWidth = realMetrics.widthPixels;
nDeviceHeight = realMetrics.heightPixels;
// Use densityDpi instead of density to more closely match what the UI scale is
density = (float)realMetrics.densityDpi / 160.0f;
} catch(Exception ignored) {
}
synchronized(SDLActivity.getContext()) {
// In case we're waiting on a size change after going fullscreen, send a notification.
SDLActivity.getContext().notifyAll();
}
Log.v("SDL", "Window size: " + width + "x" + height);
Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, density, mDisplay.getRefreshRate());
SDLActivity.onNativeResize();
// Prevent a screen distortion glitch,
// for instance when the device is in Landscape and a Portrait App is resumed.
boolean skip = false;
int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
if (mWidth > mHeight) {
skip = true;
}
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
if (mWidth < mHeight) {
skip = true;
}
}
// Special Patch for Square Resolution: Black Berry Passport
if (skip) {
double min = Math.min(mWidth, mHeight);
double max = Math.max(mWidth, mHeight);
if (max / min < 1.20) {
Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
skip = false;
}
}
// Don't skip if we might be multi-window or have popup dialogs
if (skip) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
skip = false;
}
}
if (skip) {
Log.v("SDL", "Skip .. Surface is not ready.");
mIsSurfaceReady = false;
return;
}
/* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
SDLActivity.onNativeSurfaceChanged();
/* Surface is ready */
mIsSurfaceReady = true;
SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
SDLActivity.handleNativeState();
}
// Window inset
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
if (Build.VERSION.SDK_INT >= 30 /* Android 11 (R) */) {
Insets combined = insets.getInsets(WindowInsets.Type.systemBars() |
WindowInsets.Type.systemGestures() |
WindowInsets.Type.mandatorySystemGestures() |
WindowInsets.Type.tappableElement() |
WindowInsets.Type.displayCutout());
SDLActivity.onNativeInsetsChanged(combined.left, combined.right, combined.top, combined.bottom);
}
// Pass these to any child views in case they need them
return insets;
}
// Key events
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
return SDLActivity.handleKeyEvent(v, keyCode, event, null);
}
private float getNormalizedX(float x)
{
if (mWidth <= 1) {
return 0.5f;
} else {
return (x / (mWidth - 1));
}
}
private float getNormalizedY(float y)
{
if (mHeight <= 1) {
return 0.5f;
} else {
return (y / (mHeight - 1));
}
}
// Touch events
@Override
public boolean onTouch(View v, MotionEvent event) {
/* Ref: http://developer.android.com/training/gestures/multi.html */
int touchDevId = event.getDeviceId();
final int pointerCount = event.getPointerCount();
int action = event.getActionMasked();
int pointerId;
int i = 0;
float x,y,p;
if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN)
i = event.getActionIndex();
do {
int toolType = event.getToolType(i);
if (toolType == MotionEvent.TOOL_TYPE_MOUSE) {
int buttonState = event.getButtonState();
boolean relative = false;
// We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
// if we are. We'll leverage our existing mouse motion listener
SDLGenericMotionListener_API14 motionListener = SDLActivity.getMotionListener();
x = motionListener.getEventX(event, i);
y = motionListener.getEventY(event, i);
relative = motionListener.inRelativeMode();
SDLActivity.onNativeMouse(buttonState, action, x, y, relative);
} else if (toolType == MotionEvent.TOOL_TYPE_STYLUS || toolType == MotionEvent.TOOL_TYPE_ERASER) {
pointerId = event.getPointerId(i);
x = event.getX(i);
y = event.getY(i);
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
// BUTTON_STYLUS_PRIMARY is 2^5, so shift by 4, and apply SDL_PEN_INPUT_DOWN/SDL_PEN_INPUT_ERASER_TIP
int buttonState = (event.getButtonState() >> 4) | (1 << (toolType == MotionEvent.TOOL_TYPE_STYLUS ? 0 : 30));
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
buttonState |= 0x08;
}
SDLActivity.onNativePen(pointerId, SDLActivity.getMotionListener().getPenDeviceType(event.getDevice()), buttonState, action, x, y, p);
} else { // MotionEvent.TOOL_TYPE_FINGER or MotionEvent.TOOL_TYPE_UNKNOWN
pointerId = event.getPointerId(i);
x = getNormalizedX(event.getX(i));
y = getNormalizedY(event.getY(i));
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
SDLActivity.onNativeTouch(touchDevId, pointerId, action, x, y, p);
}
// Non-primary up/down
if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN)
break;
} while (++i < pointerCount);
scaleGestureDetector.onTouchEvent(event);
return true;
}
// Sensor events
protected void enableSensor(int sensortype, boolean enabled) {
// TODO: This uses getDefaultSensor - what if we have >1 accels?
if (enabled) {
mSensorManager.registerListener(this,
mSensorManager.getDefaultSensor(sensortype),
SensorManager.SENSOR_DELAY_GAME, null);
} else {
mSensorManager.unregisterListener(this,
mSensorManager.getDefaultSensor(sensortype));
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// TODO
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
// Since we may have an orientation set, we won't receive onConfigurationChanged events.
// We thus should check here.
int newRotation;
float x, y;
switch (mDisplay.getRotation()) {
case Surface.ROTATION_0:
default:
x = event.values[0];
y = event.values[1];
newRotation = 0;
break;
case Surface.ROTATION_90:
x = -event.values[1];
y = event.values[0];
newRotation = 90;
break;
case Surface.ROTATION_180:
x = -event.values[0];
y = -event.values[1];
newRotation = 180;
break;
case Surface.ROTATION_270:
x = event.values[1];
y = -event.values[0];
newRotation = 270;
break;
}
if (newRotation != SDLActivity.mCurrentRotation) {
SDLActivity.mCurrentRotation = newRotation;
SDLActivity.onNativeRotationChanged(newRotation);
}
SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
y / SensorManager.GRAVITY_EARTH,
event.values[2] / SensorManager.GRAVITY_EARTH);
}
}
// Prevent android internal NullPointerException (https://github.com/libsdl-org/SDL/issues/13306)
@Override
public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
try {
return super.onResolvePointerIcon(event, pointerIndex);
} catch (NullPointerException e) {
return null;
}
}
// Captured pointer events for API 26.
@Override
public boolean onCapturedPointerEvent(MotionEvent event)
{
int action = event.getActionMasked();
int pointerCount = event.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
float x, y;
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, i);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, i);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
case MotionEvent.ACTION_MOVE:
x = event.getX(i);
y = event.getY(i);
SDLActivity.onNativeMouse(0, action, x, y, true);
return true;
case MotionEvent.ACTION_BUTTON_PRESS:
case MotionEvent.ACTION_BUTTON_RELEASE:
// Change our action value to what SDL's code expects.
if (action == MotionEvent.ACTION_BUTTON_PRESS) {
action = MotionEvent.ACTION_DOWN;
} else { /* MotionEvent.ACTION_BUTTON_RELEASE */
action = MotionEvent.ACTION_UP;
}
x = event.getX(i);
y = event.getY(i);
int button = event.getButtonState();
SDLActivity.onNativeMouse(button, action, x, y, true);
return true;
}
}
return false;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scale = detector.getScaleFactor();
SDLActivity.onNativePinchUpdate(scale);
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
SDLActivity.onNativePinchStart();
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
SDLActivity.onNativePinchEnd();
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Dusk</string>
</resources>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<game-mode-config
xmlns:android="http://schemas.android.com/apk/res/android"
android:supportsBatteryGameMode="true"
android:supportsPerformanceGameMode="true"
/>
+3
View File
@@ -0,0 +1,3 @@
plugins {
id 'com.android.application' version '8.13.2' apply false
}
+2
View File
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
android.useAndroidX=true

Some files were not shown because too many files have changed in this diff Show More