Compare commits

...

260 Commits

Author SHA1 Message Date
Laura K b572fdfd47
Fix dead Discord CDN urls [ci skip] 2024-04-27 20:00:37 +02:00
Laura K 94b9f930ce
ci: fix windows targets 2024-04-27 00:24:32 +02:00
莯凛 01ec93dd27
support custom text encoding for `.tsc` and stage table (#259)
* feat: support optional custom encoding
* feat: support custom encoding for stage table
2024-04-10 13:06:14 +03:00
biroder ca5361cc58 Add panic logging 2024-03-25 16:06:47 +00:00
poly000 08f086bfc4
localize difficulty name in save menu (#263) 2024-03-25 11:41:21 +00:00
poly000 1f288e2a39
replace `impl Clone` with `#[derive(Clone)]` (#262) [ci skip] 2024-03-25 11:04:13 +00:00
poly000 0e0cd66564
ci: migrate to node 20 actions (#261) 2024-03-24 14:58:32 +02:00
József Sallai 9c95b20f5c use localized soundtrack names 2024-03-24 00:57:56 +02:00
Alula 7630a9b60e
Update gamecontrollerdb 2024-03-11 07:57:07 +01:00
Alula ae909878c4
Parse machine code to determine vanilla stage table offset 2024-03-08 02:46:21 +01:00
Alula 7785513e8b
Fix inaccuracy: bloody water droplets 2024-03-08 01:44:30 +01:00
Alula 7f33f7b6c8
Support for loading 0.9.x.x beta saves 2024-03-08 01:39:48 +01:00
József Sallai 6c9b4d9a54
fix minor grammar mistake in readme 2024-03-04 00:12:12 +02:00
Edward Stuckey af9947b931 Fix ORG sampling bug
Also included the full org2 wavetable.
2024-02-29 19:54:09 +01:00
Laura K b275816e76
Update README.md [ci skip] 2024-02-23 11:44:25 +01:00
Laura K 8e3ccea8a1
Add font credits [ci skip] 2024-02-19 22:21:21 +01:00
biroder 76ec4771b3 Add local.properties file [ci skip] 2024-02-09 21:07:29 +00:00
biroder 3b77cdf0c5 Change the search directory for Lua scripts back to data dir[ci skip] 2024-01-26 10:30:08 +02:00
biroder 397cd8f584 Make some changes to the Android port
- Add application category and description
- Hide system navigation bar
- Change documents provider name to application name
- Disable multi-window mode support because touch controls don't work in this mode
2024-01-16 12:17:52 +00:00
biroder 21c255efb4
Make discord-rich-presence an optional dependency [ci skip] 2024-01-09 11:58:05 +00:00
Sobakin e1fb118910
Added cutscene auto skip option (#249) 2024-01-09 11:17:08 +00:00
Laura K c56bd2e8ae
Add EGS warning + some cleanup [ci skip] 2023-12-29 03:22:00 +01:00
periwinkle e09fbf5c63 Make all occurrences of Stage::change_tile spawn an accurate amount of smoke
Wouldn't it be better to have change_tile itself make the smoke?
2023-12-29 03:08:46 +01:00
biroder c4cffd54a8
Update sdl2 2023-12-28 17:12:38 +00:00
biroder 99f13a746e
Dummy change to trigger CI build 2023-12-27 03:07:33 +02:00
biroder 690c97e44d
Add nightly builds link for Android 2023-12-26 20:37:22 +02:00
biroder 311ca8b12f
Update ci.yml [ci skip] 2023-12-14 20:04:44 +02:00
biroder 152e31966a
Update ci.yml
This will need to be fixed later
2023-12-14 19:43:30 +02:00
biroder b4ede5bcad
Enable Android nightly builds 2023-12-14 19:18:44 +02:00
biroder a5f49c07e4 Fix #241 2023-12-08 15:07:54 +00:00
biroder fc66b84d8f
Add commit sha to nightly builds metadata 2023-11-27 09:13:14 +00:00
biroder 87cc7f12e3
Fix nightly build links [ci skip] 2023-11-26 15:56:49 +02:00
biroder e9cf548cc2 Add builds metadata saving
And remove the `pkg-config` dependency installation for Linux builds.
2023-11-26 15:31:09 +02:00
biroder 3af1d61e5b
Fix CI 2023-11-12 12:34:38 +02:00
Edward Stuckey 3b14adf949
Fix Player gun desync bug and inventory inconsistencies (#245)
* add inventory and player render fixes
2023-11-12 10:02:59 +00:00
biroder 46710dd31a Make some changes to README and CI config[ci skip]
Change links to nightly builds in README.

Change the version of nightly builds (use the build number at the end of the version string for master and part of the commit hash for other branches). Make it so that the workflow can be run manually. Disable cache saving for non-master branches, because in this case the cache is always created new and doesn't update the existing one, which wastes cache space. Temporarily disable build for Android
2023-11-11 00:38:47 +02:00
biroder 10e4d3efff
Change build number 2023-11-06 16:14:48 +00:00
biroder 2b1411787b Switch to Github Actions and add Android nightly builds 2023-11-06 17:48:00 +00:00
biroder 0e33bcaaf9
Prevent "Missing data files" message box from closing 2023-11-02 13:11:36 +00:00
biroder 45443dfa23 Fix #221 2023-10-30 09:35:35 +00:00
biroder 06ae269b7b Implement "Open game/user data directory" menus on Android[ci skip] 2023-10-29 17:17:01 +00:00
periwinkle 12a305becb
Accuracy fixes for misc NPCs (#242) 2023-10-06 13:14:05 +03:00
periwinkle bd203cfddb
Fix Misery bubble going the wrong way (oops sorry) (#240) 2023-09-28 11:18:51 +03:00
periwinkle c2ad3dd643
Accuracy fixes for Waterway through Hell (#237)
* Accuracy fixes for Waterway through Balcony

* Hell accuracy fixes
2023-09-27 22:14:26 -04:00
biroder 3468bcf5fd Disable debug hotkeys in non-debug mode, fix Balrog text scrolling sound from #165 2023-09-25 14:30:12 +03:00
biroder 21221d80e7 Fix #233 2023-09-03 11:40:00 +03:00
József Sallai a45c630116 add compact jukebox for non-switch CS+ (#232) 2023-08-23 02:50:03 +03:00
biroder 73295a6351 <FMU opcode fix 2023-08-10 13:39:40 +03:00
biroder ab87862646
Fix #222 [ci skip]
In general this isn't a bug or inaccuracy, because in the vanilla it's normal behaviour. But since most of the pause menu entries don't working during the intro, this had to be fixed anyway
2023-07-14 18:15:21 +03:00
József Sallai 5f24ee52b0 make strafing toggleable (closes #220) 2023-07-11 10:20:13 +03:00
biroder 02e1763e1d Fix a couple of warnings[ci skip] 2023-07-05 11:58:54 +03:00
biroder 44c6af2146
Add guide to setting up game data on Android 2023-07-05 11:37:58 +03:00
periwinkle d1d008edda Balfrog accuracy fixes 2023-07-04 08:39:58 +02:00
periwinkle 4d7dfd0266 Accuracy fixes for First Cave through Grasstown 2023-07-04 08:39:58 +02:00
biroder b70f0007b1
add japanese translation of display touch controls option[ci skip] 2023-06-28 14:39:18 +03:00
biroder f7148edd96 This implementation is better[ci skip] 2023-06-28 12:15:54 +03:00
biroder 425a26b3a0 Add display touch controls option[ci skip] 2023-06-28 11:37:05 +03:00
periwinkle f6caffd624
Accuracy fixes for Sand Zone and Labyrinth (#215)
* Accuracy fixes for Sand Zone and Labyrinth
And a couple of smaller things as well

* Remove NPCList::remove_by_type (kill_npcs_by_type does the same thing)
Also rename remove_by_event to kill_npcs_by_event for consistency
2023-06-22 17:26:51 +03:00
biroder b294f65656
fix of fix [ci skip] 2023-06-21 12:15:08 +03:00
karnak a5ed1a370e Fix fullscreen mode on glutin backend[ci skip] 2023-06-21 10:54:52 +03:00
karnak f07228895c Add some tsc settings to the Lua API[ci skip] 2023-06-19 16:05:24 +03:00
periwinkle b3007c10e3 Better number popup behavior (fixes #163) 2023-06-17 15:04:26 +02:00
periwinkle 7caff84e04
Fix LOTS of bugs and inaccuracies (#213)
- Player vs. soft solid NPCs used the wrong collision size
- Curly w/ Nemesis attached too high above the player when standing facing up
- Butes from Bute spawners started following the player at the wrong conditions
- Small pignons in Cemetery never turned around
- Refill stations spawned with alt direction didn't spawn with upward velocity
- Misery blocks spawned with glitchy framerects
- Various issues with possessed Misery
- Various issues with possessed Sue
- Ballos (phase 1) didn't face the player at certain points
- Ballos orbiting eyes (phase 4) didn't spawn smoke when killed
- A part of Ballos phase 2 had the wrong collision type
  (apparently this was intentional due to a bug with soft solid collision;
  hopefully that's also fixed by these changes?)
- A few missing sound effects during the Ballos fight
- Green devil generators weren't spawning enough enemies
- Balrog missile trails were in the wrong direction
- Core blade projectiles had the wrong check for water collision
- Monster X fish missiles moved at the wrong initial angle
- Sisters fireball attacks shot in the wrong direction
- Sisters missing smoke when shot
- Undead Core missing smoke in some cases
- <DNA'd NPCs spawned the wrong amount of smoke
  (fixes Heavy Press lightning despawn for the speedrun ;) )
- Player jumping and falling animations were flipped
- Implemented <MYDxxxx facing towards entity xxxx if xxxx >= 10
  (also <MYD0003 should not set the player's direction to down)
- <TRA resets invincibility in freeware (due to starting a new event)
- Bubbler level 1 bullets spawned at the wrong offset
- Bubbler and Machine Gun autofire timers shouldn't reset when fire button is pressed
- Fixed level 3 missile and Super Missile spawn offsets and trajectories
- Nemesis carets spawned at wrong offsets
- Various small typos/logic errors
2023-06-16 12:51:22 +03:00
Alula dcdb4e9d39
Fix <FOB emitting wrong code 2023-06-15 21:24:14 +02:00
biroder 596c7b8aff Lua api fixes[ci skip] 2023-06-15 13:01:09 +03:00
biroder ed2c5f510a Fixed game crash when disabing Discord Rich Presense setting and Discord isn't running. Fixed crash o exit when Discord isn't running 2023-05-30 12:37:41 +03:00
biroder 0ba5aad8af Merge branch 'master' of https://github.com/doukutsu-rs/doukutsu-rs 2023-05-30 11:35:07 +03:00
biroder 50ff888141 Fix #208 and add a different log level for file logging 2023-05-29 10:42:34 +03:00
biroder 72e74e2113 Merge branch 'master' of https://github.com/doukutsu-rs/doukutsu-rs 2023-05-29 10:38:28 +03:00
biroder 47a43467d0 Fix #208 and add different log level for file logging 2023-05-29 10:32:27 +03:00
biroder 19e0da519f
Fix of.fix(again) [ci skip] 2023-05-24 15:04:56 +03:00
biroder 721a0c907a Fix og logo offset 2023-05-24 13:59:50 +03:00
biroder 2a3341e3c5 Update android port strings 2023-05-23 10:10:12 +03:00
biroder 63f903104a Fix of #192 2023-05-16 17:00:17 +03:00
biroder 277f5d957b Add logging to file 2023-05-11 17:40:11 +03:00
biroder 9409bb35fe
The final change to the save point smoke [ci skip]
Hope this is the last change:/
2023-04-30 12:21:49 +03:00
biroder 9e09d56b76
Some changes to creating smoke when save point spawned in a shelter 2023-04-26 18:22:09 +03:00
biroder 40767c021b Fix innacuracy [ci skip] 2023-04-26 12:07:20 +03:00
biroder 0e164a44df
Revert changes [ci skip] 2023-04-23 15:04:41 +03:00
biroder 38338ae551
let's try again 2023-04-23 14:22:37 +03:00
biroder dbb72ff6c1
yet another fix 2023-04-23 14:12:07 +03:00
biroder 0a45332f3f
fix 2023-04-23 12:45:08 +03:00
biroder aa27680baf
Trying to add android CI builds 2023-04-23 12:42:45 +03:00
biroder 67fb32499f Partial fixes of bosses 2023-04-17 16:51:52 +03:00
biroder 5821f06928
Dehardcode menu toogles text [ci skip] 2023-04-14 16:39:16 +03:00
biroder 5f93658f0f
Fix of fix textscript encoding [ci skip] 2023-04-13 18:02:41 +03:00
biroder cd671cce48 * i18n improving for Android [ci skip] 2023-04-13 17:10:27 +03:00
biroder 2a162d948f
Fix textscript encoding 2023-04-12 21:36:32 +03:00
biroder 72a2ada0b2
Minor refactoring of TSC decryption [ci skip] 2023-04-10 16:23:46 +03:00
biroder 0ccbbefbed
Fix `skip_commits` rule in CI config [ci skip] 2023-04-08 11:46:13 +03:00
biroder bc91d9f366 Minor icon change 2023-04-03 17:35:22 +03:00
Alula beb290dafb
no more april fools 2023-04-02 11:52:32 +02:00
biroder 5f7bae8f4f
Fix a linker error on Windows 2023-04-02 11:52:12 +03:00
biroder 11eafb8d03
Add window icons for non-Windows systems (#206) 2023-04-01 21:18:25 +03:00
biroder f99b452073
Fix sound volume change and inventory overflow on Android (#207)
* Android changing volume fix. Added arrows to indicate volume buttons

* Fix inventory overflowing on Android
2023-04-01 20:27:30 +03:00
Alula 51834be404
yeah 2023-04-01 13:56:52 +02:00
Alula b2fb548584
april fools 2023-04-01 13:53:52 +02:00
biroder 1633eef3dd
Skip appveyor builds for android and horizon (#205)
Builds for Android and Horizon isn't currently configured, so starting the build after changing these files is useless
2023-03-25 12:42:44 +02:00
biroder e57c9cdb27
Minor editor fixes (#204) 2023-03-24 12:52:55 +02:00
biroder 07b1550cf4 Revoke remove of kotlin builtins
They may be used by one of dependencies, so maybe it's better to leave them
2023-03-17 13:22:33 +01:00
biroder a15fd4d190 Remove useless files from output android build 2023-03-17 13:22:33 +01:00
biroder b8af9aed25 Fix display of picked up expirience when player took damage 2023-03-15 15:15:56 +01:00
biroder 8992889e94 Maybe this implementation would be better 2023-03-15 15:15:56 +01:00
biroder f91e793062 Add damage amount display when taking fatal damage
Fix doukutsu-rs/doukutsu-rs#163
2023-03-15 15:15:56 +01:00
biroder 3fbe94ecd1 Remove use of MakePortable enrty on unsupported targets (#201)
`MakePortable` not supported on `android` and `horizon` targets
2023-03-03 20:28:43 +02:00
biroder 5bd0dcd564
DocumentsProvider refreshing after changing files (#197)
* DocumentsProvider refreshing after changing files

* Add a flags for notifyChange function
2023-03-03 17:46:00 +02:00
József Sallai 41b840d13f reset game difficulty on title screen (fixes #196) 2023-02-25 13:45:36 +02:00
József Sallai 95e09ded99 fix game crashing when discord isn't open (lol) 2023-02-24 13:42:47 +02:00
József Sallai 6bccf59f5b improve quick discord RPC updates and prevent crashes 2023-02-23 15:23:59 +02:00
József Sallai 890c0596ed add portable user directory setting 2023-02-23 14:27:53 +02:00
József Sallai b22ca8b35e add difficulty icons to discord rich presence 2023-02-18 22:13:14 +02:00
József Sallai e77241cd56 improve command line appearance and autofocus 2023-02-18 21:05:30 +02:00
József Sallai 3dbe56690a add experimental discord rich presence support 2023-02-18 20:42:00 +02:00
József Sallai 1810bf6d5b fix missing `as_any` methods in the glutin backend 2023-02-17 16:06:11 +02:00
József Sallai d6715bccea add menu options for opening user and data dirs 2023-02-17 16:05:38 +02:00
alula 30c85c2f8b
Merge pull request #193 from biroder/patch-1 2023-02-07 09:16:34 +01:00
biroder 979909faa8
Fix 'render_opengl' import
Import 'render_opengl' only when feature 'render-opengl' is enabled
2023-02-06 10:23:39 +02:00
Alula 90df8faa7a
fix a nasty type confusion bug in SDL backend 2023-01-30 23:18:16 +01:00
Daedliy 6d3c127912
Minor missing texture rewording (#190)
* Minor missing texture rewording

* ababa

---------

Co-authored-by: Daedliy <missari.bb@gmail.com>
2023-01-29 19:40:42 +02:00
Alula 515a0a7fe7
Well, there's something to see there now... [ci skip] 2023-01-28 00:47:50 +01:00
Alula 7fe96780de
oops I pushed and forgot to save [ci skip] 2023-01-28 00:44:13 +01:00
Alula f1e5d11b2a
Switch to 0.101 and introduce new versioning scheme 2023-01-28 00:42:56 +01:00
Alula 3739e9f170
do not hardcode x86_64 as arch name on windows 2023-01-28 00:41:07 +01:00
József Sallai 3c79582fed bump webbrowser 2023-01-26 17:42:16 +02:00
József Sallai a187942cff remove 2P options from pause menu on android 2023-01-26 17:34:35 +02:00
József Sallai 4ae085f7e2 add engine constant flags for difficulty and 2P menus 2023-01-26 17:29:53 +02:00
József Sallai 153b1b4962 fix P2 skin OOB error 2023-01-26 16:56:13 +02:00
Alula 2774eae66f
revert [ci skip] 2023-01-25 20:46:16 +01:00
Alula 4928ce4682
set version to 0.100.0-beta5 for just this commit for gh release builds 2023-01-25 20:45:34 +01:00
Alula 64290ae5a3
h 2023-01-25 20:08:20 +01:00
Alula 1457aa3caa
preserve cache 2023-01-25 19:52:24 +01:00
Alula ee58c1c8af
revert [ci skip] 2023-01-25 19:40:48 +01:00
Alula a5bb130e73
this is stupid as fuck but I don't want to remake the release once again 2023-01-25 19:40:19 +01:00
Alula bbd20a003b
bruh [ci skip] 2023-01-25 19:38:21 +01:00
Alula ef1c2a5930
hotfix time 2023-01-25 18:23:15 +01:00
Alula 074af609bc
fix syntax error [ci skip] 2023-01-25 15:53:40 +01:00
Alula efa8a47b8d
use tag for CI builds if present [ci skip] 2023-01-25 15:48:39 +01:00
Alula bc3616d073
android + hos fixes 2023-01-25 15:25:51 +01:00
Devon W 510442490e
Map System Equip fix (#185)
Changing map key enable from hardcoded to item 2 to the actual vanilla behavior of checking for equip flag.
2023-01-23 17:51:40 -05:00
József Sallai 0a7fd4dc47 add win32 build to readme [skip ci] 2023-01-23 13:27:29 +02:00
József Sallai 4fa7069e82 fix 32-bit windows builds 2023-01-22 21:13:19 +02:00
József Sallai 212d7b915b don't try to render fps counter on no data scene 2023-01-22 20:27:46 +02:00
József Sallai cb95db1e89 fix text wrapping on no data scene 2023-01-22 19:42:40 +02:00
József Sallai 334e64f499
kurwa mać 2023-01-22 18:19:13 +02:00
József Sallai 90e58649a7
fuck yaml 2023-01-22 18:06:24 +02:00
Alula e845c87738
let's see if win32 builds work 2023-01-22 16:19:26 +01:00
alula 2cb36de715
Merge pull request #183 from doukutsu-rs/horizon-os
merge Horizon branch with portability fixes
2023-01-18 00:12:48 +01:00
alula c2a8bf52e9
Merge branch 'master' into horizon-os 2023-01-17 17:57:55 +01:00
József Sallai e975a75ec4 sue 2023-01-06 10:01:45 +02:00
Alula 5854735392
i forgor to push 2023-01-01 17:36:16 +01:00
József Sallai 356f4230b5 refactor co-op skins 2022-12-23 00:26:46 +02:00
József Sallai 4be3dd518b add basic support for switch P2 skins 2022-12-22 22:59:24 +02:00
József Sallai 5ed2d40e23 rumble failure shouldn't crash game 2022-12-18 19:07:37 +02:00
alula a246d1a2f9
Switch homebrew port notes 2022-12-01 15:20:02 +01:00
Alula e567dd6296
Use correct branch for cpal 2022-12-01 14:41:57 +01:00
Alula 066389a29f
thanks dawn 2022-12-01 14:39:07 +01:00
Alula 67979a03ea
Mostly-working Horizon port 2022-12-01 14:30:59 +01:00
Alula f91513edd2
small oops 2022-11-21 15:16:57 +01:00
Alula 5d92cafe67
Initial Horizon OS bringup 2022-11-21 15:15:51 +01:00
Alula d87bbf2b46 abstract gamepad away from SDL 2022-11-21 15:13:46 +01:00
Alula 2860938b9a
abstract gamepad away from SDL 2022-11-21 15:12:45 +01:00
Alula e74b586dd1
Font rendering refactor 2022-11-20 20:38:49 +01:00
alula 0fca898c54
Update README.md 2022-11-20 10:19:01 +01:00
Alula df467e8764
gitignore moment 2022-11-19 18:27:28 +01:00
Alula 17e1156850
Refactoring time 2022-11-19 18:20:03 +01:00
Alula 9fd04ed47a
remove this stupid feature 2022-11-19 12:21:10 +01:00
Alula c9e6dd7181
add teleport_player command and use if let() instead of unwrapping 2022-11-19 12:10:02 +01:00
dawnDus 0330cf3b2b
Fix MYB bumping away from NPC when direction >4 2022-11-12 17:11:03 -05:00
dawnDus 2be1c422d6
Adjust horizontal booster caret (Fixes #173) 2022-11-03 20:29:24 -04:00
Awesomegamer6566 0f79474aae Fix downward facing fans pointing upwards 2022-11-01 22:42:52 +01:00
dawnDus 713e704b9b
Fix seasonal mychar offsets 2022-10-26 17:04:06 -04:00
dawnDus d42632f973
Fix Curly NPC 2022-10-21 12:07:43 -04:00
dawnDus 57b2be5211
Fix stuck popups with Spur 2022-10-17 17:40:30 -04:00
Alula 3a756e0ac4
abuse ASLR for RNG seeding 2022-10-15 14:13:04 +02:00
József Sallai 6607d2fc15 play get item sfx on AM+ 2022-10-10 10:44:51 +03:00
József Sallai 8ebe210105 play get item sound on <I+N 2022-10-06 19:31:10 +03:00
József Sallai f0949f49cf fix a few NPC inaccuracies from #165 2022-09-24 23:27:27 +03:00
dawnDus 2b9a0198cb
Safely get current stage name for Save Menu 2022-09-17 18:05:10 -04:00
József Sallai 8684dd8448 some runtime optimizations 2022-09-17 13:57:37 +03:00
Awesomegamer6566 72c268647f
Fix changing BGM Interpolation setting with left and right keys (#161)
* Fix changing BGM Interpolation setting with left and right keys

* Actually fix changing BGM Interpolation Setting with left and right keys
2022-09-16 22:08:19 +03:00
Daedliy e9d2099f42
Add Links Menu (#159)
* Add Links Menu

* Suggested Changes
2022-08-29 00:06:58 +03:00
Sallai József f7d635a3d7 add switch boss health bar 2022-08-28 16:17:54 +03:00
Sallai József 029d6d52e4 add/drop player 2 from pause menu 2022-08-28 15:00:53 +03:00
Sallai József dec913dd65 make vanilla extractor input and output configurable 2022-08-28 14:07:01 +03:00
Sallai József 028c60157d make localization system dynamic 2022-08-26 03:17:45 +03:00
dawnDus 3d86995feb
Add fast-forward cutscene skip option 2022-08-23 18:27:39 -04:00
Sallai József 670e6891c1 run arbitrary TSC from debug command line 2022-08-22 03:08:57 +03:00
dawnDus a25dc297ef
Add spawn_npc command 2022-08-21 18:32:51 -04:00
Sallai József 4ed7ba66b8 add in-game debug command line 2022-08-22 01:10:33 +03:00
Sallai József b1b3b131e2 process debug keys in game scene and add quick save shortcut 2022-08-21 22:06:06 +03:00
Sallai József f1b3c680c9 throttle number popup on NPC damage 2022-08-21 18:57:01 +03:00
Sallai József ca1fa7b7c0 fix egg corridor inaccuracies (fixes #155) 2022-08-21 13:20:04 +03:00
dawnDus 3cc9d75681
Fix spur damage inaccuracy 2022-08-20 21:22:45 -04:00
dawnDus 92bc887663
Fixed order of operations for bullets (Fixes #131) 2022-08-20 18:43:48 -04:00
Sallai József 59da01b7b9 even more rust
sprites by @RedCoder09 and @Krunchy0920
2022-08-20 22:08:15 +03:00
Sallai József e07207b40c more rust
crab headband sprites by @RedCoder09
2022-08-16 14:47:11 +03:00
Daedliy 1688f4bcbc
Proper wiiware DEMO support (#153)
* Fixed Crashing

* Add Demo Splash Rect + Fixes

* FIX newgame pos (Joe)
2022-08-14 18:36:50 +03:00
Sallai József 8c70e1a13d fix missing muscle doctor action and sue teleport sound 2022-08-14 16:49:11 +03:00
Sallai József b1d578c0b4 record last challenge attempts 2022-08-14 14:19:46 +03:00
Sallai József 290068dd37 add gamepad rumble 2022-08-13 17:54:05 +03:00
Sallai József 7b359ae4c1 main menu improvements 2022-08-07 22:54:23 +03:00
Sallai József 56631201c8 fix igor inaccuracies (fixes #132) 2022-08-06 20:41:18 +03:00
dawnDus 81755d4ad9
Require control to look up/down while falling 2022-08-06 12:44:14 -04:00
Sallai József f1542246c6 make curly's weapon respect light cone setting (fixes #109) 2022-08-06 17:06:17 +03:00
Sallai József b29c375a7a fix broken new game menu after deleting save 2022-08-06 14:47:02 +03:00
Sallai József bfd9c8c343 oops 2022-08-06 14:10:21 +03:00
Sallai József 1883045f75 make controller selection its own menu 2022-08-06 14:07:00 +03:00
Sallai József 4cfbcc50ac add gamecontrollerdb, update default inputs, add reset controls option 2022-08-01 03:18:43 +03:00
Sallai József f74ec19cb5 make menu ok/back remappable and fix jump/shoot rebind bug (#150) 2022-07-31 14:50:46 +03:00
Sallai József c68fedaa50 fix menu controller swap issues and handle jump/shoot rebind edge case 2022-07-31 03:00:19 +03:00
Sallai József 8a4201f381 fix some controls menu bugs 2022-07-31 02:16:34 +03:00
Sallai József ffaf12cca8 add controls settings menu 2022-07-30 23:20:53 +03:00
Sallai József 03e9c9db0c make menus scrollable 2022-07-30 15:01:22 +03:00
dawnDus c9ba05c948
Ballos inaccuracies (Fixes #149) 2022-07-27 21:27:26 -04:00
Sallai József 914555eac0 don't disable keyboard input when using gamepad 2022-07-27 14:14:05 +03:00
Sallai József fee63f2600 make pausing on focus loss toggleable 2022-07-25 11:41:20 +03:00
alula 2a792db797
Update README.md 2022-07-25 06:09:22 +02:00
Sallai József a598f31716 don't pause the game loop when the window loses focus 2022-07-24 19:38:39 +03:00
Sallai József ef040a393c refactor menus to use enums instead of indices 2022-07-24 17:52:51 +03:00
Sallai József 4a6b2c4400 add gamepad jukebox indicators 2022-07-23 23:29:58 +03:00
Sallai József 398f610c09 skip after Y/N prompts 2022-07-23 20:55:58 +03:00
Sallai József fb4ac0dae8 add gamepad input icons 2022-07-23 18:45:08 +03:00
Sallai József 0415f917f8 render = as white circle (closes #110) 2022-07-23 14:57:40 +03:00
Sallai József 6f95e6109c add methods for rendering text with sprite rects 2022-07-23 13:59:13 +03:00
dawnDus cdd3a37754
Fixed snack tile rendering (Fixes #147) 2022-07-22 17:32:16 -04:00
József Sallai f00f35ca2c
actually bump the version number 2022-07-21 16:49:45 +03:00
Sallai József eee0f9eff9 bump settings version to fix skip keybinding and gamepad mappings 2022-07-21 14:38:44 +03:00
Sallai József 84d9dbf877 remove gilrs dependency for controller support 2022-07-21 03:19:23 +03:00
Sallai József 9932b1209f very initial gamepad support 2022-07-20 16:07:24 +03:00
dawnDus 6d8e58090a
Allow switch data to use original portraits 2022-07-17 22:37:35 -04:00
Sallai József 93e567981e teleport second player when off-screen 2022-07-18 01:57:49 +03:00
József Sallai 7693a4ff20
Merge pull request #146 from Daedliy/patch-1
re-add wiiware extraction guide
2022-07-17 21:31:46 +03:00
Daedliy 86f89e7522
re-add wiiware extraction guide
using the more complete one from the CSMC
2022-07-17 14:48:02 -03:00
Sallai József 3297b151ad update extraction guide link on android 2022-07-17 17:57:33 +03:00
Sallai József aaecafcb72 stop shipping data files in appveyor builds and update readme 2022-07-17 17:38:35 +03:00
Sallai József 444539405a detect and extract vanilla exe resources 2022-07-16 15:33:31 +03:00
Sallai József 2177382b5a add fullscreen toggle 2022-07-13 17:03:17 +03:00
Sallai József a1d0f2dc63 fix 2P behavior in TSC commands and ironhead fight 2022-07-09 20:01:31 +03:00
Sallai József 75b077c772 add screen shake intensity setting (closes #129) 2022-07-09 18:06:22 +03:00
Sallai József d8636bc693 refactor sound manager to prevent audio crashes (fixes #112) 2022-07-09 16:49:56 +03:00
József Sallai 9d7c63571d
Merge pull request #145 from jozsefsallai/bugfix/filesystem-errors
fix filesystem errors and UB warnings on rust >= 1.62.0
2022-07-07 17:02:18 +03:00
Sallai József 3faf99b535 fix filesystem errors and UB warnings on rust >= 1.62.0 2022-07-07 16:32:33 +03:00
Sallai József 1440a91fba trigger appveyor rebuild 2022-07-07 12:21:35 +03:00
dawnDus 6d08eb716e
Add missing fireplace smoke 2022-06-30 11:40:24 -04:00
dawnDus 69fdc7d3d2
Fix warnings from #141 2022-06-30 11:39:56 -04:00
IruzzArcana 18a7670248
add coop menu (#141) 2022-06-22 18:08:36 -04:00
József Sallai d7face2544
fix zombie curly corpses not despawning (fixes #137) (#140) 2022-06-07 14:48:43 -04:00
dawnDus 987c857b1c
show missing mod.txt 2022-05-22 09:17:55 -04:00
dawnDus 3f8c66db0f
fix undead minicore rotation 2022-05-14 13:48:58 -04:00
dawnDus dc2476c9dd
fix volume rounding 2022-05-14 09:18:50 -04:00
dawnDus 8a2e9fa569
bugfixes for #120 and #122 2022-05-11 20:19:43 -04:00
dawndus 588b0d53dd
Update README.md 2022-05-10 18:31:57 -04:00
dawnDus 7c07986b5d
Minor rendering tweak [ci skip] 2022-05-02 19:34:16 -04:00
dawnDus 02a9cac305
Added Android cutscene skip 2022-05-02 18:54:57 -04:00
dawnDus af39130fed
undo android restriction 2022-05-02 16:36:27 -04:00
dawnDus 2d2e712eab
fix loading order 2022-04-30 11:54:48 -04:00
dawnDus acad65d233
enable cs+ japanese font when valid 2022-04-30 11:48:47 -04:00
Alula 24762a1c45
enable cs+ japanese font 2022-04-29 20:38:02 +02:00
dawnDus 75f5e9f364
Cutscene skip tutorial prompt 2022-04-26 23:23:55 -04:00
dawnDus 7e793e09a8
Bug Fixes 2022-04-26 23:23:14 -04:00
256 changed files with 20291 additions and 14798 deletions

View File

@ -1,8 +1,12 @@
version: "0.99.0.{build}-{branch}"
version: "0.101.0-{build}-{branch}"
skip_commits:
files:
- README.md
- LICENSE
- app/
- drsandroid/
- drshorizon/
environment:
global:
@ -11,12 +15,15 @@ environment:
- channel: stable
target: x86_64-pc-windows-msvc
target_name: win64
arch_name: x86_64
job_name: windows-x64
appveyor_build_worker_image: Visual Studio 2019
# - channel: stable
# target: i686-pc-windows-msvc
# target_name: win32
# job_name: windows-x32
- channel: stable
target: i686-pc-windows-msvc
target_name: win32
arch_name: i686
job_name: windows-x32
appveyor_build_worker_image: Visual Studio 2019
- channel: stable
target: x86_64-unknown-linux-gnu
target_name: linux
@ -52,25 +59,23 @@ for:
- cargo -vV
cache:
- '%USERPROFILE%\.cache -> Cargo.toml'
- '%USERPROFILE%\.cargo\bin -> Cargo.toml'
- '%USERPROFILE%\.cargo\registry\index -> Cargo.toml'
- '%USERPROFILE%\.cargo\registry\cache -> Cargo.toml'
- '%USERPROFILE%\.cargo\git\db -> Cargo.toml'
- '%USERPROFILE%\.rustup -> Cargo.toml'
- 'target -> Cargo.toml'
- '%USERPROFILE%\.cache'
- '%USERPROFILE%\.cargo\bin'
- '%USERPROFILE%\.cargo\registry\index'
- '%USERPROFILE%\.cargo\registry\cache'
- '%USERPROFILE%\.cargo\git\db'
- '%USERPROFILE%\.rustup'
- 'target'
build_script:
- set DRS_BUILD_VERSION_OVERRIDE=%APPVEYOR_BUILD_VERSION%
#- set DRS_BUILD_VERSION_OVERRIDE=%APPVEYOR_BUILD_VERSION%
- if "%APPVEYOR_REPO_TAG%" == "true" (set DRS_BUILD_VERSION_OVERRIDE=%APPVEYOR_REPO_TAG_NAME%) else (set DRS_BUILD_VERSION_OVERRIDE=%APPVEYOR_BUILD_VERSION%)
- set CARGO_INCREMENTAL=1
- cargo build --release --bin doukutsu-rs
- mkdir release
- copy LICENSE release\LICENSE
- copy target\release\doukutsu-rs.exe release\doukutsu-rs.x86_64.exe
- copy target\release\doukutsu-rs.exe release\doukutsu-rs.%arch_name%.exe
- cd release
- appveyor DownloadFile https://github.com/doukutsu-rs/game-data/archive/master.zip -FileName ../game-data.zip
- 7z x ../game-data.zip
- rename game-data-master data
- 7z a ../doukutsu-rs_%target_name%.zip *
- appveyor PushArtifact ../doukutsu-rs_%target_name%.zip
@ -78,7 +83,14 @@ for:
matrix:
only:
- appveyor_build_worker_image: macos-monterey
init:
- ps: |
if ($env:APPVEYOR_REPO_TAG -eq "true")
{
Update-AppveyorBuild -Version "$env:APPVEYOR_REPO_TAG_NAME"
}
install:
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -yv --default-toolchain $channel
- export PATH=$PATH:$HOME/.cargo/bin
@ -90,19 +102,17 @@ for:
- cargo install cargo-bundle --force
cache:
- '$HOME/.cache -> Cargo.toml'
- '$HOME/.cargo/bin -> Cargo.toml'
- '$HOME/.cargo/registry/index -> Cargo.toml'
- '$HOME/.cargo/registry/cache -> Cargo.toml'
- '$HOME/.cargo/git/db -> Cargo.toml'
- '$HOME/.rustup -> Cargo.toml'
- 'target -> Cargo.toml'
- '$HOME/.cache'
- '$HOME/.cargo/bin'
- '$HOME/.cargo/registry/index'
- '$HOME/.cargo/registry/cache'
- '$HOME/.cargo/git/db'
- '$HOME/.rustup'
- 'target'
build_script:
- export DRS_BUILD_VERSION_OVERRIDE=$APPVEYOR_BUILD_VERSION
- appveyor DownloadFile https://github.com/doukutsu-rs/game-data/archive/master.zip -FileName ../game-data.zip
- 7z x ../game-data.zip
- mv game-data-master data
#- export DRS_BUILD_VERSION_OVERRIDE=$APPVEYOR_BUILD_VERSION
- if [ "$APPVEYOR_REPO_TAG" = "true" ]; then export DRS_BUILD_VERSION_OVERRIDE=$APPVEYOR_REPO_TAG_NAME; else export DRS_BUILD_VERSION_OVERRIDE=$APPVEYOR_BUILD_VERSION; fi
- CARGO_INCREMENTAL=1 cargo bundle --release --target $target
- mkdir release
- cp LICENSE ./release/LICENSE
@ -127,23 +137,22 @@ for:
- cargo -vV
cache:
- '$HOME/.cache -> Cargo.toml'
- '$HOME/.cargo/bin -> Cargo.toml'
- '$HOME/.cargo/registry/index -> Cargo.toml'
- '$HOME/.cargo/registry/cache -> Cargo.toml'
- '$HOME/.cargo/git/db -> Cargo.toml'
- '$HOME/.rustup -> Cargo.toml'
- 'target -> Cargo.toml'
- '$HOME/.cache'
- '$HOME/.cargo/bin'
- '$HOME/.cargo/registry/index'
- '$HOME/.cargo/registry/cache'
- '$HOME/.cargo/git/db'
- '$HOME/.rustup'
- 'target'
build_script:
- export DRS_BUILD_VERSION_OVERRIDE=$APPVEYOR_BUILD_VERSION
#- export DRS_BUILD_VERSION_OVERRIDE=$APPVEYOR_BUILD_VERSION
- if [ "$APPVEYOR_REPO_TAG" = "true" ]; then export DRS_BUILD_VERSION_OVERRIDE=$APPVEYOR_REPO_TAG_NAME; else export DRS_BUILD_VERSION_OVERRIDE=$APPVEYOR_BUILD_VERSION; fi
- RUSTFLAGS="-C link-arg=-s" CARGO_INCREMENTAL=1 cargo build --release --bin doukutsu-rs
- mkdir release
- cp LICENSE ./release/LICENSE
- cp -a target/release/doukutsu-rs ./release/doukutsu-rs.x86_64.elf
- cd release
- appveyor DownloadFile https://github.com/doukutsu-rs/game-data/archive/master.zip -FileName ../game-data.zip
- 7z x ../game-data.zip
- mv game-data-master data
- 7z a ../doukutsu-rs_$target_name.zip *
- appveyor PushArtifact ../doukutsu-rs_$target_name.zip

258
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,258 @@
name: CI
on:
push:
branches-ignore:
- cpp-rewrite
- horizon-os
paths-ignore:
- '.gitignore'
- '.github/*'
- '**.md'
- 'LICENSE'
- 'drshorizon/**'
- 'res/**'
workflow_dispatch:
defaults:
run:
shell: bash
env:
VERSION: "0.101.0"
jobs:
build:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
include:
- name: Linux x86_64
os: ubuntu-latest
channel: stable
target: x86_64-unknown-linux-gnu
target_name: linux-x64
arch_name: x86_64
- name: Windows x64
os: windows-latest
channel: stable
target: x86_64-pc-windows-msvc
target_name: windows-x64
arch_name: x86_64
- name: Windows x32
os: windows-latest
channel: stable
target: i686-pc-windows-msvc
target_name: windows-x32
arch_name: i686
- name: macOS x64 (Intel Macs)
os: macos-latest
channel: stable
target: x86_64-apple-darwin
target_name: mac-x64
- name: macOS ARM64 (M1 Macs)
os: macos-latest
channel: stable
target: aarch64-apple-darwin
target_name: mac-arm64
steps:
- uses: actions/checkout@v4
- name: Install dependencies
if: ${{ matrix.os == 'ubuntu-latest' }}
run: sudo apt install libasound2-dev libudev-dev libgl1-mesa-dev
- name: Restore cache
uses: actions/cache/restore@v4
with:
path: |
~/.cargo
~/.rustup
target
key: ${{ matrix.target_name }}-cargo
- name: Setup rust toolchain
run: |
rustup default ${{ matrix.channel }}
rustup target add ${{ matrix.target }}
rustc -vV
cargo -vV
if [ "${{ runner.os }}" == "macOS" ]; then
cargo install cargo-bundle
fi
- name: Build
run: |
if [ "${{ github.ref_type }}" == "tag" ]; then
export DRS_BUILD_VERSION_OVERRIDE="{{ github.ref_name }}"
elif [ "${{ github.ref_name }}" == "master"]; then
export DRS_BUILD_VERSION_OVERRIDE="${{ env.VERSION }}-$((${{ github.run_number }} + 654))"
else
export DRS_BUILD_VERSION_OVERRIDE="${{ env.VERSION }}-${GITHUB_SHA:0:7}"
fi
mkdir release
cp LICENSE release
if [ "${{ runner.os }}" == "macOS" ]; then
CARGO_INCREMENTAL=1 cargo bundle --release --target ${{ matrix.target }}
cp -a ./target/${{ matrix.target }}/release/bundle/osx/doukutsu-rs.app release/doukutsu-rs.app
codesign -s - -f ./release/doukutsu-rs.app/Contents/MacOS/doukutsu-rs
elif [ "${{ runner.os }}" == "Windows" ]; then
CARGO_INCREMENTAL=1 cargo build --release --bin doukutsu-rs --target ${{ matrix.target }}
cp ./target/${{ matrix.target }}/release/doukutsu-rs.exe release/doukutsu-rs.${{ matrix.arch_name }}.exe
elif [ "${{ runner.os }}" == "Linux" ]; then
RUSTFLAGS="-C link-args=-s" CARGO_INCREMENTAL=1 cargo build --release --bin doukutsu-rs
cp -a ./target/release/doukutsu-rs release/doukutsu-rs.${{ matrix.arch_name }}.elf
fi
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: doukutsu-rs_${{ matrix.target_name }}
path: ./release/*
if-no-files-found: error
- name: Save cache
if: ${{ github.ref_name == 'master' || github.ref_type == 'tag' }}
uses: actions/cache/save@v4
with:
path: |
~/.cargo
~/.rustup
target
key: ${{ matrix.target_name }}-cargo
build_android:
name: Android build
runs-on: ubuntu-latest
env:
APP_OUTPUTS_DIR: "app/app/build/outputs/apk/release"
strategy:
fail-fast: true
steps:
- uses: actions/checkout@v4
- name: Restore cache
uses: actions/cache/restore@v4
with:
path: |
~/.cache
~/.cargo
~/.rustup
~/.gradle
app/app/.cxx
app/app/build
drsandroid/target
key: android-cargo
- name: Setup rust toolchain
run: |
rustup default stable
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android
rustc -vV
cargo -vV
cargo install cargo-ndk
- name: Build
run: |
if [ "${{ github.ref_type }}" == "tag" ]; then
export DRS_BUILD_VERSION_OVERRIDE="{{ github.ref_name }}"
elif [ "${{ github.ref_name }}" == "master"]; then
export DRS_BUILD_VERSION_OVERRIDE="${{ env.VERSION }}-$((${{ github.run_number }} + 654))"
else
export DRS_BUILD_VERSION_OVERRIDE="${{ env.VERSION }}-${GITHUB_SHA:0:7}"
fi
cd app
chmod +x ./gradlew
./gradlew assembleRelease
- name: Sign app
run: |
BUILD_TOOLS=$ANDROID_HOME/build-tools/33.0.0
echo "${{ secrets.ANDROID_SIGNING_KEYSTORE }}" | base64 --decode > keystore.jks
if [ "${{ secrets.ANDROID_SIGNING_KEY_PASS }}" != "" ]; then
$BUILD_TOOLS/apksigner sign --ks ./keystore.jks --ks-key-alias "${{ secrets.ANDROID_SIGNING_ALIAS }}" --ks-pass "pass:${{ secrets.ANDROID_SIGNING_KEYSTORE_PASS }}" --key-pass "pass:${{ secrets.ANDROID_SIGNING_KEY_PASS }}" --out $APP_OUTPUTS_DIR/app-signed.apk $APP_OUTPUTS_DIR/app-release-unsigned.apk
else
$BUILD_TOOLS/apksigner sign --ks ./keystore.jks --ks-key-alias "${{ secrets.ANDROID_SIGNING_ALIAS }}" --ks-pass "pass:${{ secrets.ANDROID_SIGNING_KEYSTORE_PASS }}" --out $APP_OUTPUTS_DIR/app-signed.apk $APP_OUTPUTS_DIR/app-release-unsigned.apk
fi
rm keystore.jks
- name: Prepare artifact
run: |
mkdir release
mv $APP_OUTPUTS_DIR/app-signed.apk release/doukutsu-rs.apk
cp LICENSE ./release
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: doukutsu-rs_android
path: ./release/*
if-no-files-found: error
- name: Save cache
if: ${{ github.ref_name == 'master' || github.ref_type == 'tag' }}
uses: actions/cache/save@v4
with:
path: |
~/.cache
~/.cargo
~/.rustup
~/.gradle
app/app/.cxx
app/app/build
drsandroid/target
key: android-cargo
update_metadata:
name: Update metadata
runs-on: ubuntu-latest
if: ${{ github.ref_type != 'tag' && always() }}
needs: [build, build_android]
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
repository: doukutsu-rs/metadata
token: ${{ secrets.METADATA_USER_TOKEN }}
- name: Update metadata
id: metadata
run: |
export FILE="./metadata/nightly.json"
if [ "${{ github.ref_name }}" == "master" ]; then
export VERSION="${{ env.VERSION }}-$((${{ github.run_number }} + 654))"
else
export VERSION="${{ env.VERSION }}-${GITHUB_SHA:0:7}"
fi
if [ "${{ needs.build.result }}" == "success" ]; then
node ./metadata.js --os linux --arch x86_64 --version $VERSION --commit $GITHUB_SHA --link https://nightly.link/doukutsu-rs/doukutsu-rs/actions/runs/${{ github.run_id }}/doukutsu-rs_linux-x64.zip $FILE
node ./metadata.js --os windows --arch x86_64 --version $VERSION --commit $GITHUB_SHA --link https://nightly.link/doukutsu-rs/doukutsu-rs/actions/runs/${{ github.run_id }}/doukutsu-rs_windows-x64.zip $FILE
node ./metadata.js --os windows --arch i686 --version $VERSION --commit $GITHUB_SHA --link https://nightly.link/doukutsu-rs/doukutsu-rs/actions/runs/${{ github.run_id }}/doukutsu-rs_windows-x32.zip $FILE
node ./metadata.js --os macos --arch x64 --version $VERSION --commit $GITHUB_SHA --link https://nightly.link/doukutsu-rs/doukutsu-rs/actions/runs/${{ github.run_id }}/doukutsu-rs_mac-x64.zip $FILE
node ./metadata.js --os macos --arch arm64 --version $VERSION --commit $GITHUB_SHA --link https://nightly.link/doukutsu-rs/doukutsu-rs/actions/runs/${{ github.run_id }}/doukutsu-rs_mac-arm64.zip $FILE
fi
if [ "${{ needs.build_android.result }}" == "success" ]; then
node ./metadata.js --os android --version $VERSION --commit $GITHUB_SHA --link https://nightly.link/doukutsu-rs/doukutsu-rs/actions/runs/${{ github.run_id }}/doukutsu-rs_android.zip $FILE
fi
echo "file=$FILE" >> "$GITHUB_OUTPUT"
- name: Upload metadata
run: |
git config user.name ${{ vars.METADATA_USER_NAME }}
git config user.email ${{ vars.METADATA_USER_EMAIL }}
git add ${{ steps.metadata.outputs.file }}
git commit -m "Update nightly builds metadata(CI)"
git push

46
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Release
on:
release:
types:
- released
defaults:
run:
shell: bash
jobs:
update_metadata:
name: Update metadata
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
with:
repository: doukutsu-rs/metadata
token: ${{ secrets.METADATA_USER_TOKEN }}
- name: Update metadata
id: metadata
run: |
export VERSION="${{ github.event.release.tag_name }}"
export FILE="./metadata/stable.json"
node ./metadata.js --os windows --arch x86_64 --version $VERSION --link https://github.com/doukutsu-rs/doukutsu-rs/releases/download/$VERSION/doukutsu-rs.windows.x86_64.$VERSION.exe $FILE
node ./metadata.js --os windows --arch i686 --version $VERSION --link https://github.com/doukutsu-rs/doukutsu-rs/releases/download/$VERSION/doukutsu-rs.windows.i686.$VERSION.exe $FILE
node ./metadata.js --os macos --arch x86_64 --version $VERSION --link https://github.com/doukutsu-rs/doukutsu-rs/releases/download/$VERSION/doukutsu-rs.macos.x86_64.$VERSION.zip $FILE
node ./metadata.js --os macos --arch arm64 --version $VERSION --link https://github.com/doukutsu-rs/doukutsu-rs/releases/download/$VERSION/doukutsu-rs.macos.arm64.$VERSION.zip $FILE
node ./metadata.js --os linux --arch x86_64 --version $VERSION --link https://github.com/doukutsu-rs/doukutsu-rs/releases/download/$VERSION/doukutsu-rs.linux.x86_64.$VERSION.elf $FILE
node ./metadata.js --os android --version $VERSION --link https://github.com/doukutsu-rs/doukutsu-rs/releases/download/$VERSION/doukutsu-rs.android.$VERSION.apk $FILE
echo "file=$FILE" >> "$GITHUB_OUTPUT"
- name: Upload metadata
run: |
git config user.name ${{ vars.METADATA_USER_NAME }}
git config user.email ${{ vars.METADATA_USER_EMAIL }}
git add ${{ steps.metadata.outputs.file }}
git commit -m "Update stable builds metadata(CI)"
git push

7
.gitignore vendored
View File

@ -2,13 +2,16 @@
.vscode/
# Cave Story (copyrighted) data files
data/
/data/
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Shader binary files
*.dksh
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
@ -60,3 +63,5 @@ ehthumbs_vista.db
# Recycle Bin used on file shares
$RECYCLE.BIN/
*.log

View File

@ -1,9 +1,10 @@
[package]
name = "doukutsu-rs"
description = "A re-implementation of Cave Story (Doukutsu Monogatari) engine"
version = "0.99.0"
version = "0.101.0"
authors = ["Alula", "dawnDus"]
edition = "2018"
edition = "2021"
rust-version = "1.65"
[lib]
crate-type = ["lib"]
@ -18,30 +19,36 @@ required-features = ["exe"]
[profile.release]
lto = "off"
panic = "abort"
codegen-units = 256
incremental = true
split-debuginfo = "packed"
[profile.dev.package."*"]
opt-level = 3
overflow-checks = false
codegen-units = 256
[package.metadata.bundle]
name = "doukutsu-rs"
identifier = "io.github.doukutsu_rs"
version = "0.99.0"
version = "0.101.0"
resources = ["data"]
copyright = "Copyright (c) 2020-2022 doukutsu-rs dev team"
copyright = "Copyright (c) 2020-2023 doukutsu-rs contributors"
category = "Game"
osx_minimum_system_version = "10.12"
[features]
default = ["default-base", "backend-sdl", "render-opengl", "exe"]
default-base = ["scripting-lua", "ogg-playback", "netplay"]
default = ["default-base", "backend-sdl", "render-opengl", "exe", "webbrowser", "discord-rpc"]
default-base = ["ogg-playback"]
ogg-playback = ["lewton"]
backend-sdl = ["sdl2", "sdl2-sys"]
backend-glutin = ["winit", "glutin", "render-opengl"]
backend-horizon = []
render-opengl = []
scripting-lua = ["lua-ffi"]
netplay = ["tokio", "serde_cbor"]
discord-rpc = ["discord-rich-presence"]
netplay = ["serde_cbor"]
editor = []
hooks = ["libc"]
exe = []
android = []
@ -51,41 +58,47 @@ android = []
#winit = { path = "./3rdparty/winit", optional = true, default_features = false, features = ["x11"] }
#sdl2 = { path = "./3rdparty/rust-sdl2", optional = true, features = ["unsafe_textures", "bundled", "static-link"] }
#sdl2-sys = { path = "./3rdparty/rust-sdl2/sdl2-sys", optional = true, features = ["bundled", "static-link"] }
bitvec = "0.20"
#cpal = { path = "./3rdparty/cpal" }
byteorder = "1.4"
case_insensitive_hashmap = "1.0.0"
chrono = "0.4"
cpal = "0.13"
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
cpal = { git = "https://github.com/doukutsu-rs/cpal", rev = "9d269d8724102404e73a61e9def0c0cbc921b676" }
directories = "3"
discord-rich-presence = { version = "0.2", optional = true }
downcast = "0.11"
funty = "=1.1.0" # https://github.com/bitvecto-rs/bitvec/issues/105
glutin = { git = "https://github.com/doukutsu-rs/glutin.git", rev = "8dd457b9adb7dbac7ade337246b6356c784272d9", optional = true, default_features = false, features = ["x11"] }
imgui = "0.8.0"
image = { version = "0.23", default-features = false, features = ["png", "bmp"] }
encoding_rs = "0.8.33"
fern = "0.6.2"
glutin = { git = "https://github.com/doukutsu-rs/glutin.git", rev = "2dd95f042e6e090d36f577cbea125560dd99bd27", optional = true, default_features = false, features = ["x11"] }
imgui = "0.8"
image = { version = "0.24", default-features = false, features = ["png", "bmp"] }
itertools = "0.10"
lazy_static = "1.4.0"
lewton = { version = "0.10.2", optional = true }
libc = { version = "0.2", optional = true }
lazy_static = "1.4"
lewton = { version = "0.10", optional = true }
log = "0.4"
lua-ffi = { git = "https://github.com/doukutsu-rs/lua-ffi.git", rev = "e0b2ff5960f7ef9974aa9675cebe4907bee0134f", optional = true }
num-derive = "0.3.2"
num-traits = "0.2.12"
paste = "1.0.0"
sdl2 = { git = "https://github.com/doukutsu-rs/rust-sdl2.git", rev = "95bcf63768abf422527f86da41da910649b9fcc9", optional = true, features = ["unsafe_textures", "bundled", "static-link"] }
sdl2-sys = { git = "https://github.com/doukutsu-rs/rust-sdl2.git", rev = "95bcf63768abf422527f86da41da910649b9fcc9", optional = true, features = ["bundled", "static-link"] }
num-derive = "0.3"
num-traits = "0.2"
open = "3.2"
paste = "1.0"
pelite = { version = ">=0.9.2", default-features = false, features = ["std"] }
sdl2 = { git = "https://github.com/doukutsu-rs/rust-sdl2.git", rev = "f2f1e29a416bcc22f2faf411866db2c8d9536308", optional = true, features = ["unsafe_textures", "bundled", "static-link"] }
sdl2-sys = { git = "https://github.com/doukutsu-rs/rust-sdl2.git", rev = "f2f1e29a416bcc22f2faf411866db2c8d9536308", optional = true, features = ["bundled", "static-link"] }
rc-box = "1.2.0"
serde = { version = "1", features = ["derive"] }
serde_derive = "1"
serde_cbor = { version = "0.11.2", optional = true }
serde_cbor = { version = "0.11", optional = true }
serde_json = "1.0"
simple_logger = { version = "1.16", features = ["colors", "threads"] }
strum = "0.20"
strum_macros = "0.20"
tokio = { version = "1.12.0", features = ["net"], optional = true }
strum = "0.24"
strum_macros = "0.24"
# remove and replace when drain_filter is in stable
vec_mut_scan = "0.4"
webbrowser = "0.5.5"
winit = { git = "https://github.com/alula/winit.git", rev = "6acf76ff192dd8270aaa119b9f35716c03685f9f", optional = true, default_features = false, features = ["x11"] }
xmltree = "0.10.3"
webbrowser = { version = "0.8.6", optional = true }
winit = { git = "https://github.com/doukutsu-rs/winit.git", rev = "878f206d19af01b0977277929eee5e32667453c0", optional = true, default_features = false, features = ["x11"] }
xmltree = "0.10"
#hack to not link SDL_image on Windows(causes a linker error)
[target.'cfg(not(target_os = "windows"))'.dependencies]
sdl2 = { git = "https://github.com/doukutsu-rs/rust-sdl2.git", rev = "f2f1e29a416bcc22f2faf411866db2c8d9536308", optional = true, features = ["image", "unsafe_textures", "bundled", "static-link"] }
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winuser"] }
@ -94,10 +107,14 @@ winapi = { version = "0.3", features = ["winuser"] }
winres = "0.1"
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2.7"
objc = "0.2"
[target.'cfg(target_os = "android")'.dependencies]
ndk = "0.3"
ndk-glue = "0.3"
ndk-sys = "0.2"
jni = "0.19"
ndk = "0.7"
ndk-glue = "0.7"
ndk-sys = "0.4"
jni = "0.20"
[target.'cfg(target_os = "horizon")'.dependencies]
#deko3d = { path = "./3rdparty/deko3d" }
deko3d = { git = "https://github.com/doukutsu-rs/deko3d-rs", branch = "master" }

181
README.md
View File

@ -1,82 +1,78 @@
![doukutsu-rs](./res/sue_crab_banner_github.png)
A fully playable re-implementation of Cave Story (Doukutsu Monogatari) engine written
A fully playable re-implementation of the Cave Story (Doukutsu Monogatari) engine written
in [Rust](https://www.rust-lang.org/).
[Join the Discord server](https://discord.gg/fbRsNNB)
![https://ci.appveyor.com/api/projects/status/github/doukutsu-rs/doukutsu-rs](https://ci.appveyor.com/api/projects/status/github/doukutsu-rs/doukutsu-rs)
[![CI](https://github.com/doukutsu-rs/doukutsu-rs/actions/workflows/ci.yml/badge.svg?branch=master)](https://nightly.link/doukutsu-rs/doukutsu-rs/workflows/ci/master?preview)
- [Get nightly builds](https://nightly.link/doukutsu-rs/doukutsu-rs/workflows/ci/master?preview) (recommended for now, has latest fixes and improvements)
- [Get nightly builds from AppVeyor](https://ci.appveyor.com/project/alula/doukutsu-rs) (recommended for now, has latest fixes and improvements)
Permalinks to latest builds from `master` branch:
- [Windows (x86_64)](https://ci.appveyor.com/api/projects/alula/doukutsu-rs/artifacts/doukutsu-rs_win64.zip?branch=master&job=windows-x64)
- [macOS (Intel, 64-bit, 10.14+)](https://ci.appveyor.com/api/projects/alula/doukutsu-rs/artifacts/doukutsu-rs_mac-intel.zip?branch=master&job=mac-x64)
- [macOS (Apple M1, 11.0+)](https://ci.appveyor.com/api/projects/alula/doukutsu-rs/artifacts/doukutsu-rs_mac-m1.zip?branch=master&job=mac-arm64)
- [Linux (x86_64)](https://ci.appveyor.com/api/projects/alula/doukutsu-rs/artifacts/doukutsu-rs_linux.zip?branch=master&job=linux-x64)
- [Windows (64-bit)](https://nightly.link/doukutsu-rs/doukutsu-rs/workflows/ci/master/doukutsu-rs_windows-x64.zip)
- [Windows (32-bit)](https://nightly.link/doukutsu-rs/doukutsu-rs/workflows/ci/master/doukutsu-rs_windows-x32.zip)
- [macOS (Intel, 64-bit, 10.14+)](https://nightly.link/doukutsu-rs/doukutsu-rs/workflows/ci/master/doukutsu-rs_mac-x64.zip)
- [macOS (Apple M1, 11.0+)](https://nightly.link/doukutsu-rs/doukutsu-rs/workflows/ci/master/doukutsu-rs_mac-arm64.zip)
- [Linux (64-bit)](https://nightly.link/doukutsu-rs/doukutsu-rs/workflows/ci/master/doukutsu-rs_linux-x64.zip)
- [Android (armv7/arm64/x86)](https://nightly.link/doukutsu-rs/doukutsu-rs/workflows/ci/master/doukutsu-rs_android.zip)
- [Get stable/beta builds from GitHub Releases](https://github.com/doukutsu-rs/doukutsu-rs/releases)
**macOS note:** If you get a `"doukutsu-rs" can't be opened` message, right-click doukutsu-rs.app and click open.
- [Get stable/beta builds from GitHub Releases](https://github.com/doukutsu-rs/doukutsu-rs/releases) (executables only,
no data files bundled, see below for instructions)
> [!NOTE]
> macOS note: If you get a `"doukutsu-rs" can't be opened` message, right-click doukutsu-rs.app and click open.
> [!NOTE]
> If you get issues with Epic Games Store version, scroll down for instructions.
#### Data files
In order to work doukutsu-rs needs to be paired with supported data files. This repository does not contain any data
files.
doukutsu-rs works fine with pre-extracted freeware data from [this repository](https://github.com/doukutsu-rs/game-data)
builds or [NXEngine(-evo)](https://github.com/nxengine/nxengine-evo) or from a supported copy
of [Cave Story+](https://www.nicalis.com/games/cavestory+).
doukutsu-rs works fine with freeware data files or [NXEngine(-evo)](https://github.com/nxengine/nxengine-evo) or from a
supported copy of [Cave Story+](https://www.nicalis.com/games/cavestory+).
<details>
<summary>How to set up data files on Android</summary>
If your phone has an app called **"Files"**:
1. Launch this app.
2. Press **☰** on the top left corner.
3. Tap on **"doukutsu-rs game data"**.
4. Copy your game data files to the opened folder.
If your phone does not have this app:
1. Install the **"Material Files"** app from *Hai Zhang* and launch it([Google Play](https://play.google.com/store/apps/details?id=me.zhanghai.android.files) | [F-Droid](https://f-droid.org/en/packages/me.zhanghai.android.files/) | [Github Releases](https://github.com/zhanghai/MaterialFiles/releases)).
2. Press **☰** on the top left corner.
3. Press **"+ Add storage"**.
4. In the window that pops up, press **"External storage"**.
5. Press **☰** on the top left corner.
6. Tap on **"doukutsu-rs game data"**.
7. Press the large blue button at the bottom labelled **"USE THIS FOLDER"**.
8. Then click on **☰** in the top left corner again and open.
9. Tap on **"files"** above **"+ Add storage"**.
10. Copy your game data files to the opened folder.
</details>
#### Supported game editions and data file acquisition guides
**Freeware**
- Vanilla freeware can't be just used without additional work, because some important data files are embedded inside the
executable. An automatic extractor might be available in future.
doukutsu-rs works out of the box when it's placed in the same directory as the original Doukutsu.exe executable. On the initial
startup, doukutsu-rs will automatically extract the additional resources that are embedded in the vanilla game into the `data`
directory. Until that is done, both doukutsu-rs and the vanilla executable have to exist in the directory.
<details>
<summary>Manual extraction guide</summary>
<summary>Example root directory</summary>
Tools required:
- Windows version of the game (1.0.0.6), original Japanese or with Aeon Genesis patch.
- [Resource Hacker](http://www.angusj.com/resourcehacker/#download)
- [Booster's Lab](https://www.cavestory.org/download/editors.php)
1. Open Doukutsu.exe in Resource Hacker.
2. Click on `ORG` group, select `Action` -> `Save [ORG] group to an .RC file`.
3. Navigate to `data` folder and create a folder named `Org` and save the .RC file there.
4. Click on `BITMAP` group, select `Action` -> `Save [BITMAP] group to an .RC file`.
5. Save them in `data` folder (**NOT** in `Org` folder).
6. Go to file explorer and navigate to `data` folder.
7. Delete Bitmap.rc
8. Go to `Org` folder.
9. Delete Org.rc
10. Rename extension of all files from `.bin` to `.org` - you won't have music if you don't do that!
11. Close Resource Hacker.
12. Open Booster's Lab
13. Load `Doukutsu.exe` in Booster's Lab - you can ignore the fact it tries to apply any patches or renames .pbm to .bmp, d-rs doesn't care.
14. Select `File` -> `Export mapdata` -> `stage.tbl`
15. Close Booster's Lab, saving isn't necessary.
16. Optionally delete leftover files and folders - `.boostlab`, `ScriptSource`, `tsc_def.txt`
17. That's all, you have everything to use it with doukutsu-rs now.
If you followed the above steps, the directory structure should look like this:
`data/`:
![files in /data/](https://media.discordapp.net/attachments/745322954660905103/947915770376102008/unknown.png?width=844&height=629)
`data/Org`:
![files in /data/Org/](https://media.discordapp.net/attachments/745322954660905103/947915770690687016/unknown.png)
![example root directory with doukutsu-rs and vanilla Cave Story](https://i.imgur.com/3dJ7WMB.png)
</details>
- https://github.com/doukutsu-rs/game-data - Pre-extracted freeware game data, graphics converted to .png, already
distributed with AppVeyor builds for your convenience. (recommended)
- https://github.com/nxengine/nxengine-evo/releases/download/v2.6.5-1/NXEngine-Evo-v2.6.5-1-Win64.zip -
copy `NXEngine-evo-2.6.5-1-xxx/data` from the archive to runtime directory
**Cave Story+**
@ -93,8 +89,8 @@ If you want to use doukutsu-rs as a substitute for Mac version of Cave Story+ (w
on 10.15+ anymore), do the following:
1. Find the doukutsu-rs executable:
- In AppVeyor builds, it's in `doukutsu-rs.app/Contents/MacOS/doukutsu-rs`
- In your own builds, it's in `target/(release|debug)/doukutsu-rs`
- In AppVeyor builds, it's in `doukutsu-rs.app/Contents/MacOS/doukutsu-rs`
- In your own builds, it's in `target/(release|debug)/doukutsu-rs`
2. Open Steam Library, select `Cave Story+`, press the `Manage` button (gear icon) and select `Properties...`
3. Select `Local Files` and press `Browse...`.
4. Open the `Cave Story+.app` bundle and navigate to `Contents/MacOS` directory.
@ -103,14 +99,25 @@ on 10.15+ anymore), do the following:
7. Launch the game from Steam and enjoy!
![image](https://user-images.githubusercontent.com/53099651/155904982-eb6032d8-7a4d-4af7-ae6f-b69041ecfaa4.png)
</details>
> [!WARNING]
> **EPIC GAMES STORE VERSION WARNING**
>
> Nicalis for some reason ships a stray `opengl32.dll` DLL from Windows 7 with the Epic Games Store copies of Cave Story+.
>
> However as the game is 32-bit and the dll is 64-bit it has no effect on the original version, but as it's a core Windows DLL and doukutsu-rs ships 64-bit builds and uses OpenGL, it's makes the game crash on startup.
>
> The fix is to simply delete `opengl32.dll`, as it's not used anyway.
<details>
<summary>Epic Games Store</summary>
Check your default installation directory.
![image](https://user-images.githubusercontent.com/53099651/155905035-0080eace-bd98-4cf5-9628-c98334ea768c.png)
</details>
<details>
@ -119,6 +126,7 @@ Check your default installation directory.
Check your default installation directory.
![image](https://user-images.githubusercontent.com/53099651/155906494-1e53f174-f12f-41be-ab53-8745cdf735b5.png)
</details>
<details>
@ -127,48 +135,64 @@ Check your default installation directory.
The archive from Humble Bundle contains the necessary `data` folder, in the same folder as `CaveStory+.exe`.
![image](https://user-images.githubusercontent.com/96957561/156861929-7fa03951-442b-4277-b673-474189411103.png)
</details>
<details>
<summary>WiiWare</summary>
Extract the `.wad` in order to get valid [Cave Story assets](https://user-images.githubusercontent.com/53099651/159585593-43fead24-b041-48f4-8332-be50d712310d.png)
1. [Dump Your WiiWare `.wad`](https://wii.guide/dump-wads.html)
2. [Extract and decompress the `data` folder](https://docs.google.com/document/d/1hDNDgNl0cUDlFOQ_BUOq3QCGb7S0xfUxRoob-hfM-DY)
Example of a [valid uncompressed `data` folder](https://user-images.githubusercontent.com/53099651/159585593-43fead24-b041-48f4-8332-be50d712310d.png)
</details>
**Remastered version (first released in 2017 on Switch)**
Note that this version is **incompatible** with saves from the original version.
Interchanging the save files may result in spawning in wrong locations, softlocks, graphical glitches, or other issues.
> [!NOTE]
> This version is **incompatible** with saves from the original version.
>
> Interchanging the save files may result in spawning in wrong locations, softlocks, graphical glitches, or other issues.
<details>
<summary>Nintendo Switch</summary>
Extract the `data` folder directly from the ROM.
Extract the `data` folder (contained in `romfs`) from your console using tool such as [nxdumptool](https://github.com/DarkMatterCore/nxdumptool).
**Important notes:**
- doukutsu-rs doesn't rely on the original ROM or executable, you just need the data files, go to `RomFS options` menu to just extract the files to SD card so you don't need to do any extra steps.
- Ensure you're dumping the files **with update included** (`Use update/DLC` option), as 1.0 isn't supported.
**Nintendo Switch homebrew port specific info**
If you're running the homebrew port (drshorizon.nro) on your Switch, you can avoid the dumping step, doukutsu-rs will
automatically detect and mount the data files if you run it over Cave Story+ in Title Override mode (hold `R` while starting CS+ and launch d-rs from hbmenu).
You can put your own data files in `/switch/doukutsu-rs/data` directory on SD Card, which will be overlayed over RomFS if
you run it in setup described above.
</details>
#### Controls
Same controls as the default for freeware and Cave Story+ keyboard.
Same controls as the default for freeware and Cave Story+ keyboard.
To change, edit `doukutsu-rs\data\settings.json` within your user directory.
To change, use the control customization menu or edit `doukutsu-rs\data\settings.json` within your user directory.
| | P1 | P2 |
| ------------------------- | --------- | --------- |
| Movement | `← ↑ ↓ →` | `, L . /` |
| Jump | `Z` | `B` |
| Shoot | `X` | `N` |
| Cycle Weapon | `A and S` | `G and H` |
| Inventory / Skip cutscene | `Q` | `T` |
| Map | `W` | `Y` |
| Strafe | `LShift` | `RShift` |
| | P1 | P2 |
|--------------|-----------|-----------|
| Movement | `← ↑ ↓ →` | `, L . /` |
| Jump | `Z` | `B` |
| Shoot | `X` | `N` |
| Cycle Weapon | `A and S` | `G and H` |
| Inventory | `Q` | `T` |
| Map | `W` | `Y` |
| Strafe | `LShift` | `RShift` |
- `Alt + Enter` - Toggle Fullscreen
- `F2` (While paused) - Quick Restart
#### Screenshots
<details>
@ -198,9 +222,9 @@ To change, edit `doukutsu-rs\data\settings.json` within your user directory.
![Balcony Switch](https://user-images.githubusercontent.com/53099651/155918810-063c0f06-2d48-485f-8367-6337525deab7.png)
![Dogs Switch](https://media.discordapp.net/attachments/745322954660905103/947895408196202617/unknown.png)
![Dogs Switch](https://github.com/doukutsu-rs/doukutsu-rs/assets/6276139/30ba01ae-375d-4488-98c4-98e3e8c7f187)
![Almond Switch](https://media.discordapp.net/attachments/745322954660905103/947898268631826492/unknown.png)
![Almond Switch](https://github.com/doukutsu-rs/doukutsu-rs/assets/6276139/42d4b6a3-4fc5-4aaf-9535-462c4c484dc7)
![Hell Switch](https://user-images.githubusercontent.com/53099651/155918602-62268274-c529-41c2-a87e-0c31e7874b94.png)
@ -212,4 +236,5 @@ To change, edit `doukutsu-rs\data\settings.json` within your user directory.
- [@Daedily](https://twitter.com/Daedliy) - brand artwork (Icon / Banner / Server), screenshots for this guide.
- [ggez](https://github.com/ggez/ggez) - parts of it are used in `crate::framework`, notably the VFS code.
- [Clownacy](https://github.com/Clownacy) - widescreen camera code.
- [LunarLambda for organism](https://gitdab.com/LunarLambda/organism) - used as basis for our Organya playback engine.
- [LunarLambda for organism](https://github.com/doukutsu-rs/organism) - used as basis for our Organya playback engine.
- [Zoroyoshi](http://z.apps.atjp.jp/k12x10/) - k12x10 font we use as built-in font.

1
app/.gitignore vendored
View File

@ -231,3 +231,4 @@ fabric.properties
!/gradle/wrapper/gradle-wrapper.jar
# End of https://www.toptal.com/developers/gitignore/api/androidstudio,gradle,android
app/release/

View File

@ -4,21 +4,23 @@ plugins {
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
ndkVersion "22.1.7171670"
namespace "io.github.doukutsu_rs"
compileSdkVersion 33
buildToolsVersion "33.0.0"
ndkVersion "25.2.9519653"
defaultConfig {
applicationId "io.github.doukutsu_rs"
minSdkVersion 24
targetSdkVersion 30
versionCode 1
versionName "1.0"
targetSdkVersion 33
versionCode 2
versionName "0.101.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters 'x86', 'arm64-v8a', 'armeabi-v7a'
abiFilters 'arm64-v8a'
stl = "c++_shared"
}
externalNativeBuild {
@ -41,6 +43,15 @@ android {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
ndk {
abiFilters 'x86', 'arm64-v8a', 'armeabi-v7a'
stl = "c++_shared"
}
}
debug {
jniDebuggable true
renderscriptDebuggable true
}
}
@ -58,28 +69,28 @@ android {
path "src/main/cpp/CMakeLists.txt"
}
}
packagingOptions {
jniLibs {
excludes += "**/dummy.so"
}
}
}
dependencies {
implementation 'com.android.support:support-annotations:28.0.0'
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support:design:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:2.0.1'
implementation 'android.arch.navigation:navigation-fragment:1.0.0'
implementation 'android.arch.navigation:navigation-ui:1.0.0'
implementation 'androidx.annotation:annotation:1.5.0'
implementation 'androidx.appcompat:appcompat:1.6.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'androidx.core:core:1.9.0'
implementation 'com.google.android.material:material:1.8.0'
}
println("cargo target: ${project.buildDir.getAbsolutePath()}/rust-target")
println("ndk dir: ${android.ndkDirectory}")
cargoNdk {
targets = [
"x86",
"arm",
"arm64"
]
librariesNames = ["libdrsandroid.so"]
//targetDirectory = "${project.buildDir.getAbsolutePath()}/rust-target"
module = "../drsandroid/"
extraCargoEnv = ["ANDROID_NDK_HOME": android.ndkDirectory]
extraCargoBuildArguments = []
@ -88,9 +99,17 @@ cargoNdk {
buildTypes {
release {
buildType = "release"
targets = [
"x86",
"arm",
"arm64"
]
}
debug {
buildType = "debug"
targets = [
"arm64"
]
}
}
}

View File

@ -1,26 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.github.doukutsu_rs">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application android:allowBackup="true" android:extractNativeLibs="true" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true"
<application
android:allowBackup="true"
android:appCategory="game"
android:description="@string/app_description"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:isGame="true"
android:label="@string/app_name"
android:resizeableActivity="false"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Doukutsurs">
<activity android:name=".DownloadActivity" android:label="Download" android:screenOrientation="sensorLandscape"
android:theme="@style/Theme.Doukutsurs.NoActionBar"></activity>
<activity android:name=".MainActivity" android:configChanges="orientation|keyboardHidden|screenSize"
android:label="doukutsu-rs" android:launchMode="standard" android:screenOrientation="sensorLandscape">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:exported="true"
android:label="doukutsu-rs"
android:launchMode="standard"
android:screenOrientation="sensorLandscape"
android:theme="@style/Theme.Doukutsurs.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="drsandroid" />
</activity>
<provider android:name=".DoukutsuDocumentsProvider" android:authorities="${documentsAuthority}"
android:exported="true" android:grantUriPermissions="true"
<activity
android:name=".DownloadActivity"
android:label="Download"
android:screenOrientation="sensorLandscape"
android:theme="@style/Theme.Doukutsurs.NoActionBar" />
<activity
android:name=".GameActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:exported="true"
android:launchMode="standard"
android:screenOrientation="sensorLandscape">
<meta-data
android:name="android.app.lib_name"
android:value="drsandroid" />
</activity>
<provider
android:name=".DoukutsuDocumentsProvider"
android:authorities="${documentsAuthority}"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />

View File

@ -1,8 +1,8 @@
# Sets the minimum version of CMake required to build your native library.
# This ensures that a certain set of CMake features is available to
# your build.
cmake_minimum_required(VERSION 3.10)
project(doukutsu-rs)
cmake_minimum_required(VERSION 3.18)
# Copy shared STL files to Android Studio output directory so they can be
# packaged in the APK.
@ -21,7 +21,7 @@ cmake_minimum_required(VERSION 3.10)
function(configure_shared_stl lib_path so_base)
message("Configuring STL ${so_base} for ${ANDROID_ABI}")
configure_file(
"${ANDROID_NDK}/sources/cxx-stl/${lib_path}/libs/${ANDROID_ABI}/lib${so_base}.so"
"${ANDROID_NDK}/toolchains/llvm/prebuilt/${ANDROID_HOST_TAG}/sysroot/usr/lib/${CMAKE_LIBRARY_ARCHITECTURE}/lib${so_base}.so"
"${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/lib${so_base}.so"
COPYONLY)
endfunction()
@ -50,4 +50,4 @@ endif()
# and CMake builds them for you. When you build your app, Gradle
# automatically packages shared libraries with your APK.
#add_library(dummy SHARED dummy.cpp)
add_library(dummy SHARED dummy.cpp)

View File

@ -0,0 +1,16 @@
package io.github.doukutsu_rs;
import android.app.Activity;
import android.view.Window;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
public class ActivityUtils {
public static void hideSystemBars(Activity activity) {
Window window = activity.getWindow();
WindowInsetsControllerCompat windowInsetsController =
WindowCompat.getInsetsController(window, window.getDecorView());
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars());
}
}

View File

@ -1,21 +1,31 @@
package io.github.doukutsu_rs;
import android.content.ContentResolver;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
import android.net.Uri;
import android.os.Build;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Path;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.util.Log;
import android.webkit.MimeTypeMap;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.util.LinkedList;
import static android.os.Build.VERSION.SDK_INT;
public class DoukutsuDocumentsProvider extends DocumentsProvider {
private final static String[] DEFAULT_ROOT_PROJECTION =
@ -54,7 +64,7 @@ public class DoukutsuDocumentsProvider extends DocumentsProvider {
row.add(Root.COLUMN_ROOT_ID, id);
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher);
row.add(Root.COLUMN_TITLE,
getContext().getString(R.string.document_provider_name));
getContext().getString(R.string.app_name));
row.add(Root.COLUMN_MIME_TYPES, "*/*");
row.add(Root.COLUMN_AVAILABLE_BYTES, file.getFreeSpace());
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_CREATE);
@ -95,7 +105,9 @@ public class DoukutsuDocumentsProvider extends DocumentsProvider {
pushFile(result, file);
}
}
result.setNotificationUri(getContext().getContentResolver(), DocumentsContract.buildDocumentUri(BuildConfig.DOCUMENTS_AUTHORITY, parentDocumentId));
return result;
}
@ -134,7 +146,16 @@ public class DoukutsuDocumentsProvider extends DocumentsProvider {
} catch (IOException e) {
throw new FileNotFoundException("Couldn't create file: " + e.getMessage());
}
Uri uri = DocumentsContract.buildDocumentUri(BuildConfig.DOCUMENTS_AUTHORITY, file.getParent());
if (SDK_INT >= Build.VERSION_CODES.R) {
getContext().getContentResolver().notifyChange(uri, null, ContentResolver.NOTIFY_INSERT);
} else {
getContext().getContentResolver().notifyChange(uri, null);
}
return file.getAbsolutePath();
}
@ -147,8 +168,38 @@ public class DoukutsuDocumentsProvider extends DocumentsProvider {
}
deleteRecursive(file);
// todo refresh this shit
// getContext().getContentResolver().refresh()
Uri uri = DocumentsContract.buildDocumentUri(BuildConfig.DOCUMENTS_AUTHORITY, file.getParent());
if (SDK_INT >= Build.VERSION_CODES.R) {
getContext().getContentResolver().notifyChange(uri, null, ContentResolver.NOTIFY_DELETE);
} else {
getContext().getContentResolver().notifyChange(uri, null);
}
}
@Override
@RequiresApi(Build.VERSION_CODES.O)
public Path findDocumentPath(@Nullable String parentDocumentId, String childDocumentId) throws FileNotFoundException {
if (parentDocumentId == null) {
parentDocumentId = getContext().getFilesDir().getAbsolutePath();
}
File childFile = new File(childDocumentId);
if (!childFile.exists()) {
throw new FileNotFoundException(childFile.getAbsolutePath()+" doesn't exist");
} else if (!isChildDocument(parentDocumentId, childDocumentId)) {
throw new FileNotFoundException(childDocumentId+" is not child of "+parentDocumentId);
}
LinkedList<String> path = new LinkedList<>();
while (childFile != null && isChildDocument(parentDocumentId, childFile.getAbsolutePath())) {
path.addFirst(childFile.getAbsolutePath());
childFile = childFile.getParentFile();
}
return new Path(parentDocumentId, path);
}
@Override
@ -187,11 +238,26 @@ public class DoukutsuDocumentsProvider extends DocumentsProvider {
File newPath = new File(file.getParentFile().getAbsolutePath() + "/" + displayName);
try {
Files.move(file.toPath(), newPath.toPath());
if (SDK_INT >= Build.VERSION_CODES.O) {
Files.move(file.toPath(), newPath.toPath());
} else {
if (!file.renameTo(newPath)) {
throw new IOException("Couldn't rename file: " + file.getAbsolutePath());
}
}
} catch (IOException e) {
throw new FileNotFoundException(e.getMessage());
}
Uri uri = DocumentsContract.buildDocumentUri(BuildConfig.DOCUMENTS_AUTHORITY, file.getParent());
if (SDK_INT >= Build.VERSION_CODES.R) {
getContext().getContentResolver().notifyChange(uri, null, ContentResolver.NOTIFY_UPDATE);
} else {
getContext().getContentResolver().notifyChange(uri, null);
}
return newPath.getAbsolutePath();
}
@ -205,8 +271,18 @@ public class DoukutsuDocumentsProvider extends DocumentsProvider {
File[] files = file.listFiles();
if (files != null) {
for (File f : files) {
if (!Files.isSymbolicLink(f.toPath())) {
deleteRecursive(f);
if (SDK_INT >= Build.VERSION_CODES.O) {
if (!Files.isSymbolicLink(f.toPath())) {
deleteRecursive(f);
}
} else {
try {
if (!f.getAbsolutePath().equals(f.getCanonicalPath())) {
deleteRecursive(f);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

View File

@ -2,14 +2,15 @@ package io.github.doukutsu_rs;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.os.Handler;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Locale;
import java.util.ArrayList;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@ -18,6 +19,7 @@ public class DownloadActivity extends AppCompatActivity {
private ProgressBar progressBar;
private DownloadThread downloadThread;
private String basePath;
private Handler handler = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -26,7 +28,9 @@ public class DownloadActivity extends AppCompatActivity {
setContentView(R.layout.activity_download);
txtProgress = findViewById(R.id.txtProgress);
progressBar = findViewById(R.id.progressBar);
basePath = getFilesDir().getAbsolutePath() + "/data/";
ActivityUtils.hideSystemBars(this);
basePath = getFilesDir().getAbsolutePath() + "/";
downloadThread = new DownloadThread();
downloadThread.start();
@ -39,23 +43,37 @@ public class DownloadActivity extends AppCompatActivity {
}
private class DownloadThread extends Thread {
private static final String DOWNLOAD_URL = "https://github.com/doukutsu-rs/game-data/archive/refs/heads/master.zip";
private final ArrayList<DownloadEntry> urls = new ArrayList<>();
private final ArrayList<String> filesWhitelist = new ArrayList<>();
@Override
public void run() {
this.filesWhitelist.add("data/");
this.filesWhitelist.add("Doukutsu.exe");
// DON'T SET `true` VALUE FOR TRANSLATIONS
this.urls.add(new DownloadEntry(R.string.download_entries_base, "https://www.cavestory.org/downloads/cavestoryen.zip", true));
for (DownloadEntry entry : this.urls) {
this.download(entry);
}
}
private void download(DownloadEntry downloadEntry) {
HttpURLConnection connection = null;
try {
URL url = new URL(DOWNLOAD_URL);
URL url = new URL(downloadEntry.url);
connection = (HttpURLConnection) url.openConnection();
connection.connect();
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IllegalStateException("Bad HTTP response code: " + connection.getResponseCode());
throw new IllegalStateException(getString(R.string.download_status_error_http, connection.getResponseCode()));
}
int fileLength = connection.getContentLength();
if (fileLength == 0) {
progressBar.setIndeterminate(true);
handler.post(() -> progressBar.setIndeterminate(true));
}
byte[] zipFile;
@ -78,10 +96,10 @@ public class DownloadActivity extends AppCompatActivity {
if (last + 1000 >= now) {
int speed = (int) ((downloaded - downloadedLast) / 1024.0);
String text = (fileLength > 0)
? String.format(Locale.ENGLISH, "Downloading... %d%% (%d/%d KiB, %d KiB/s)", downloaded * 100 / fileLength, downloaded / 1024, fileLength / 1024, speed)
: String.format(Locale.ENGLISH, "Downloading... --%% (%d KiB, %d KiB/s)", downloaded / 1024, speed);
? getString(R.string.download_status_downloading, downloadEntry.name, downloaded * 100 / fileLength, downloaded / 1024, fileLength / 1024, speed)
: getString(R.string.download_status_downloading_null, downloadEntry.name, downloaded / 1024, speed);
txtProgress.setText(text);
handler.post(() -> txtProgress.setText(text));
downloadedLast = downloaded;
last = now;
@ -94,46 +112,90 @@ public class DownloadActivity extends AppCompatActivity {
}
new File(basePath).mkdirs();
try (ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(zipFile))) {
ZipEntry entry;
byte[] buffer = new byte[4096];
while ((entry = in.getNextEntry()) != null) {
String entryName = entry.getName();
this.unpack(zipFile, downloadEntry.isBase);
// strip prefix
if (entryName.startsWith("game-data-master/")) {
entryName = entryName.substring("game-data-master/".length());
}
handler.post(() -> txtProgress.setText(getString(R.string.download_status_done)));
txtProgress.setText("Unpacking: " + entryName);
if (entry.isDirectory()) {
new File(basePath + entryName).mkdirs();
} else {
try (FileOutputStream fos = new FileOutputStream(basePath + entryName)) {
int count;
while ((count = in.read(buffer)) != -1) {
fos.write(buffer, 0, count);
}
}
}
in.closeEntry();
}
}
txtProgress.setText("Done!");
Intent intent = new Intent(DownloadActivity.this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
startActivity(intent);
DownloadActivity.this.finish();
handler.post(() -> {
Intent intent = new Intent(DownloadActivity.this, GameActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
DownloadActivity.this.finish();
});
} catch (Exception e) {
if (txtProgress != null) txtProgress.setText(e.getMessage());
handler.post(() -> {
if (txtProgress != null)
txtProgress.setText(e.getMessage());
});
e.printStackTrace();
} finally {
if (connection != null) connection.disconnect();
}
}
private void unpack(byte[] zipFile, boolean isBase) throws IOException {
ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(zipFile));
ZipEntry entry;
byte[] buffer = new byte[4096];
while ((entry = in.getNextEntry()) != null) {
String entryName = entry.getName();
// strip prefix
if (entryName.startsWith("CaveStory/")) {
entryName = entryName.substring("CaveStory/".length());
}
if (!this.entryInWhitelist(entryName)) {
continue;
}
final String s = entryName;
handler.post(() -> txtProgress.setText(
getString(R.string.download_status_unpacking, s)
));
if (entry.isDirectory()) {
new File(basePath + entryName).mkdirs();
} else {
try (FileOutputStream fos = new FileOutputStream(basePath + entryName)) {
int count;
while ((count = in.read(buffer)) != -1) {
fos.write(buffer, 0, count);
}
}
}
in.closeEntry();
}
}
private boolean entryInWhitelist(String entry) {
for (String file : this.filesWhitelist) {
if (entry.startsWith(file)) {
return true;
}
}
return false;
}
}
private class DownloadEntry {
public String name; //e.g. "Polish translation", "Base data files"
public String url;
public boolean isBase = false;
DownloadEntry(String name, String url, boolean isBase) {
this.name = name;
this.url = url;
this.isBase = isBase;
}
DownloadEntry(int name, String url, boolean isBase) {
this.name = getString(name);
this.url = url;
this.isBase = isBase;
}
}
}

View File

@ -0,0 +1,118 @@
package io.github.doukutsu_rs;
import android.app.NativeActivity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.res.Configuration;
import android.hardware.SensorManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.view.OrientationEventListener;
import android.view.WindowInsets;
import android.widget.Toast;
import java.io.File;
import static android.os.Build.VERSION.SDK_INT;
public class GameActivity extends NativeActivity {
private int[] displayInsets = new int[]{0, 0, 0, 0};
private OrientationEventListener listener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityUtils.hideSystemBars(this);
listener = new OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
@Override
public void onOrientationChanged(int orientation) {
GameActivity.this.updateCutouts();
}
};
if (listener.canDetectOrientation()) {
listener.enable();
} else {
listener = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (listener != null) {
listener.disable();
listener = null;
}
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
this.updateCutouts();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
this.updateCutouts();
}
private void updateCutouts() {
this.displayInsets[0] = 0;
this.displayInsets[1] = 0;
this.displayInsets[2] = 0;
this.displayInsets[3] = 0;
WindowInsets insets = getWindow().getDecorView().getRootWindowInsets();
if (insets != null) {
this.displayInsets[0] = Math.max(this.displayInsets[0], insets.getStableInsetLeft());
this.displayInsets[1] = Math.max(this.displayInsets[1], insets.getStableInsetTop());
this.displayInsets[2] = Math.max(this.displayInsets[2], insets.getStableInsetRight());
this.displayInsets[3] = Math.max(this.displayInsets[3], insets.getStableInsetBottom());
} else {
return;
}
if (SDK_INT >= Build.VERSION_CODES.P) {
android.view.DisplayCutout cutout = insets.getDisplayCutout();
if (cutout != null) {
this.displayInsets[0] = Math.max(this.displayInsets[0], cutout.getSafeInsetLeft());
this.displayInsets[1] = Math.max(this.displayInsets[0], cutout.getSafeInsetTop());
this.displayInsets[2] = Math.max(this.displayInsets[0], cutout.getSafeInsetRight());
this.displayInsets[3] = Math.max(this.displayInsets[0], cutout.getSafeInsetBottom());
}
}
}
public void openDir(String path) {
Uri uri = DocumentsContract.buildDocumentUri(BuildConfig.DOCUMENTS_AUTHORITY, path);
File file = new File(path);
if (!file.isDirectory()) {
Toast.makeText(getApplicationContext(), R.string.dir_not_found, Toast.LENGTH_LONG).show();
return;
}
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setDataAndType(uri, DocumentsContract.Document.MIME_TYPE_DIR);
intent.setFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
try {
startActivity(intent);
} catch(ActivityNotFoundException e) {
Toast.makeText(getApplicationContext(), R.string.no_app_found_to_open_dir, Toast.LENGTH_LONG).show();
}
}
}

View File

@ -1,118 +1,50 @@
package io.github.doukutsu_rs;
import android.app.AlertDialog;
import android.app.NativeActivity;
import android.content.Intent;
import android.content.res.Configuration;
import android.hardware.SensorManager;
import android.os.Build;
import android.os.Bundle;
import android.view.OrientationEventListener;
import android.view.WindowInsets;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
import static android.os.Build.VERSION.SDK_INT;
public class MainActivity extends NativeActivity {
private int[] displayInsets = new int[]{0, 0, 0, 0};
private OrientationEventListener listener;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ActivityUtils.hideSystemBars(this);
File f = new File(getFilesDir().getAbsolutePath() + "/data/");
String[] list = f.list();
if (!f.exists() || (list != null && list.length == 0)) {
messageBox("Missing data files", "No data files found, would you like to download them?", () -> {
messageBox(getString(R.string.missing_data_title), getString(R.string.missing_data_desc), () -> {
Intent intent = new Intent(this, DownloadActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
this.finish();
});
}
super.onCreate(savedInstanceState);
listener = new OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
@Override
public void onOrientationChanged(int orientation) {
MainActivity.this.updateCutouts();
}
};
if (listener.canDetectOrientation()) {
listener.enable();
}, this::launchGame);
} else {
listener = null;
launchGame();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (listener != null) {
listener.disable();
listener = null;
}
private void launchGame() {
Intent intent = new Intent(this, GameActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
this.finish();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
this.updateCutouts();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
this.updateCutouts();
}
private void updateCutouts() {
this.displayInsets[0] = 0;
this.displayInsets[1] = 0;
this.displayInsets[2] = 0;
this.displayInsets[3] = 0;
WindowInsets insets = getWindow().getDecorView().getRootWindowInsets();
if (insets != null) {
this.displayInsets[0] = Math.max(this.displayInsets[0], insets.getStableInsetLeft());
this.displayInsets[1] = Math.max(this.displayInsets[1], insets.getStableInsetTop());
this.displayInsets[2] = Math.max(this.displayInsets[2], insets.getStableInsetRight());
this.displayInsets[3] = Math.max(this.displayInsets[3], insets.getStableInsetBottom());
} else {
return;
}
if (SDK_INT >= Build.VERSION_CODES.P) {
android.view.DisplayCutout cutout = insets.getDisplayCutout();
if (cutout != null) {
this.displayInsets[0] = Math.max(this.displayInsets[0], cutout.getSafeInsetLeft());
this.displayInsets[1] = Math.max(this.displayInsets[0], cutout.getSafeInsetTop());
this.displayInsets[2] = Math.max(this.displayInsets[0], cutout.getSafeInsetRight());
this.displayInsets[3] = Math.max(this.displayInsets[0], cutout.getSafeInsetBottom());
}
}
}
private void messageBox(String title, String message, Runnable callback) {
private void messageBox(String title, String message, Runnable yesCallback, Runnable noCallback) {
this.runOnUiThread(() -> {
AlertDialog.Builder alert = new AlertDialog.Builder(this);
alert.setTitle(title);
alert.setMessage(message);
alert.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
callback.run();
});
alert.setNegativeButton(android.R.string.no, (dialog, whichButton) -> {
// hide
});
alert.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> yesCallback.run());
alert.setNegativeButton(android.R.string.no, (dialog, whichButton) -> noCallback.run());
alert.setCancelable(false);
alert.show();
});
}

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -21,7 +22,7 @@
android:id="@+id/txtTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Downloading game data"
android:text="@string/download_title"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.AppCompat.Display1" />
@ -39,4 +40,4 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</android.support.constraint.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,4 +1,21 @@
<resources>
<string name="app_name">doukutsu-rs</string>
<string name="document_provider_name">doukutsu-rs game data</string>
<string name="app_name" translatable="false">doukutsu-rs</string>
<string name="app_description">A faithful and open-source remake of Cave Story engine written in Rust.</string>
<string name="missing_data_title">Missing data files</string>
<string name="missing_data_desc">No data files found, would you like to download them?</string>
<string name="download_title">Downloading game data</string>
<string name="download_status_error_http">Bad HTTP response code: %d</string>
<!-- Downloading {entry_name}… {progress}% ({downloaded_size}/{total_size} KiB, {speed} KiB/s) -->
<string name="download_status_downloading">Downloading %1$s… %2$d%% (%3$d/%4$d KiB, %5$d KiB/s)</string>
<string name="download_status_downloading_null">Downloading %1$s… --%% (%2$d KiB, %3$d KiB/s)</string>
<string name="download_status_unpacking">Unpacking: %s</string>
<string name="download_status_done">Done!</string>
<!-- Look {entry_name} on 9th line -->
<string name="download_entries_base">base game files</string>
<string name="dir_not_found">Dir not found</string>
<string name="no_app_found_to_open_dir">No app found to open dir</string>
</resources>

View File

@ -2,13 +2,13 @@
buildscript {
repositories {
google()
jcenter()
mavenCentral()
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "com.android.tools.build:gradle:4.2.0"
classpath "com.android.tools.build:gradle:7.3.1"
classpath "gradle.plugin.com.github.willir.rust:plugin:0.3.4"
// NOTE: Do not place your application dependencies here; they belong
@ -19,7 +19,7 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}

View File

@ -1,6 +1,6 @@
#Wed Feb 17 23:16:31 CET 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

0
app/local.properties Normal file
View File

View File

@ -15,6 +15,12 @@ fn main() {
let mut res = winres::WindowsResource::new();
res.set_icon("res/sue.ico");
res.compile().unwrap();
if target.contains("i686") {
// yet another hack
println!("cargo:rustc-link-arg=/FORCE:MULTIPLE");
println!("cargo:rustc-link-lib=shlwapi");
}
}
if target.contains("darwin") {

View File

@ -1,14 +1,20 @@
[package]
name = "drsandroid"
description = "doukutsu-rs targeted for Android"
version = "0.1.0"
authors = ["Alula"]
edition = "2018"
edition = "2021"
[profile.release]
opt-level = 3
lto = "off"
codegen-units = 256
incremental = true
[lib]
crate-type = ["cdylib"]
[dependencies]
ndk = "0.3"
ndk-glue = "0.3"
ndk-sys = "0.2"
doukutsu-rs = { path = "../", default-features = false, features = ["default-base", "backend-glutin"] }
ndk = "0.7"
ndk-glue = "0.7"
ndk-sys = "0.4"
doukutsu-rs = { path = "../", default-features = false, features = ["default-base", "backend-glutin", "webbrowser"] }

View File

@ -1,7 +1,11 @@
#[cfg(target_os = "android")]
#[cfg_attr(target_os = "android", ndk_glue::main())]
pub fn android_main() {
let options = doukutsu_rs::LaunchOptions { server_mode: false, editor: false };
let resource_dir = std::path::PathBuf::from(ndk_glue::native_activity().internal_data_path().to_string_lossy().to_string());
doukutsu_rs::init(options).unwrap();
std::env::set_current_dir(&resource_dir).unwrap();
let options = doukutsu_rs::game::LaunchOptions { server_mode: false, editor: false };
doukutsu_rs::game::init(options).unwrap();
}

9
drshorizon/.cargo/config Normal file
View File

@ -0,0 +1,9 @@
[build]
target = ["aarch64-nintendo-switch"]
[target.aarch64-nintendo-switch]
cc = {path = "/opt/devkitpro/devkitA64/bin/aarch64-none-elf-gcc"}
cxx = {path = "/opt/devkitpro/devkitA64/bin/aarch64-none-elf-g++"}
ar = "/opt/devkitpro/devkitA64/bin/aarch64-none-elf-ar"
ranlib = {path = "/opt/devkitpro/devkitA64/bin/aarch64-none-elf-ranlib"}
linker = "/opt/devkitpro/devkitA64/bin/aarch64-none-elf-gcc"

31
drshorizon/Cargo.toml Normal file
View File

@ -0,0 +1,31 @@
[package]
name = "drshorizon"
description = "doukutsu-rs targeted for Nintendo Switch"
version = "0.1.0"
edition = "2021"
[profile.release]
opt-level = 3
codegen-units = 256
incremental = true
[profile.dev]
opt-level = 3
lto = "off"
overflow-checks = false
codegen-units = 256
incremental = true
[profile.dev.package."*"]
opt-level = 3
overflow-checks = false
incremental = true
[profile.dev.package."doukutsu-rs"]
opt-level = 3
overflow-checks = false
codegen-units = 256
incremental = true
[dependencies]
doukutsu-rs = { path = "../", default-features = false, features = ["default-base", "backend-horizon"] }

3
drshorizon/README.md Normal file
View File

@ -0,0 +1,3 @@
Experimental.
ld script and .specs taken from devkitPro

View File

@ -0,0 +1,45 @@
{
"arch": "aarch64",
"data-layout": "e-m:e-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128",
"dynamic-linking": true,
"disable-redzone": true,
"env": "newlib",
"executables": true,
"exe-suffix": ".elf",
"features": "+a57,+strict-align,+crc,+crypto",
"has-rpath": false,
"has-thread-local": false,
"linker": "/opt/devkitpro/devkitA64/bin/aarch64-none-elf-gcc",
"linker-flavor": "gcc",
"llvm-target": "aarch64-unknown-none",
"max-atomic-width": 128,
"no-default-libraries": false,
"os": "horizon",
"panic-strategy": "abort",
"position-independent-executables": true,
"pre-link-args": {
"gcc": [
"-fPIC",
"-specs",
"aarch64_nintendo_switch.specs",
"-T",
"aarch64_nintendo_switch.ld",
"-L",
"/opt/devkitpro/portlibs/switch/lib",
"-L",
"/opt/devkitpro/libnx/lib",
"-I",
"/opt/devkitpro/libnx/include"
]
},
"relocation-model": "pic",
"requires-uwtable": true,
"target-c-int-width": "32",
"target-endian": "little",
"target-family": [
"unix"
],
"target-pointer-width": "64",
"trap-unreachable": true,
"vendor": "nintendo"
}

View File

@ -0,0 +1,200 @@
OUTPUT_ARCH(aarch64)
ENTRY(_start)
PHDRS
{
code PT_LOAD FLAGS(5) /* Read | Execute */;
rodata PT_LOAD FLAGS(4) /* Read */;
data PT_LOAD FLAGS(6) /* Read | Write */;
dyn PT_DYNAMIC;
}
SECTIONS
{
/* =========== CODE section =========== */
PROVIDE(__start__ = 0x0);
. = __start__;
__code_start = . ;
.text :
{
HIDDEN(__text_start = .);
KEEP (*(.crt0))
*(.text.unlikely .text.*_unlikely .text.unlikely.*)
*(.text.exit .text.exit.*)
*(.text.startup .text.startup.*)
*(.text.hot .text.hot.*)
*(.text .stub .text.* .gnu.linkonce.t.*)
. = ALIGN(8);
} :code
.init :
{
KEEP( *(.init) )
. = ALIGN(8);
} :code
.plt :
{
*(.plt)
*(.iplt)
. = ALIGN(8);
} :code
.fini :
{
KEEP( *(.fini) )
. = ALIGN(8);
} :code
/* =========== RODATA section =========== */
. = ALIGN(0x1000);
__rodata_start = . ;
.nx-module-name : { KEEP (*(.nx-module-name)) } :rodata
.rodata :
{
*(.rodata .rodata.* .gnu.linkonce.r.*)
. = ALIGN(8);
} :rodata
.eh_frame_hdr : { __eh_frame_hdr_start = .; *(.eh_frame_hdr) *(.eh_frame_entry .eh_frame_entry.*) __eh_frame_hdr_end = .; } :rodata
.eh_frame : ONLY_IF_RO { KEEP (*(.eh_frame)) *(.eh_frame.*) } :rodata
.gcc_except_table : ONLY_IF_RO { *(.gcc_except_table .gcc_except_table.*) } :rodata
.gnu_extab : ONLY_IF_RO { *(.gnu_extab*) } : rodata
HIDDEN(__dynamic_start = .);
.dynamic : { *(.dynamic) } :rodata :dyn
.dynsym : { *(.dynsym) } :rodata
.dynstr : { *(.dynstr) } :rodata
.rela.dyn : { *(.rela.*) } :rodata
.interp : { *(.interp) } :rodata
.hash : { *(.hash) } :rodata
.gnu.hash : { *(.gnu.hash) } :rodata
.gnu.version : { *(.gnu.version) } :rodata
.gnu.version_d : { *(.gnu.version_d) } :rodata
.gnu.version_r : { *(.gnu.version_r) } :rodata
.note.gnu.build-id : { *(.note.gnu.build-id) } :rodata
/* =========== DATA section =========== */
. = ALIGN(0x1000);
__data_start = . ;
.eh_frame : ONLY_IF_RW { KEEP (*(.eh_frame)) *(.eh_frame.*) } :data
.gcc_except_table : ONLY_IF_RW { *(.gcc_except_table .gcc_except_table.*) } :data
.gnu_extab : ONLY_IF_RW { *(.gnu_extab*) } : data
.exception_ranges : ONLY_IF_RW { *(.exception_ranges .exception_ranges*) } :data
.tdata ALIGN(8) :
{
__tdata_lma = .;
*(.tdata .tdata.* .gnu.linkonce.td.*)
. = ALIGN(8);
__tdata_lma_end = .;
} :data
.tbss ALIGN(8) :
{
*(.tbss .tbss.* .gnu.linkonce.tb.*) *(.tcommon)
. = ALIGN(8);
} :data
.preinit_array ALIGN(8) :
{
PROVIDE (__preinit_array_start = .);
KEEP (*(.preinit_array))
PROVIDE (__preinit_array_end = .);
} :data
.init_array ALIGN(8) :
{
PROVIDE (__init_array_start = .);
KEEP( *(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)) )
KEEP( *(.init_array .ctors) )
PROVIDE (__init_array_end = .);
} :data
.fini_array ALIGN(8) :
{
PROVIDE (__fini_array_start = .);
KEEP( *(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*)) )
KEEP( *(.fini_array .dtors) )
PROVIDE (__fini_array_end = .);
} :data
__got_start__ = .;
.got : { *(.got) *(.igot) } :data
.got.plt : { *(.got.plt) *(.igot.plt) } :data
__got_end__ = .;
.data ALIGN(8) :
{
*(.data .data.* .gnu.linkonce.d.*)
SORT(CONSTRUCTORS)
} :data
__bss_start__ = .;
.bss ALIGN(8) :
{
HIDDEN(__bss_start = .);
*(.dynbss)
*(.bss .bss.* .gnu.linkonce.b.*)
*(COMMON)
. = ALIGN(8);
/* Reserve space for the TLS segment of the main thread */
__tls_start = .;
. += + SIZEOF(.tdata) + SIZEOF(.tbss);
__tls_end = .;
HIDDEN(__bss_end = .);
} :data
__bss_end__ = .;
__end__ = ABSOLUTE(.) ;
. = ALIGN(0x1000);
__argdata__ = ABSOLUTE(.) ;
/* ==================
==== Metadata ====
================== */
/* Discard sections that difficult post-processing */
/DISCARD/ : { *(.group .comment .note) }
/* Stabs debugging sections. */
.stab 0 : { *(.stab) }
.stabstr 0 : { *(.stabstr) }
.stab.excl 0 : { *(.stab.excl) }
.stab.exclstr 0 : { *(.stab.exclstr) }
.stab.index 0 : { *(.stab.index) }
.stab.indexstr 0 : { *(.stab.indexstr) }
/* DWARF debug sections.
Symbols in the DWARF debugging sections are relative to the beginning
of the section so we begin them at 0. */
/* DWARF 1 */
.debug 0 : { *(.debug) }
.line 0 : { *(.line) }
/* GNU DWARF 1 extensions */
.debug_srcinfo 0 : { *(.debug_srcinfo) }
.debug_sfnames 0 : { *(.debug_sfnames) }
/* DWARF 1.1 and DWARF 2 */
.debug_aranges 0 : { *(.debug_aranges) }
.debug_pubnames 0 : { *(.debug_pubnames) }
/* DWARF 2 */
.debug_info 0 : { *(.debug_info) }
.debug_abbrev 0 : { *(.debug_abbrev) }
.debug_line 0 : { *(.debug_line) }
.debug_frame 0 : { *(.debug_frame) }
.debug_str 0 : { *(.debug_str) }
.debug_loc 0 : { *(.debug_loc) }
.debug_macinfo 0 : { *(.debug_macinfo) }
}

View File

@ -0,0 +1,8 @@
%rename link old_link
*link:
%(old_link) -pie --no-dynamic-linker --spare-dynamic-tags=0 -z text -z nodynamic-undefined-weak --build-id=sha1 --nx-module-name
*startfile:
crti%O%s crtbegin%O%s --require-defined=main

5
drshorizon/build.rs Normal file
View File

@ -0,0 +1,5 @@
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rustc-link-lib=dylib=nx");
}

33
drshorizon/build_debug.sh Executable file
View File

@ -0,0 +1,33 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
set -e
DARK_GRAY=$(tput setaf 8)
YELLOW=$(tput bold)$(tput setaf 3)
RESET=$(tput sgr0)
function message() {
echo "${DARK_GRAY}----${RESET} ${YELLOW}$*${RESET}"
}
message "Compiling shaders..."
uam -s vert -o ../src/framework/shaders/deko3d/vertex_basic.dksh ../src/framework/shaders/deko3d/vertex_basic.glsl
uam -s frag -o ../src/framework/shaders/deko3d/fragment_textured.dksh ../src/framework/shaders/deko3d/fragment_textured.glsl
uam -s frag -o ../src/framework/shaders/deko3d/fragment_color.dksh ../src/framework/shaders/deko3d/fragment_color.glsl
message "Building crate..."
rustup run rust-switch cargo build -Z build-std=core,alloc,std,panic_abort --target aarch64-nintendo-switch.json
rm -f target/aarch64-nintendo-switch/debug/drshorizon.nro
rm -f target/aarch64-nintendo-switch/debug/drshorizon.nacp
message "Creating NACP..."
nacptool --create 'doukutsu-rs' 'doukutsu-rs contributors' '0.101.0' target/aarch64-nintendo-switch/debug/drshorizon.nacp
message "Running elf2nro..."
elf2nro target/aarch64-nintendo-switch/debug/drshorizon.elf target/aarch64-nintendo-switch/debug/drshorizon.nro \
--icon=../res/nx_icon.jpg \
--nacp=target/aarch64-nintendo-switch/debug/drshorizon.nacp
message "done!"

33
drshorizon/build_release.sh Executable file
View File

@ -0,0 +1,33 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
set -e
DARK_GRAY=$(tput setaf 8)
YELLOW=$(tput bold)$(tput setaf 3)
RESET=$(tput sgr0)
function message() {
echo "${DARK_GRAY}----${RESET} ${YELLOW}$*${RESET}"
}
message "Compiling shaders..."
uam -s vert -o ../src/framework/shaders/deko3d/vertex_basic.dksh ../src/framework/shaders/deko3d/vertex_basic.glsl
uam -s frag -o ../src/framework/shaders/deko3d/fragment_textured.dksh ../src/framework/shaders/deko3d/fragment_textured.glsl
uam -s frag -o ../src/framework/shaders/deko3d/fragment_color.dksh ../src/framework/shaders/deko3d/fragment_color.glsl
message "Building crate..."
rustup run rust-switch cargo build -Z build-std=core,alloc,std,panic_abort --target aarch64-nintendo-switch.json --release
rm -f target/aarch64-nintendo-switch/release/drshorizon.nro
rm -f target/aarch64-nintendo-switch/release/drshorizon.nacp
message "Creating NACP..."
nacptool --create 'doukutsu-rs' 'doukutsu-rs contributors' '0.101.0' target/aarch64-nintendo-switch/release/drshorizon.nacp
message "Running elf2nro..."
elf2nro target/aarch64-nintendo-switch/release/drshorizon.elf target/aarch64-nintendo-switch/release/drshorizon.nro \
--icon=../res/nx_icon.jpg \
--nacp=target/aarch64-nintendo-switch/release/drshorizon.nacp
message "done!"

47
drshorizon/src/main.rs Normal file
View File

@ -0,0 +1,47 @@
//#![feature(restricted_std)]
#[repr(C)]
pub struct PrintConsole {}
#[repr(C)]
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum ApmCpuBoostMode {
Normal = 0,
FastLoad = 1,
}
extern "C" {
fn consoleInit(unk: *mut PrintConsole) -> *mut PrintConsole;
fn consoleUpdate(unk: *mut PrintConsole);
fn socketInitialize(unk: *const std::ffi::c_void) -> u32;
fn nxlinkConnectToHost(redir_stdout: bool, redir_stderr: bool) -> i32;
fn appletSetCpuBoostMode(mode: ApmCpuBoostMode) -> u32;
static __text_start: u32;
}
fn main() {
unsafe {
// if socketInitialize(std::ptr::null()) == 0 {
// nxlinkConnectToHost(true, true);
// }
// appletSetCpuBoostMode(ApmCpuBoostMode::FastLoad);
std::env::set_var("RUST_BACKTRACE", "full");
println!("__text_start = {:#x}", (&__text_start) as *const _ as usize);
let options = doukutsu_rs::game::LaunchOptions { server_mode: false, editor: false };
let result = doukutsu_rs::game::init(options);
if let Err(e) = result {
println!("Initialization error: {}", e);
loop {
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
}
}

51
drshorizon/symbolize.js Normal file
View File

@ -0,0 +1,51 @@
// <reference types="node" />
const readline = require('readline');
const childProcess = require('child_process');
const rl = readline.createInterface({
input: process.stdin,
terminal: false
});
let textStart = 0;
const textStartRegex = /__text_start = 0x([0-9a-f]+)/i;
let symbolize = false;
if (process.argv.length <= 2) {
console.error('Usage: node symbolize.js <path to ELF file>');
process.exit(1);
}
const elfPath = process.argv[2];
rl.on('line', (line) => {
if (textStart === 0) {
const match = textStartRegex.exec(line);
if (match) {
textStart = parseInt(match[1], 16);
}
}
if (line.includes("stack backtrace:")) {
symbolize = true;
}
if (symbolize) {
const match = /0x([0-9a-f]+) - \<unknown\>/.exec(line);
if (match) {
const addr = parseInt(match[1], 16);
const relative = addr - textStart;
// run addr2line on the address
const addr2line = childProcess.spawnSync('addr2line', ['-e', elfPath, '-j', '.text', '-f', '-C', '0x' + relative.toString(16)]);
if (addr2line.status === 0) {
const output = addr2line.stdout.toString();
const lines = output.split('\n');
const [func, file] = lines;
line = line.replace(match[0], `0x${addr.toString(16)} - ${func} (${file})`);
}
}
}
console.log(line);
});

View File

@ -0,0 +1 @@
curl -o ./src/data/builtin/gamecontrollerdb.txt https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt

BIN
res/nx_icon.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
res/nx_icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
res/nx_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
res/sue.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,4 +1,4 @@
edition = "2018"
edition = "2021"
max_width = 120
use_small_heuristics = "Max"
newline_style = "Unix"

View File

@ -1,94 +0,0 @@
use std::collections::HashMap;
use std::io;
use byteorder::{ReadBytesExt, LE};
use crate::framework::error::GameError::ResourceLoadError;
use crate::framework::error::GameResult;
#[derive(Debug)]
pub struct BmChar {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
pub xoffset: i16,
pub yoffset: i16,
pub xadvance: i16,
pub page: u8,
pub chnl: u8,
}
#[derive(Debug)]
pub struct BMFont {
pub pages: u16,
pub font_size: i16,
pub line_height: u16,
pub base: u16,
pub chars: HashMap<char, BmChar>,
}
const MAGIC: [u8; 4] = [b'B', b'M', b'F', 3];
impl BMFont {
pub fn load_from<R: io::Read + io::Seek>(mut data: R) -> GameResult<Self> {
let mut magic = [0u8; 4];
let mut pages = 0u16;
let mut chars = HashMap::with_capacity(128);
let mut font_size = 0i16;
let mut line_height = 0u16;
let mut base = 0u16;
data.read_exact(&mut magic)?;
if magic != MAGIC {
return Err(ResourceLoadError("Invalid magic".to_owned()));
}
while let Ok(block_type) = data.read_u8() {
let length = data.read_u32::<LE>()?;
match block_type {
1 => {
font_size = data.read_i16::<LE>()?;
data.seek(io::SeekFrom::Current(length as i64 - 2))?;
}
2 => {
line_height = data.read_u16::<LE>()?;
base = data.read_u16::<LE>()?;
data.seek(io::SeekFrom::Current(4))?;
pages = data.read_u16::<LE>()?;
data.seek(io::SeekFrom::Current(length as i64 - 10))?;
}
3 | 5 => {
data.seek(io::SeekFrom::Current(length as i64))?;
}
4 => {
let count = length / 20;
for _ in 0..count {
let id = data.read_u32::<LE>()?;
let x = data.read_u16::<LE>()?;
let y = data.read_u16::<LE>()?;
let width = data.read_u16::<LE>()?;
let height = data.read_u16::<LE>()?;
let xoffset = data.read_i16::<LE>()?;
let yoffset = data.read_i16::<LE>()?;
let xadvance = data.read_i16::<LE>()?;
let page = data.read_u8()?;
let chnl = data.read_u8()?;
if let Some(chr) = std::char::from_u32(id) {
chars.insert(chr, BmChar { x, y, width, height, xoffset, yoffset, xadvance, page, chnl });
}
}
}
_ => {
return Err(ResourceLoadError("Unknown block type.".to_owned()));
}
}
}
Ok(Self { pages, font_size, line_height, base, chars })
}
}

View File

@ -1,200 +0,0 @@
use std::collections::HashSet;
use std::path::PathBuf;
use crate::bmfont::BMFont;
use crate::common::{Rect, FILE_TYPES};
use crate::engine_constants::EngineConstants;
use crate::framework::context::Context;
use crate::framework::error::GameError::ResourceLoadError;
use crate::framework::error::GameResult;
use crate::framework::filesystem;
use crate::texture_set::TextureSet;
pub struct BMFontRenderer {
font: BMFont,
pages: Vec<String>,
}
impl BMFontRenderer {
pub fn load(roots: &Vec<String>, desc_path: &str, ctx: &mut Context) -> GameResult<BMFontRenderer> {
let full_path = PathBuf::from(desc_path);
let desc_stem =
full_path.file_stem().ok_or_else(|| ResourceLoadError("Cannot extract the file stem.".to_owned()))?;
let stem = full_path.parent().unwrap_or(&full_path).join(desc_stem);
let font = BMFont::load_from(filesystem::open_find(ctx, roots, &full_path)?)?;
let mut pages = Vec::new();
let (zeros, _, _) = FILE_TYPES
.iter()
.map(|ext| (1, ext, format!("{}_0{}", stem.to_string_lossy(), ext)))
.find(|(_, _, path)| filesystem::exists_find(ctx, roots, &path))
.or_else(|| {
FILE_TYPES
.iter()
.map(|ext| (2, ext, format!("{}_00{}", stem.to_string_lossy(), ext)))
.find(|(_, _, path)| filesystem::exists_find(ctx, roots, &path))
})
.ok_or_else(|| ResourceLoadError(format!("Cannot find glyph atlas 0 for font: {:?}", desc_path)))?;
for i in 0..font.pages {
let page_path = format!("{}_{:02$}", stem.to_string_lossy(), i, zeros);
pages.push(page_path);
}
Ok(Self { font, pages })
}
pub fn line_height(&self, constants: &EngineConstants) -> f32 {
self.font.line_height as f32 * constants.font_scale
}
pub fn text_width<I: Iterator<Item = char>>(&self, iter: I, constants: &EngineConstants) -> f32 {
let mut offset_x = 0.0;
for chr in iter {
if let Some(glyph) = self.font.chars.get(&chr) {
offset_x += glyph.xadvance as f32 * constants.font_scale;
}
}
offset_x
}
pub fn draw_text<I: Iterator<Item = char>>(
&self,
iter: I,
x: f32,
y: f32,
constants: &EngineConstants,
texture_set: &mut TextureSet,
ctx: &mut Context,
) -> GameResult {
self.draw_colored_text(iter, x, y, (255, 255, 255, 255), constants, texture_set, ctx)
}
pub fn draw_text_with_shadow<I: Iterator<Item = char> + Clone>(
&self,
iter: I,
x: f32,
y: f32,
constants: &EngineConstants,
texture_set: &mut TextureSet,
ctx: &mut Context,
) -> GameResult {
self.draw_colored_text(iter.clone(), x + 1.0, y + 1.0, (0, 0, 0, 150), constants, texture_set, ctx)?;
self.draw_colored_text(iter, x, y, (255, 255, 255, 255), constants, texture_set, ctx)
}
pub fn draw_colored_text_with_shadow_scaled<I: Iterator<Item = char> + Clone>(
&self,
iter: I,
x: f32,
y: f32,
scale: f32,
color: (u8, u8, u8, u8),
constants: &EngineConstants,
texture_set: &mut TextureSet,
ctx: &mut Context,
) -> GameResult {
self.draw_colored_text_scaled(
iter.clone(),
x + scale,
y + scale,
scale,
(0, 0, 0, 150),
constants,
texture_set,
ctx,
)?;
self.draw_colored_text_scaled(iter, x, y, scale, color, constants, texture_set, ctx)
}
pub fn draw_colored_text_scaled<I: Iterator<Item = char>>(
&self,
iter: I,
x: f32,
y: f32,
scale: f32,
color: (u8, u8, u8, u8),
constants: &EngineConstants,
texture_set: &mut TextureSet,
ctx: &mut Context,
) -> GameResult {
if self.pages.len() == 1 {
let batch = texture_set.get_or_load_batch(ctx, constants, self.pages.get(0).unwrap())?;
let mut offset_x = x;
for chr in iter {
if let Some(glyph) = self.font.chars.get(&chr) {
batch.add_rect_scaled_tinted(
offset_x + (glyph.xoffset as f32 * constants.font_scale),
y + (glyph.yoffset as f32 * constants.font_scale),
color,
constants.font_scale,
constants.font_scale,
&Rect::new_size(glyph.x as u16, glyph.y as u16, glyph.width as u16, glyph.height as u16),
);
offset_x += glyph.xadvance as f32 * constants.font_scale;
}
}
batch.draw(ctx)?;
} else {
let mut pages = HashSet::new();
let mut chars = Vec::new();
for chr in iter {
if let Some(glyph) = self.font.chars.get(&chr) {
pages.insert(glyph.page);
chars.push((chr, glyph));
}
}
for page in pages {
let page_tex = if let Some(p) = self.pages.get(page as usize) {
p
} else {
continue;
};
let batch = texture_set.get_or_load_batch(ctx, constants, page_tex)?;
let mut offset_x = x;
for (_chr, glyph) in chars.iter() {
if glyph.page == page {
batch.add_rect_scaled_tinted(
offset_x + (glyph.xoffset as f32 * constants.font_scale),
y + (glyph.yoffset as f32 * constants.font_scale),
color,
constants.font_scale * scale,
constants.font_scale * scale,
&Rect::new_size(glyph.x as u16, glyph.y as u16, glyph.width as u16, glyph.height as u16),
);
}
offset_x += scale * (glyph.xadvance as f32 * constants.font_scale);
}
batch.draw(ctx)?;
}
}
Ok(())
}
pub fn draw_colored_text<I: Iterator<Item = char>>(
&self,
iter: I,
x: f32,
y: f32,
color: (u8, u8, u8, u8),
constants: &EngineConstants,
texture_set: &mut TextureSet,
ctx: &mut Context,
) -> GameResult {
self.draw_colored_text_scaled(iter, x, y, 1.0, color, constants, texture_set, ctx)
}
}

View File

@ -1,116 +0,0 @@
{
"common": {
"name": "doukutsu-rs",
"back": "< Back",
"yes": "Yes",
"no": "No",
"on": "ON",
"off": "OFF"
},
"menus": {
"main_menu": {
"start": "Start Game",
"challenges": "Challenges",
"options": "Options",
"editor": "Editor",
"jukebox": "Jukebox",
"quit": "Quit"
},
"pause_menu": {
"resume": "Resume",
"retry": "Retry",
"options": "Options",
"title": "Title",
"title_confirm": "Title?",
"quit": "Quit",
"quit_confirm": "Quit?"
},
"save_menu": {
"new": "New Save",
"delete_info": "Press Right to Delete",
"delete_confirm": "Delete?"
},
"difficulty_menu": {
"title": "Select Difficulty",
"easy": "Easy",
"normal": "Normal",
"hard": "Hard"
},
"challenge_menu": {
"start": "Start",
"no_replay": "No Replay",
"replay_best": "Replay Best",
"delete_replay": "Delete Replay"
},
"options_menu": {
"graphics": "Graphics...",
"graphics_menu": {
"lighting_effects": "Lighting effects:",
"weapon_light_cone": "Weapon light cone:",
"motion_interpolation": "Motion interpolation:",
"subpixel_scrolling": "Subpixel scrolling:",
"original_textures": "Original textures:",
"seasonal_textures": "Seasonal textures:",
"renderer": "Renderer:",
"vsync_mode": {
"entry": "V-Sync:",
"uncapped": "Uncapped",
"uncapped_desc": "V-Sync Off.",
"vsync": "Enabled",
"vsync_desc": "V-Sync On.",
"vrr_1x": "Variable Refresh Rate (1x)",
"vrr_1x_desc": "Uses (G-/Free)Sync if available.",
"vrr_2x": "Variable Refresh Rate (2x)",
"vrr_2x_desc": "Uses (G-/Free)Sync if available.",
"vrr_3x": "Variable Refresh Rate (3x)",
"vrr_3x_desc": "Uses (G-/Free)Sync if available."
}
},
"sound": "Sound...",
"sound_menu": {
"music_volume": "Music Volume",
"effects_volume": "Effects Volume",
"bgm_interpolation": {
"entry": "BGM Interpolation:",
"linear": "Linear",
"linear_desc": "Fast, similar to freeware on Vista+",
"cosine": "Cosine",
"cosine_desc": "Cosine interpolation",
"cubic": "Cubic",
"cubic_desc": "Cubic interpolation",
"linear_lp": "Linear+LP",
"linear_lp_desc": "Slowest, similar to freeware on XP",
"nearest": "Nearest",
"nearest_desc": "Fastest, lowest quality"
},
"soundtrack": "Soundtrack: {soundtrack}"
},
"language": "Language...",
"game_timing": {
"entry": "Game timing:",
"50tps": "50tps (freeware)",
"60tps": "60tps (CS+)"
}
}
},
"soundtrack": {
"organya": "Organya",
"remastered": "Remastered",
"new": "New",
"famitracks": "Famitracks"
},
"game": {
"cutscene_skip": "Hold {key} to skip the cutscene"
}
}

View File

@ -1,105 +0,0 @@
{
"common": {
"name": "doukutsu-rs",
"back": "< 戻る",
"yes": "はい",
"no": "いいえ",
"on": "オン",
"off": "オフ"
},
"menus": {
"main_menu": {
"start": "ゲームスタート",
"challenges": "チャレンジ",
"options": "設定",
"editor": "レベルエディタ",
"jukebox": "ジュークボックス",
"quit": "辞める"
},
"pause_menu": {
"resume": "再開",
"retry": "リトライ",
"options": "設定",
"title": "メインメニュー",
"title_confirm": "メインメニュー?",
"quit": "辞める",
"quit_confirm": "辞める?"
},
"save_menu": {
"new": "新しいデータ",
"delete_info": "右矢印キーで削除",
"delete_confirm": "消去?"
},
"difficulty_menu": {
"title": "難易度選択",
"easy": "簡単",
"normal": "普通",
"hard": "難しい"
},
"challenge_menu": {
"start": "スタート",
"no_replay": "ノーリプレイ",
"replay_best": "ベストプレイを再生",
"delete_replay": "リプレイを削除"
},
"options_menu": {
"graphics": "グラフィック",
"graphics_menu": {
"lighting_effects": "ライティング効果:",
"weapon_light_cone": "兵器のライトコーン:",
"motion_interpolation": "モーション補間:",
"subpixel_scrolling": "サブピクセルスクロール:",
"original_textures": "オリジナルテクスチャ:",
"seasonal_textures": "季節ものテクスチャ:",
"renderer": "レンダラ:",
"vsync_mode": {
"entry": "V-Sync:",
"uncapped": "Uncapped",
"uncapped_desc": "V-Sync Off.",
"vsync": "Enabled",
"vsync_desc": "V-Sync On.",
"vrr_1x": "Variable Refresh Rate (1x)",
"vrr_1x_desc": "Uses (G-/Free)Sync if available.",
"vrr_2x": "Variable Refresh Rate (2x)",
"vrr_2x_desc": "Uses (G-/Free)Sync if available.",
"vrr_3x": "Variable Refresh Rate (3x)",
"vrr_3x_desc": "Uses (G-/Free)Sync if available."
}
},
"sound": "サウンド",
"sound_menu": {
"music_volume": "BGM音量",
"effects_volume": "サウンド音量",
"bgm_interpolation": {
"entry": "BGM内挿",
"linear": "線形補間",
"linear_desc": "速い、フリーウェア版に近いVista+",
"cosine": "余弦",
"cosine_desc": "余弦補間",
"cubic": "立方体",
"cubic_desc": "立方体補間",
"linear_lp": "線形補間+LP",
"linear_lp_desc": "最も遅い、フリーウェア版に近いXP",
"nearest": "最近傍",
"nearest_desc": "最速、最低品質"
},
"soundtrack": "サウンドトラック: {soundtrack}"
},
"language": "言語",
"game_timing": {
"entry": "ゲームのタイミング:",
"50tps": "50tps (freeware)",
"60tps": "60tps (CS+)"
}
}
},
"soundtrack": {
"organya": "オルガーニャ",
"remastered": "リマスター",
"new": "新",
"famitracks": "ファミトラック"
},
"game": {
"cutscene_skip": "{key} を押し続け、カットシーンをスキップ"
}
}

View File

@ -3,12 +3,12 @@ use std::time::{SystemTime, UNIX_EPOCH};
use lazy_static::lazy_static;
use num_traits::{abs, Num};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde::de::{SeqAccess, Visitor};
use serde::ser::SerializeTupleStruct;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use crate::bitfield;
use crate::texture_set::G_MAG;
use crate::graphics::texture_set::G_MAG;
/// Multiply cave story degrees (0-255, which corresponds to 0°-360°) with this constant to get
/// respective value in radians.
@ -47,7 +47,7 @@ bitfield! {
pub in_water, set_in_water: 8;
pub weapon_hit_block, set_weapon_hit_block: 9; // 0x200
pub hit_by_spike, set_hit_by_spike: 10; // 0x400
pub water_splash_facing_right, set_water_splash_facing_right: 11; // 0x800
pub bloody_droplets, set_bloody_droplets: 11; // 0x800
pub force_left, set_force_left: 12; // 0x1000
pub force_up, set_force_up: 13; // 0x2000
pub force_right, set_force_right: 14; // 0x4000
@ -302,8 +302,8 @@ impl<T: Num + PartialOrd + Copy> Rect<T> {
impl<T: Num + PartialOrd + Copy + Serialize> Serialize for Rect<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
where
S: Serializer,
{
let mut state = serializer.serialize_tuple_struct("Rect", 4)?;
state.serialize_field(&self.left)?;

View File

@ -1,7 +1,10 @@
use crate::common::{Color, Rect};
use crate::frame::Frame;
use crate::stage::{BackgroundType, Stage, StageTexturePaths};
use crate::{graphics, Context, GameResult, SharedGameState};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics;
use crate::game::frame::Frame;
use crate::game::shared_game_state::SharedGameState;
use crate::game::stage::{BackgroundType, Stage, StageTexturePaths};
pub struct Background {
pub tick: usize,

View File

@ -1,11 +1,11 @@
use crate::common::Rect;
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::npc::boss::BossNPC;
use crate::npc::list::NPCList;
use crate::shared_game_state::SharedGameState;
use crate::game::frame::Frame;
use crate::game::shared_game_state::SharedGameState;
use crate::game::npc::boss::BossNPC;
use crate::game::npc::list::NPCList;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
@ -25,13 +25,7 @@ pub struct BossLifeBar {
impl BossLifeBar {
pub fn new() -> BossLifeBar {
BossLifeBar {
target: BossLifeTarget::None,
life: 0,
max_life: 0,
prev_life: 0,
counter: 0,
}
BossLifeBar { target: BossLifeTarget::None, life: 0, max_life: 0, prev_life: 0, counter: 0 }
}
pub fn set_npc_target(&mut self, npc_id: u16, npc_list: &NPCList) {
@ -49,6 +43,91 @@ impl BossLifeBar {
self.max_life = self.life;
self.prev_life = self.life;
}
fn draw_regular(&self, state: &mut SharedGameState, ctx: &mut Context, _frame: &Frame) -> GameResult {
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?;
let box_length = 256;
let bar_length = box_length - 58;
let text_rect = Rect::new_size(0, 48, 32, 8);
let box_rect1 = Rect::new_size(0, 0, 244, 8);
let box_rect2 = Rect::new_size(0, 16, 244, 8);
let mut rect_prev_bar = Rect::new_size(0, 32, 232, 8);
let mut rect_life_bar = Rect::new_size(0, 24, 232, 8);
rect_prev_bar.right = ((self.prev_life as u32 * bar_length) / self.max_life as u32).min(bar_length) as u16;
rect_life_bar.right = ((self.life as u32 * bar_length) / self.max_life as u32).min(bar_length) as u16;
batch.add_rect(
((state.canvas_size.0 - box_length as f32) / 2.0).floor(),
state.canvas_size.1 - 20.0,
&box_rect1,
);
batch.add_rect(
((state.canvas_size.0 - box_length as f32) / 2.0).floor(),
state.canvas_size.1 - 12.0,
&box_rect2,
);
batch.add_rect(
((state.canvas_size.0 - box_length as f32) / 2.0).floor(),
state.canvas_size.1 - 20.0,
&box_rect1,
);
batch.add_rect(
((state.canvas_size.0 - box_length as f32) / 2.0 + 40.0).floor(),
state.canvas_size.1 - 16.0,
&rect_prev_bar,
);
batch.add_rect(
((state.canvas_size.0 - box_length as f32) / 2.0 + 40.0).floor(),
state.canvas_size.1 - 16.0,
&rect_life_bar,
);
batch.add_rect(
((state.canvas_size.0 - box_length as f32) / 2.0 + 8.0).floor(),
state.canvas_size.1 - 16.0,
&text_rect,
);
batch.draw(ctx)?;
Ok(())
}
fn draw_nx(&self, state: &mut SharedGameState, ctx: &mut Context, _frame: &Frame) -> GameResult {
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?;
let box_length = 148;
let bar_length = box_length - 52;
let text_rect = Rect::new_size(0, 48, 32, 8);
let box_rect1 = Rect::new_size(0, 0, 136, 8);
let box_rect2 = Rect::new_size(0, 16, 136, 8);
let box_rect3 = Rect::new_size(238, 0, 6, 8);
let box_rect4 = Rect::new_size(238, 16, 6, 8);
let mut rect_prev_bar = Rect::new_size(0, 32, 124, 8);
let mut rect_life_bar = Rect::new_size(0, 24, 124, 8);
rect_prev_bar.right = ((self.prev_life as u32 * bar_length) / self.max_life as u32).min(bar_length) as u16;
rect_life_bar.right = ((self.life as u32 * bar_length) / self.max_life as u32).min(bar_length) as u16;
let base_x = state.canvas_size.0 - box_length as f32;
batch.add_rect((base_x - 6.0).floor(), state.canvas_size.1 - 20.0, &box_rect1);
batch.add_rect((base_x - 6.0).floor(), state.canvas_size.1 - 12.0, &box_rect2);
batch.add_rect((base_x - 6.0).floor(), state.canvas_size.1 - 20.0, &box_rect1);
batch.add_rect((state.canvas_size.0 - 18.0).floor(), state.canvas_size.1 - 20.0, &box_rect3);
batch.add_rect((state.canvas_size.0 - 18.0).floor(), state.canvas_size.1 - 12.0, &box_rect4);
batch.add_rect((state.canvas_size.0 - 18.0).floor(), state.canvas_size.1 - 20.0, &box_rect3);
batch.add_rect((base_x + 34.0).floor(), state.canvas_size.1 - 16.0, &rect_prev_bar);
batch.add_rect((base_x + 34.0).floor(), state.canvas_size.1 - 16.0, &rect_life_bar);
batch.add_rect((base_x + 2.0).floor(), state.canvas_size.1 - 16.0, &text_rect);
batch.draw(ctx)?;
Ok(())
}
}
impl GameEntity<(&NPCList, &BossNPC)> for BossLifeBar {
@ -86,35 +165,9 @@ impl GameEntity<(&NPCList, &BossNPC)> for BossLifeBar {
return Ok(());
}
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?;
let box_length = 256;
let bar_length = box_length - 58;
let text_rect = Rect::new_size(0, 48, 32, 8);
let box_rect1 = Rect::new_size(0, 0, 244, 8);
let box_rect2 = Rect::new_size(0, 16, 244, 8);
let mut rect_prev_bar = Rect::new_size(0, 32, 232, 8);
let mut rect_life_bar = Rect::new_size(0, 24, 232, 8);
rect_prev_bar.right = ((self.prev_life as u32 * bar_length) / self.max_life as u32).min(bar_length) as u16;
rect_life_bar.right = ((self.life as u32 * bar_length) / self.max_life as u32).min(bar_length) as u16;
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0).floor(),
state.canvas_size.1 - 20.0, &box_rect1);
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0).floor(),
state.canvas_size.1 - 12.0, &box_rect2);
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0).floor(),
state.canvas_size.1 - 20.0, &box_rect1);
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0 + 40.0).floor(),
state.canvas_size.1 - 16.0, &rect_prev_bar);
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0 + 40.0).floor(),
state.canvas_size.1 - 16.0, &rect_life_bar);
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0 + 8.0).floor(),
state.canvas_size.1 - 16.0, &text_rect);
batch.draw(ctx)?;
Ok(())
match state.constants.is_switch {
true => self.draw_nx(state, ctx, _frame),
false => self.draw_regular(state, ctx, _frame),
}
}
}

View File

@ -0,0 +1,86 @@
use crate::entity::GameEntity;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::game::frame::Frame;
use crate::game::shared_game_state::SharedGameState;
use crate::graphics::font::Font;
#[derive(Clone, Copy)]
pub struct CompactJukebox {
song_id: usize,
shown: bool,
}
impl CompactJukebox {
pub fn new() -> CompactJukebox {
CompactJukebox { song_id: 0, shown: false }
}
pub fn change_song(&mut self, song_id: usize, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
self.song_id = song_id;
if self.song_id == state.sound_manager.current_song() {
return Ok(());
}
return state.sound_manager.play_song(song_id, &state.constants, &state.settings, ctx, false);
}
pub fn next_song(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
let mut new_song_id = if self.song_id == state.constants.music_table.len() - 1 { 1 } else { self.song_id + 1 };
// skip ika if soundtrack is not set to new
if self.is_ika_unavailable(new_song_id, state) {
new_song_id = 1;
}
self.change_song(new_song_id, state, ctx)
}
pub fn prev_song(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
let mut new_song_id = if self.song_id == 1 { state.constants.music_table.len() - 1 } else { self.song_id - 1 };
// skip ika if soundtrack is not set to new
if self.is_ika_unavailable(new_song_id, state) {
new_song_id = 42;
}
self.change_song(new_song_id, state, ctx)
}
pub fn show(&mut self) {
self.shown = true;
}
pub fn is_shown(&self) -> bool {
self.shown
}
fn is_ika_unavailable(&self, song_id: usize, state: &SharedGameState) -> bool {
song_id == 43 && state.settings.soundtrack != "New"
}
}
impl GameEntity<&mut Context> for CompactJukebox {
fn tick(&mut self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult {
Ok(())
}
fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, _frame: &Frame) -> GameResult {
if !self.shown {
return Ok(());
}
let text = format!("< {:02} >", self.song_id);
let font_builder = state.font.builder();
let text_width = font_builder.compute_width(&text);
let x = state.canvas_size.0 as f32 - text_width - 15.0;
let y = state.canvas_size.1 as f32 - 15.0;
font_builder.x(x).y(y).shadow(true).draw(&text, ctx, &state.constants, &mut state.texture_set)?;
Ok(())
}
}

View File

@ -1,11 +1,12 @@
use crate::common::{Color, Rect};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics;
use crate::scripting::tsc::text_script::IllustrationState;
use crate::shared_game_state::SharedGameState;
use crate::game::frame::Frame;
use crate::game::scripting::tsc::text_script::IllustrationState;
use crate::game::shared_game_state::SharedGameState;
use crate::graphics::font::Font;
pub struct Credits {}
@ -65,18 +66,59 @@ impl GameEntity<()> for Credits {
let y = ((line.cast_id / 13) & 0xff) * 24;
let rect = Rect::new_size(x, y, 24, 24);
batch.add_rect(line.pos_x - 24.0, line.pos_y - 8.0, &rect);
if state.more_rust && line.cast_id == 1 {
// sue with more rust
batch.add_rect_tinted(line.pos_x - 24.0, line.pos_y - 8.0, (200, 200, 255, 255), &rect);
} else {
batch.add_rect(line.pos_x - 24.0, line.pos_y - 8.0, &rect);
}
}
batch.draw(ctx)?;
if state.more_rust {
// draw sue's headband separately because rust doesn't let me mutate the texture set multiple times at once
let headband_spritesheet = {
let base = if state.settings.original_textures { "ogph" } else { "plus" };
format!("headband/{}/Casts", base)
};
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, headband_spritesheet.as_str())?;
for line in &state.creditscript_vm.lines {
if line.cast_id != 1 {
continue;
}
let x = (line.cast_id % 13) * 24;
let y = ((line.cast_id / 13) & 0xff) * 24;
let rect = Rect::new_size(x, y, 24, 24);
batch.add_rect(line.pos_x - 24.0, line.pos_y - 8.0, &rect);
break;
}
batch.draw(ctx)?;
}
for line in &state.creditscript_vm.lines {
state.font.draw_text_with_shadow(
line.text.chars(),
line.pos_x,
line.pos_y,
let mut text_ovr = None;
if state.more_rust {
text_ovr = Some(line.text.replace("Sue Sakamoto", "Crabby Sue"));
}
let mut text = line.text.as_str();
if let Some(ovr) = text_ovr.as_ref() {
text = ovr.as_str();
}
state.font.builder().position(line.pos_x, line.pos_y).shadow(true).draw(
text,
ctx,
&state.constants,
&mut state.texture_set,
ctx,
)?;
}

View File

@ -1,7 +1,7 @@
use crate::common::Rect;
use crate::shared_game_state::SharedGameState;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::game::shared_game_state::SharedGameState;
#[derive(Debug, EnumIter, PartialEq, Eq, Hash, Copy, Clone)]
pub enum Alignment {

View File

@ -1,9 +1,9 @@
use crate::common::{FadeDirection, FadeState, Rect};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::shared_game_state::SharedGameState;
use crate::game::frame::Frame;
use crate::game::shared_game_state::SharedGameState;
pub struct Fade;

View File

@ -2,12 +2,12 @@ use std::ops::Deref;
use crate::common::{Color, Rect};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::graphics;
use crate::scripting::tsc::text_script::TextScriptExecutionState;
use crate::shared_game_state::SharedGameState;
use crate::framework::graphics;
use crate::game::frame::Frame;
use crate::game::shared_game_state::SharedGameState;
use crate::game::scripting::tsc::text_script::TextScriptExecutionState;
pub struct FallingIsland {}

View File

@ -1,10 +1,10 @@
use crate::common::{Color, Rect};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics;
use crate::shared_game_state::SharedGameState;
use crate::game::frame::Frame;
use crate::game::shared_game_state::SharedGameState;
pub enum FlashState {
None,
@ -83,7 +83,7 @@ impl GameEntity<()> for Flash {
left: 0,
top: ((cen_y - width) * state.scale) as isize,
right: (state.canvas_size.0 * state.scale) as isize,
bottom: ((cen_y + width) * state.scale) as isize
bottom: ((cen_y + width) * state.scale) as isize,
};
graphics::draw_rect(ctx, rect, WHITE)?;
@ -93,7 +93,7 @@ impl GameEntity<()> for Flash {
left: ((cen_x - width) * state.scale) as isize,
top: 0,
right: ((cen_x + width) * state.scale) as isize,
bottom: (state.canvas_size.1 * state.scale) as isize
bottom: (state.canvas_size.1 * state.scale) as isize,
};
graphics::draw_rect(ctx, rect, WHITE)?;

View File

@ -1,13 +1,14 @@
use crate::common::Rect;
use crate::components::draw_common::{draw_number, Alignment};
use crate::components::draw_common::{Alignment, draw_number};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics::screen_insets_scaled;
use crate::inventory::Inventory;
use crate::player::Player;
use crate::shared_game_state::SharedGameState;
use crate::game::frame::Frame;
use crate::game::inventory::Inventory;
use crate::game::shared_game_state::SharedGameState;
use crate::game::player::Player;
use crate::game::weapon::WeaponType;
pub struct HUD {
pub alignment: Alignment,
@ -111,12 +112,26 @@ impl GameEntity<(&Player, &mut Inventory)> for HUD {
if player.controller.trigger_next_weapon() {
state.sound_manager.play_sfx(4);
inventory.next_weapon();
if let Some(weapon) = inventory.get_current_weapon_mut() {
if weapon.wtype == WeaponType::Spur {
weapon.reset_xp();
}
}
self.weapon_x_pos = 32;
}
if player.controller.trigger_prev_weapon() {
state.sound_manager.play_sfx(4);
inventory.prev_weapon();
if let Some(weapon) = inventory.get_current_weapon_mut() {
if weapon.wtype == WeaponType::Spur {
weapon.reset_xp();
}
}
self.weapon_x_pos = 0;
}
}

View File

@ -1,16 +1,16 @@
use crate::common::Rect;
use crate::components::draw_common::{draw_number, Alignment};
use crate::components::draw_common::{Alignment, draw_number};
use crate::components::hud::HUD;
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::game::frame::Frame;
use crate::game::inventory::Inventory;
use crate::game::shared_game_state::SharedGameState;
use crate::input::touch_controls::TouchControlType;
use crate::inventory::Inventory;
use crate::player::Player;
use crate::scripting::tsc::text_script::{ScriptMode, TextScriptExecutionState};
use crate::shared_game_state::SharedGameState;
use crate::weapon::{WeaponLevel, WeaponType};
use crate::game::player::Player;
use crate::game::scripting::tsc::text_script::{ScriptMode, TextScriptExecutionState};
use crate::game::weapon::{WeaponLevel, WeaponType};
#[derive(Copy, Clone, PartialEq, Eq)]
#[repr(u8)]
@ -91,9 +91,9 @@ impl GameEntity<(&mut Context, &mut Player, &mut Inventory, &mut HUD)> for Inven
if state.control_flags.control_enabled()
&& (player.controller.trigger_inventory()
|| player.controller.trigger_menu_back()
|| (player.controller.trigger_menu_ok() && self.focus == InventoryFocus::Weapons)
|| (state.settings.touch_controls && state.touch_controls.consume_click_in(slot_rect)))
|| player.controller.trigger_menu_back()
|| (player.controller.trigger_menu_ok() && self.focus == InventoryFocus::Weapons)
|| (state.settings.touch_controls && state.touch_controls.consume_click_in(slot_rect)))
{
state.control_flags.set_ok_button_disabled(false);
self.exit(state, player, inventory, hud);
@ -140,24 +140,36 @@ impl GameEntity<(&mut Context, &mut Player, &mut Inventory, &mut HUD)> for Inven
InventoryFocus::None => {
self.focus = InventoryFocus::Weapons;
state.control_flags.set_ok_button_disabled(false);
state.textscript_vm.start_script(self.get_weapon_event_number(inventory));
// check weapon count (0 count means we run item script)
let event = if self.weapon_count > 0 {
self.get_weapon_event_number(inventory)
} else {
self.get_item_event_number(inventory)
};
state.textscript_vm.start_script(event);
}
InventoryFocus::Weapons if state.control_flags.control_enabled() => {
if player.controller.trigger_left() {
state.sound_manager.play_sfx(4);
inventory.prev_weapon();
state.control_flags.set_ok_button_disabled(false);
state.textscript_vm.start_script(self.get_weapon_event_number(inventory));
// if we have no weapons, the TSC should not be refreshed with L/R keystrokes
if self.weapon_count > 0
{
if player.controller.trigger_left() {
state.sound_manager.play_sfx(4);
inventory.prev_weapon();
state.control_flags.set_ok_button_disabled(false);
state.textscript_vm.start_script(self.get_weapon_event_number(inventory));
}
if player.controller.trigger_right() {
state.sound_manager.play_sfx(4);
inventory.next_weapon();
state.control_flags.set_ok_button_disabled(false);
state.textscript_vm.start_script(self.get_weapon_event_number(inventory));
}
}
if player.controller.trigger_right() {
state.sound_manager.play_sfx(4);
inventory.next_weapon();
state.control_flags.set_ok_button_disabled(false);
state.textscript_vm.start_script(self.get_weapon_event_number(inventory));
}
if player.controller.trigger_up() || player.controller.trigger_down() {
// we should not move from the weapon row if there are no items
if (player.controller.trigger_up() || player.controller.trigger_down()) && self.item_count > 0 {
self.focus = InventoryFocus::Items;
state.control_flags.set_ok_button_disabled(false);
state.textscript_vm.start_script(self.get_item_event_number(inventory));
@ -310,8 +322,8 @@ impl GameEntity<(&mut Context, &mut Player, &mut Inventory, &mut HUD)> for Inven
let (item_cursor_frame, weapon_cursor_frame) = match self.focus {
InventoryFocus::None => (1, 1),
InventoryFocus::Weapons => (1, self.tick & 1),
InventoryFocus::Items => (self.tick & 1, 1),
InventoryFocus::Weapons => (1, (self.tick / 2) % 2), //every-other frame (& 1): this is not what we want, we want every 2 frames.
InventoryFocus::Items => ((self.tick / 2) % 2, 1),
};
batch.add_rect(

View File

@ -4,12 +4,13 @@ use crate::common::{Color, Rect};
use crate::framework::backend::{BackendTexture, SpriteBatchCommand};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::graphics;
use crate::framework::graphics;
use crate::game::player::Player;
use crate::game::scripting::tsc::text_script::TextScriptExecutionState;
use crate::game::shared_game_state::SharedGameState;
use crate::game::stage::Stage;
use crate::graphics::font::Font;
use crate::input::touch_controls::TouchControlType;
use crate::player::Player;
use crate::scripting::tsc::text_script::TextScriptExecutionState;
use crate::shared_game_state::{Language, SharedGameState};
use crate::stage::Stage;
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum MapSystemState {
@ -185,7 +186,7 @@ impl MapSystem {
}
let (scr_w, scr_h) = (state.canvas_size.0 * state.scale, state.canvas_size.1 * state.scale);
let text_height = state.font.line_height(&state.constants);
let text_height = state.font.line_height();
let rect_black_bar = Rect::new_size(
0,
(7.0 * state.scale) as _,
@ -197,16 +198,18 @@ impl MapSystem {
graphics::draw_rect(ctx, rect_black_bar, Color::new(0.0, 0.0, 0.0, 1.0))?;
}
let map_name = if state.settings.locale == Language::Japanese {
stage.data.name_jp.chars()
let map_name = if state.constants.is_cs_plus && state.settings.locale == "jp" {
stage.data.name_jp.as_str()
} else {
stage.data.name.chars()
stage.data.name.as_str()
};
let map_name_width = state.font.text_width(map_name.clone(), &state.constants);
let map_name_off_x = (state.canvas_size.0 - map_name_width) / 2.0;
state.font.draw_text(map_name, map_name_off_x, 9.0, &state.constants, &mut state.texture_set, ctx)?;
state.font.builder().center(state.canvas_size.0).y(9.0).draw(
map_name,
ctx,
&state.constants,
&mut state.texture_set,
)?;
let mut map_rect = Rect::new(0.0, 0.0, self.last_size.0 as f32, self.last_size.1 as f32);

View File

@ -1,5 +1,6 @@
pub mod background;
pub mod boss_life_bar;
pub mod compact_jukebox;
pub mod credits;
pub mod draw_common;
pub mod fade;

View File

@ -1,17 +1,17 @@
use byteorder::{ReadBytesExt, WriteBytesExt, LE};
use byteorder::{LE, ReadBytesExt, WriteBytesExt};
use crate::common::Rect;
use crate::components::draw_common::{draw_number, draw_number_zeros, Alignment};
use crate::components::draw_common::{Alignment, draw_number, draw_number_zeros};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::filesystem;
use crate::framework::vfs::OpenOptions;
use crate::player::Player;
use crate::rng::RNG;
use crate::scripting::tsc::text_script::TextScriptExecutionState;
use crate::shared_game_state::{SharedGameState, TimingMode};
use crate::game::frame::Frame;
use crate::game::shared_game_state::{SharedGameState, TimingMode};
use crate::game::player::Player;
use crate::game::scripting::tsc::text_script::TextScriptExecutionState;
use crate::util::rng::RNG;
#[derive(Clone, Copy)]
pub struct NikumaruCounter {
@ -63,7 +63,7 @@ impl NikumaruCounter {
let mut random_list: [u8; 4] = [0; 4];
for iter in 0..=3 {
random_list[iter] = state.game_rng.range(0..250) as u8 + iter as u8;
random_list[iter] = state.effect_rng.range(0..250) as u8 + iter as u8;
ticks[iter] = u32::from_le_bytes([
ticks[iter].to_le_bytes()[0].wrapping_add(random_list[iter]),

View File

@ -1,9 +1,9 @@
use crate::common::{interpolate_fix9_scale, Rect};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::shared_game_state::SharedGameState;
use crate::game::frame::Frame;
use crate::game::shared_game_state::SharedGameState;
#[derive(Debug, Copy, Clone)]
pub struct NumberPopup {
@ -13,43 +13,48 @@ pub struct NumberPopup {
pub prev_x: i32,
pub prev_y: i32,
counter: u16,
value_display: i16,
}
impl NumberPopup {
pub fn new() -> NumberPopup {
NumberPopup { value: 0, x: 0, y: 0, prev_x: 0, prev_y: 0, counter: 0 }
NumberPopup { value: 0, x: 0, y: 0, prev_x: 0, prev_y: 0, counter: 0, value_display: 0 }
}
pub fn set_value(&mut self, value: i16) {
if self.counter > 32 {
self.counter = 32;
}
self.value = value;
}
pub fn add_value(&mut self, value: i16) {
self.set_value(self.value + value);
}
pub fn update_displayed_value(&mut self) {
if self.counter > 32 {
self.counter = 32;
}
self.value_display += self.value;
self.value = 0;
}
}
impl GameEntity<()> for NumberPopup {
fn tick(&mut self, _state: &mut SharedGameState, _custom: ()) -> GameResult<()> {
if self.value == 0 {
if self.value_display == 0 {
return Ok(());
}
self.counter += 1;
if self.counter == 80 {
self.counter = 0;
self.value = 0;
self.value_display = 0;
}
Ok(())
}
fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, frame: &Frame) -> GameResult<()> {
if self.value == 0 {
if self.value_display == 0 {
return Ok(());
}
@ -63,9 +68,10 @@ impl GameEntity<()> for NumberPopup {
let (frame_x, frame_y) = frame.xy_interpolated(state.frame_time);
let x = interpolate_fix9_scale(self.prev_x, self.x, state.frame_time) - frame_x;
let y = interpolate_fix9_scale(self.prev_y, self.y, state.frame_time) - frame_y - y_offset;
let y = interpolate_fix9_scale(self.prev_y, self.y, state.frame_time) - frame_y - y_offset
- 3.0f32; // This is supposed to be -4, but for some reason -3 looks more accurate
let n = format!("{:+}", self.value);
let n = format!("{:+}", self.value_display);
let x = x - n.len() as f32 * 4.0;
@ -78,7 +84,7 @@ impl GameEntity<()> for NumberPopup {
batch.add_rect(x + offset as f32 * 8.0, y, &Rect::new_size(40, 48 + clip, 8, 8 - clip));
}
'0'..='9' => {
let number_set = if self.value < 0 { 64 } else { 56 };
let number_set = if self.value_display < 0 { 64 } else { 56 };
let idx = chr as u16 - '0' as u16;
batch.add_rect(
x + offset as f32 * 8.0,

View File

@ -1,17 +1,18 @@
use std::io::{Cursor, Read};
use byteorder::{ReadBytesExt, WriteBytesExt, LE};
use byteorder::{LE, ReadBytesExt, WriteBytesExt};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::filesystem;
use crate::framework::keyboard::ScanCode;
use crate::framework::vfs::OpenOptions;
use crate::game::frame::Frame;
use crate::game::shared_game_state::{ReplayKind, ReplayState, SharedGameState};
use crate::input::replay_player_controller::{KeyState, ReplayController};
use crate::player::Player;
use crate::shared_game_state::{ReplayState, SharedGameState};
use crate::game::player::Player;
use crate::graphics::font::Font;
#[derive(Clone)]
pub struct Replay {
@ -46,26 +47,42 @@ impl Replay {
}
}
pub fn stop_recording(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
pub fn stop_recording(
&mut self,
state: &mut SharedGameState,
ctx: &mut Context,
is_new_record: bool,
) -> GameResult {
state.replay_state = ReplayState::None;
self.write_replay(state, ctx)?;
self.write_replay(state, ctx, ReplayKind::Last)?;
if is_new_record {
self.write_replay(state, ctx, ReplayKind::Best)?;
}
Ok(())
}
pub fn initialize_playback(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
pub fn initialize_playback(
&mut self,
state: &mut SharedGameState,
ctx: &mut Context,
replay_kind: ReplayKind,
) -> GameResult {
if !self.is_active {
state.replay_state = ReplayState::Playback;
self.read_replay(state, ctx)?;
state.replay_state = ReplayState::Playback(replay_kind);
self.read_replay(state, ctx, replay_kind)?;
state.game_rng.load_state(self.rng_seed);
self.is_active = true;
}
Ok(())
}
fn write_replay(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
fn write_replay(&mut self, state: &mut SharedGameState, ctx: &mut Context, replay_kind: ReplayKind) -> GameResult {
if let Ok(mut file) = filesystem::open_options(
ctx,
[state.get_rec_filename(), ".rep".to_string()].join(""),
[state.get_rec_filename(), replay_kind.get_suffix()].join(""),
OpenOptions::new().write(true).create(true),
) {
file.write_u16::<LE>(0)?; // Space for versioning replay files
@ -77,8 +94,9 @@ impl Replay {
Ok(())
}
fn read_replay(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
if let Ok(mut file) = filesystem::user_open(ctx, [state.get_rec_filename(), ".rep".to_string()].join("")) {
fn read_replay(&mut self, state: &mut SharedGameState, ctx: &mut Context, replay_kind: ReplayKind) -> GameResult {
if let Ok(mut file) = filesystem::user_open(ctx, [state.get_rec_filename(), replay_kind.get_suffix()].join(""))
{
self.replay_version = file.read_u16::<LE>()?;
self.rng_seed = file.read_u64::<LE>()?;
@ -120,7 +138,7 @@ impl GameEntity<(&mut Context, &mut Player)> for Replay {
self.keylist.push(inputs);
}
ReplayState::Playback => {
ReplayState::Playback(_) => {
let pause = ctx.keyboard_context.is_key_pressed(ScanCode::Escape) && (self.tick - self.resume_tick > 3);
let next_input = if pause { 1 << 10 } else { *self.keylist.get(self.tick).unwrap_or(&0) };
@ -153,18 +171,15 @@ impl GameEntity<(&mut Context, &mut Player)> for Replay {
match state.replay_state {
ReplayState::None => {}
ReplayState::Playback => {
state.font.draw_text_with_shadow(
"PLAY".chars(),
x,
y,
&state.constants,
&mut state.texture_set,
ctx,
)?;
ReplayState::Playback(_) => {
state.font.builder()
.position(x, y)
.draw("PLAY", ctx, &state.constants, &mut state.texture_set)?;
}
ReplayState::Recording => {
state.font.draw_text_with_shadow("REC".chars(), x, y, &state.constants, &mut state.texture_set, ctx)?;
state.font.builder()
.position(x, y)
.draw("REC", ctx, &state.constants, &mut state.texture_set)?;
}
}

View File

@ -1,13 +1,12 @@
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::common::Rect;
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::game::frame::Frame;
use crate::game::shared_game_state::SharedGameState;
use crate::input::touch_controls::TouchControlType;
use crate::player::Player;
use crate::shared_game_state::SharedGameState;
use crate::scripting::tsc::text_script::ScriptMode;
use crate::game::player::Player;
use crate::game::scripting::tsc::text_script::ScriptMode;
pub struct StageSelect {
pub current_teleport_slot: u8,

View File

@ -1,13 +1,13 @@
use crate::common::{Color, Rect};
use crate::engine_constants::AnimatedFace;
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::graphics;
use crate::graphics::draw_rect;
use crate::scripting::tsc::text_script::{ConfirmSelection, TextScriptExecutionState, TextScriptLine};
use crate::shared_game_state::SharedGameState;
use crate::framework::graphics;
use crate::game::frame::Frame;
use crate::game::scripting::tsc::text_script::{ConfirmSelection, TextScriptExecutionState, TextScriptLine};
use crate::game::shared_game_state::SharedGameState;
use crate::graphics::font::{Font, Symbols};
pub struct TextBoxes {
pub slide_in: u8,
@ -38,6 +38,7 @@ impl GameEntity<()> for TextBoxes {
let animation = state.textscript_vm.face % 1000 / 100;
if state.constants.textscript.animated_face_pics
&& !state.settings.original_textures
&& (self.animated_face.anim_id != animation || self.animated_face.face_id != face_num)
{
self.animated_face = state
@ -154,19 +155,25 @@ impl GameEntity<()> for TextBoxes {
let face_num = state.textscript_vm.face % 100;
let animation_frame = self.animated_face.anim_frames.first().unwrap().0 as usize;
let tex_name =
if state.constants.textscript.animated_face_pics { SWITCH_FACE_TEX[animation_frame] } else { FACE_TEX };
let tex_name = if state.constants.textscript.animated_face_pics && !state.settings.original_textures {
SWITCH_FACE_TEX[animation_frame]
} else {
FACE_TEX
};
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, tex_name)?;
let face_x = (4.0 + (6 - self.slide_in) as f32 * 8.0) - 52.0;
batch.add_rect_flip(
left_pos + 14.0 + face_x,
top_pos + 8.0,
flip,
false,
&Rect::new_size((face_num as u16 % 6) * 48, (face_num as u16 / 6) * 48, 48, 48),
);
let final_x = left_pos + 14.0 + face_x;
let final_y = top_pos + 8.0;
let rect = Rect::new_size((face_num as u16 % 6) * 48, (face_num as u16 / 6) * 48, 48, 48);
if face_num >= 1 && face_num <= 4 && state.more_rust {
// sue
batch.add_rect_flip_tinted(final_x, final_y, flip, false, (200, 200, 255, 255), &rect);
} else {
batch.add_rect_flip(final_x, final_y, flip, false, &rect);
}
batch.draw(ctx)?;
graphics::set_clip_rect(ctx, None)?;
@ -220,54 +227,46 @@ impl GameEntity<()> for TextBoxes {
graphics::set_clip_rect(ctx, Some(clip_rect))?;
for (idx, line) in lines.iter().enumerate() {
if !line.is_empty() {
if state.constants.textscript.text_shadow {
state.font.draw_text_with_shadow(
line.iter().copied(),
left_pos + text_offset + 14.0,
top_pos + 10.0 + idx as f32 * 16.0 - y_offset,
&state.constants,
&mut state.texture_set,
ctx,
)?;
} else {
state.font.draw_text(
line.iter().copied(),
left_pos + text_offset + 14.0,
top_pos + 10.0 + idx as f32 * 16.0 - y_offset,
&state.constants,
&mut state.texture_set,
ctx,
)?;
}
let symbols = Symbols { symbols: &state.textscript_vm.substitution_rect_map, texture: "TextBox" };
state
.font
.builder()
.position(left_pos + text_offset + 14.0, top_pos + 10.0 + idx as f32 * 16.0 - y_offset)
.shadow(state.constants.textscript.text_shadow)
.with_symbols(Some(symbols))
.draw_iter(line.iter().copied(), ctx, &state.constants, &mut state.texture_set)?;
}
}
graphics::set_clip_rect(ctx, None)?;
if let TextScriptExecutionState::WaitInput(_, _, tick) = state.textscript_vm.state {
if tick > 10 {
let builder = state
.font
.builder()
.with_symbols(Some(Symbols { symbols: &state.textscript_vm.substitution_rect_map, texture: "" }));
let (mut x, y) = match state.textscript_vm.current_line {
TextScriptLine::Line1 => (
state.font.text_width(state.textscript_vm.line_1.iter().copied(), &state.constants),
top_pos + 10.0,
),
TextScriptLine::Line2 => (
state.font.text_width(state.textscript_vm.line_2.iter().copied(), &state.constants),
top_pos + 10.0 + 16.0,
),
TextScriptLine::Line3 => (
state.font.text_width(state.textscript_vm.line_3.iter().copied(), &state.constants),
top_pos + 10.0 + 32.0,
),
TextScriptLine::Line1 => {
(builder.compute_width_iter(state.textscript_vm.line_1.iter().copied()), top_pos + 10.0)
}
TextScriptLine::Line2 => {
(builder.compute_width_iter(state.textscript_vm.line_2.iter().copied()), top_pos + 10.0 + 16.0)
}
TextScriptLine::Line3 => {
(builder.compute_width_iter(state.textscript_vm.line_3.iter().copied()), top_pos + 10.0 + 32.0)
}
};
x += left_pos + text_offset + 14.0;
draw_rect(
graphics::draw_rect(
ctx,
Rect::new_size(
(x * state.scale) as isize,
(y * state.scale) as isize,
(5.0 * state.scale) as isize,
(state.font.line_height(&state.constants) * state.scale) as isize,
(state.font.line_height() * state.scale) as isize,
),
Color::from_rgb(255, 255, 255),
)?;

View File

@ -1,9 +1,9 @@
use crate::common::Rect;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::shared_game_state::{SharedGameState, TileSize};
use crate::stage::{BackgroundType, Stage, StageTexturePaths};
use crate::game::frame::Frame;
use crate::game::shared_game_state::{SharedGameState, TileSize};
use crate::game::stage::{BackgroundType, Stage, StageTexturePaths};
pub struct Tilemap {
tick: u32,
@ -133,7 +133,7 @@ impl Tilemap {
TileLayer::Foreground => {
let attr = stage.map.attrib[tile as usize];
if attr < 0x40 || attr >= 0x80 || attr == 0x43 {
if attr < 0x40 || attr >= 0x80 {
continue;
}

View File

@ -1,18 +1,18 @@
use std::cell::RefCell;
use crate::common::{Color, Rect};
use crate::frame::Frame;
use crate::framework::backend::{BackendShader, SpriteBatchCommand, VertexData};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics;
use crate::graphics::BlendMode;
use crate::map::{WaterParamEntry, WaterParams, WaterRegionType};
use crate::npc::list::NPCList;
use crate::physics::PhysicalEntity;
use crate::player::Player;
use crate::shared_game_state::SharedGameState;
use crate::stage::{BackgroundType, Stage};
use crate::framework::graphics::BlendMode;
use crate::game::frame::Frame;
use crate::game::map::{WaterParamEntry, WaterParams, WaterRegionType};
use crate::game::physics::PhysicalEntity;
use crate::game::shared_game_state::SharedGameState;
use crate::game::stage::{BackgroundType, Stage};
use crate::game::npc::list::NPCList;
use crate::game::player::Player;
const TENSION: f32 = 0.03;
const DAMPENING: f32 = 0.01;

View File

@ -1,11 +1,11 @@
use crate::common::{interpolate_fix9_scale, Direction, Rect};
use crate::common::{Direction, interpolate_fix9_scale, Rect};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::player::{Player, TargetPlayer};
use crate::shared_game_state::SharedGameState;
use crate::weapon::bullet::{Bullet, BulletManager};
use crate::game::frame::Frame;
use crate::game::shared_game_state::SharedGameState;
use crate::game::player::{Player, TargetPlayer};
use crate::game::weapon::bullet::{Bullet, BulletManager};
pub struct WhimsicalStar {
pub star: [Star; 3],

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1,198 @@
{
"name": "English",
"font": "csfont.fnt",
"font_scale": "0.5",
"common": {
"name": "doukutsu-rs",
"back": "< Back",
"yes": "Yes",
"no": "No",
"on": "ON",
"off": "OFF"
},
"menus": {
"main_menu": {
"start": "Start Game",
"challenges": "Challenges",
"options": "Options",
"editor": "Editor",
"jukebox": "Jukebox",
"quit": "Quit"
},
"pause_menu": {
"resume": "Resume",
"retry": "Retry",
"options": "Options",
"title": "Title",
"title_confirm": "Title?",
"quit": "Quit",
"quit_confirm": "Quit?",
"add_player2": "Add Player 2",
"drop_player2": "Drop Player 2"
},
"save_menu": {
"new": "New Save",
"delete_info": "Press Right to Delete",
"delete_confirm": "Delete?",
"invalid_save": "Invalid Save"
},
"difficulty_menu": {
"title": "Select Difficulty",
"easy": "Easy",
"normal": "Normal",
"hard": "Hard",
"difficulty_name": "Difficulty: {difficulty}",
"unknown": "(unknown)"
},
"coop_menu": {
"title": "Select Number of Players",
"one": "Single Player",
"two": "Two Players"
},
"skin_menu": {
"title": "Select Player 2's appearance",
"label": "Appearance:"
},
"challenge_menu": {
"start": "Start",
"no_replay": "No Replay",
"replay_best": "Replay Best",
"replay_last": "Replay Last",
"delete_replay": "Delete Best Replay"
},
"options_menu": {
"graphics": "Graphics...",
"graphics_menu": {
"window_mode": {
"entry": "Display mode:",
"windowed": "Windowed",
"fullscreen": "Fullscreen"
},
"lighting_effects": "Lighting effects:",
"weapon_light_cone": "Weapon light cone:",
"screen_shake": {
"entry": "Screen shake intensity:",
"full": "1x",
"half": "0.5x",
"off": "Off"
},
"motion_interpolation": "Motion interpolation:",
"subpixel_scrolling": "Subpixel scrolling:",
"original_textures": "Original textures:",
"seasonal_textures": "Seasonal textures:",
"renderer": "Renderer:",
"vsync_mode": {
"entry": "V-Sync:",
"uncapped": "Uncapped",
"uncapped_desc": "V-Sync Off.",
"vsync": "Enabled",
"vsync_desc": "V-Sync On.",
"vrr_1x": "Variable Refresh Rate (1x)",
"vrr_1x_desc": "Uses (G-/Free)Sync if available.",
"vrr_2x": "Variable Refresh Rate (2x)",
"vrr_2x_desc": "Uses (G-/Free)Sync if available.",
"vrr_3x": "Variable Refresh Rate (3x)",
"vrr_3x_desc": "Uses (G-/Free)Sync if available."
}
},
"sound": "Sound...",
"sound_menu": {
"music_volume": "Music Volume",
"effects_volume": "Effects Volume",
"bgm_interpolation": {
"entry": "BGM Interpolation:",
"linear": "Linear",
"linear_desc": "Fast, similar to freeware on Vista+",
"cosine": "Cosine",
"cosine_desc": "Cosine interpolation",
"cubic": "Cubic",
"cubic_desc": "Cubic interpolation",
"linear_lp": "Linear+LP",
"linear_lp_desc": "Slowest, similar to freeware on XP",
"nearest": "Nearest",
"nearest_desc": "Fastest, lowest quality"
},
"soundtrack": "Soundtrack: {soundtrack}"
},
"controls": "Controls...",
"controls_menu": {
"display_touch_controls": "Display touch controls:"
},
"language": "Language...",
"behavior": "Behavior...",
"behavior_menu": {
"game_timing": {
"entry": "Game timing:",
"50tps": "50tps (freeware)",
"60tps": "60tps (CS+)"
},
"pause_on_focus_loss": "Pause on focus loss:",
"cutscene_skip_method": {
"entry": "Cutscene Skip:",
"hold": "Hold to Skip",
"fastforward": "Fast-Forward",
"auto": "Auto"
},
"discord_rpc": "Discord Rich Presence:",
"allow_strafe": "Allow strafe:"
},
"links": "Links...",
"advanced": "Advanced...",
"advanced_menu": {
"open_user_data": "Open user data directory",
"open_game_data": "Open game data directory",
"make_portable": "Make portable user directory"
},
"portable_menu": {
"explanation": "This will create a local user data directory and copy your settings and save files.",
"restart_question": "Reload the game to use the new location?",
"restart": "Save and return to title",
"cancel": "Cancel"
}
},
"controls_menu": {
"select_player": {
"entry": "Select player:",
"player_1": "Player 1",
"player_2": "Player 2"
},
"controller": {
"entry": "Controller...",
"keyboard": "Keyboard"
},
"rebind": "Rebind...",
"rebind_menu": {
"up": "Up",
"down": "Down",
"left": "Left",
"right": "Right",
"jump": "Jump",
"shoot": "Shoot",
"prev_weapon": "Previous weapon",
"next_weapon": "Next weapon",
"inventory": "Inventory",
"map": "Map system",
"skip": "Skip",
"strafe": "Strafe",
"menu_ok": "Menu select/confirm",
"menu_back": "Menu back/cancel"
},
"rebind_confirm_menu": {
"title": "Press button for \"{control}\"",
"cancel": "(Esc to cancel)"
},
"rumble": "Rumble:",
"reset_confirm": "Reset...",
"reset_confirm_menu_title": "Reset controls?"
}
},
"soundtrack": {
"organya": "Organya",
"remastered": "Remastered",
"new": "New",
"famitracks": "Famitracks"
},
"game": {
"cutscene_skip": "Hold {key} to skip the cutscene"
}
}

View File

@ -0,0 +1,197 @@
{
"name": "Japanese",
"font": "csfontjp.fnt",
"font_scale": "0.5",
"common": {
"name": "doukutsu-rs",
"back": "< 戻る",
"yes": "はい",
"no": "いいえ",
"on": "オン",
"off": "オフ"
},
"menus": {
"main_menu": {
"start": "ゲームスタート",
"challenges": "チャレンジ",
"options": "オプション",
"editor": "レベルエディタ",
"jukebox": "ジュークボックス",
"quit": "辞める"
},
"pause_menu": {
"resume": "再開",
"retry": "リトライ",
"options": "設定",
"title": "メインメニュー",
"title_confirm": "メインメニュー?",
"quit": "辞める",
"quit_confirm": "辞める?",
"add_player2": "プレーヤー2を追加",
"drop_player2": "プレーヤー2を削除"
},
"save_menu": {
"new": "新しいデータ",
"delete_info": "右矢印キーで削除",
"delete_confirm": "消去?",
"invalid_save": "無効な保存"
},
"difficulty_menu": {
"title": "難易度選択",
"easy": "簡単",
"normal": "普通",
"hard": "難しい",
"difficulty_name": "難易度: {difficulty}",
"unknown": "(未知)"
},
"coop_menu": {
"title": "プレイヤー数を選択",
"one": "1人プレイ",
"two": "2人プレイ"
},
"skin_menu": {
"title": "プレーヤー2の外観を選択します",
"label": "外観:"
},
"challenge_menu": {
"start": "スタート",
"no_replay": "ノーリプレイ",
"replay_best": "ベストプレイを再生",
"replay_last": "最後のプレイを再生",
"delete_replay": "ベストリプレイを削除"
},
"options_menu": {
"graphics": "グラフィック",
"graphics_menu": {
"window_mode": {
"entry": "画面表示:",
"windowed": "ウィンドウ",
"fullscreen": "フルスクリーン"
},
"lighting_effects": "ライティング効果:",
"weapon_light_cone": "兵器のライトコーン:",
"screen_shake": {
"entry": "画面の揺れ:",
"full": "1x",
"half": "0.5x",
"off": "オフ"
},
"motion_interpolation": "モーション補間:",
"subpixel_scrolling": "サブピクセルスクロール:",
"original_textures": "オリジナルテクスチャ:",
"seasonal_textures": "季節ものテクスチャ:",
"renderer": "レンダラ:",
"vsync_mode": {
"entry": "V-Sync:",
"uncapped": "Uncapped",
"uncapped_desc": "V-Sync Off.",
"vsync": "Enabled",
"vsync_desc": "V-Sync On.",
"vrr_1x": "Variable Refresh Rate (1x)",
"vrr_1x_desc": "Uses (G-/Free)Sync if available.",
"vrr_2x": "Variable Refresh Rate (2x)",
"vrr_2x_desc": "Uses (G-/Free)Sync if available.",
"vrr_3x": "Variable Refresh Rate (3x)",
"vrr_3x_desc": "Uses (G-/Free)Sync if available."
}
},
"sound": "サウンド",
"sound_menu": {
"music_volume": "BGM音量",
"effects_volume": "サウンド音量",
"bgm_interpolation": {
"entry": "BGM内挿",
"linear": "線形補間",
"linear_desc": "速い、フリーウェア版に近いVista+",
"cosine": "余弦",
"cosine_desc": "余弦補間",
"cubic": "立方体",
"cubic_desc": "立方体補間",
"linear_lp": "線形補間+LP",
"linear_lp_desc": "最も遅い、フリーウェア版に近いXP",
"nearest": "最近傍",
"nearest_desc": "最速、最低品質"
},
"soundtrack": "サウンドトラック: {soundtrack}"
},
"controls": "ボタン変更",
"controls_menu": {
"display_touch_controls": "タッチコントロールを表示する: "
},
"language": "言語",
"behavior": "動作",
"behavior_menu": {
"game_timing": {
"entry": "ゲームのタイミング:",
"50tps": "50tps (freeware)",
"60tps": "60tps (CS+)"
},
"pause_on_focus_loss": "フォーカスが外れた時のポーズ:",
"cutscene_skip_method": {
"entry": "カットシーンをスキップ",
"hold": "を押し続け",
"fastforward": "はやおくり"
},
"discord_rpc": "Discord Rich Presence:",
"allow_strafe": "ストレイフを許可する:"
},
"links": "リンク",
"advanced": "詳細設定",
"advanced_menu": {
"open_user_data": "ユーザープロファイルを開く",
"open_game_data": "ゲームファイルを開く",
"make_portable": "ポータブルユーザーディレクトリを作成する"
},
"portable_menu": {
"explanation": "ローカルのユーザーデータディレクトリが作成され、設定とセーブファイルがそこにコピーされます。",
"restart_question": "新しい場所を使うには、ゲームを再起動しますか?",
"restart": "保存してタイトルに戻る",
"cancel": "キャンセル"
}
},
"controls_menu": {
"select_player": {
"entry": "プレイヤーを選択:",
"player_1": "プレーヤー 1",
"player_2": "プレーヤー 2"
},
"controller": {
"entry": "コントローラ",
"keyboard": "キーボード"
},
"rebind": "再バインド",
"rebind_menu": {
"up": "うえ",
"down": "した",
"left": "ひだり",
"right": "みぎ",
"jump": "ジャンプ",
"shoot": "ショット",
"prev_weapon": "前の武器",
"next_weapon": "次の武器",
"inventory": "在庫",
"map": "マップシステム",
"skip": "スキップ",
"strafe": "ストレイフ",
"menu_ok": "メニュー選択OK",
"menu_back": "メニュー残す/キャンセル"
},
"rebind_confirm_menu": {
"title": "新しい「ジャンプ」ボタンを押す",
"cancel": "(Escキーを押してキャンセル)"
},
"rumble": "ランブル",
"reset_confirm": "リセット",
"reset_confirm_menu_title": "ボタンをリセットしますか?"
}
},
"soundtrack": {
"organya": "オルガーニャ",
"remastered": "リマスター",
"new": "新",
"famitracks": "ファミトラック"
},
"game": {
"cutscene_skip": "{key} を押し続け、カットシーンをスキップ"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,13 +1,13 @@
use std::{fmt, io};
use std::fmt::Debug;
use std::io::Cursor;
use std::io::ErrorKind;
use std::io::SeekFrom;
use std::path::{Component, Path, PathBuf};
use std::{fmt, io};
use crate::framework::error::GameError::FilesystemError;
use crate::framework::error::GameResult;
use crate::framework::vfs::{OpenOptions, VFile, VMetadata, VFS};
use crate::framework::vfs::{OpenOptions, VFile, VFS, VMetadata};
#[derive(Debug)]
pub struct BuiltinFile(Cursor<&'static [u8]>);
@ -101,31 +101,100 @@ impl BuiltinFS {
FSNode::File("builtin_font.fnt", include_bytes!("builtin/builtin_font.fnt")),
FSNode::File("builtin_font_0.png", include_bytes!("builtin/builtin_font_0.png")),
FSNode::File("builtin_font_1.png", include_bytes!("builtin/builtin_font_1.png")),
FSNode::File("gamecontrollerdb.txt", include_bytes!("builtin/gamecontrollerdb.txt")),
FSNode::File("icon.bmp", include_bytes!("../../res/sue.bmp")),
FSNode::File(
"organya-wavetable-doukutsu.bin",
include_bytes!("builtin/organya-wavetable-doukutsu.bin"),
),
FSNode::File("touch.png", include_bytes!("builtin/touch.png")),
FSNode::Directory(
"shaders",
"builtin_data",
vec![
// FSNode::File("basic_150.vert.glsl", include_bytes!("builtin/shaders/basic_150.vert.glsl")),
// FSNode::File("water_150.frag.glsl", include_bytes!("builtin/shaders/water_150.frag.glsl")),
// FSNode::File("basic_es300.vert.glsl", include_bytes!("builtin/shaders/basic_es300.vert.glsl")),
// FSNode::File("water_es300.frag.glsl", include_bytes!("builtin/shaders/water_es300.frag.glsl")),
FSNode::File("buttons.png", include_bytes!("builtin/builtin_data/buttons.png")),
FSNode::File("triangles.png", include_bytes!("builtin/builtin_data/triangles.png")),
FSNode::Directory(
"headband",
vec![
FSNode::Directory(
"ogph",
vec![
FSNode::File(
"Casts.png",
include_bytes!("builtin/builtin_data/headband/ogph/Casts.png"),
),
FSNode::Directory(
"Npc",
vec![
FSNode::File(
"NpcGuest.png",
include_bytes!(
"builtin/builtin_data/headband/ogph/Npc/NpcGuest.png"
),
),
FSNode::File(
"NpcMiza.png",
include_bytes!(
"builtin/builtin_data/headband/ogph/Npc/NpcMiza.png"
),
),
FSNode::File(
"NpcRegu.png",
include_bytes!(
"builtin/builtin_data/headband/ogph/Npc/NpcRegu.png"
),
),
],
),
],
),
FSNode::Directory(
"plus",
vec![
FSNode::File(
"Casts.png",
include_bytes!("builtin/builtin_data/headband/plus/casts.png"),
),
FSNode::Directory(
"Npc",
vec![
FSNode::File(
"NpcGuest.png",
include_bytes!(
"builtin/builtin_data/headband/plus/npc/npcguest.png"
),
),
FSNode::File(
"NpcMiza.png",
include_bytes!(
"builtin/builtin_data/headband/plus/npc/npcmiza.png"
),
),
FSNode::File(
"NpcRegu.png",
include_bytes!(
"builtin/builtin_data/headband/plus/npc/npcregu.png"
),
),
],
),
],
),
],
),
FSNode::Directory(
"locale",
vec![
FSNode::File("en.json", include_bytes!("builtin/builtin_data/locale/en.json")),
FSNode::File("jp.json", include_bytes!("builtin/builtin_data/locale/jp.json")),
],
),
],
),
FSNode::Directory(
"lightmap",
vec![FSNode::File("spot.png", include_bytes!("builtin/lightmap/spot.png"))],
),
FSNode::Directory(
"locale",
vec![
FSNode::File("en.json", include_bytes!("builtin/locale/en.json")),
FSNode::File("jp.json", include_bytes!("builtin/locale/jp.json")),
],
),
],
)],
}
@ -209,7 +278,7 @@ impl VFS for BuiltinFS {
self.get_node(path).map(|v| v.to_metadata())
}
fn read_dir(&self, path: &Path) -> GameResult<Box<dyn Iterator<Item = GameResult<PathBuf>>>> {
fn read_dir(&self, path: &Path) -> GameResult<Box<dyn Iterator<Item=GameResult<PathBuf>>>> {
match self.get_node(path) {
Ok(FSNode::Directory(_, contents)) => {
let mut vec = Vec::new();

133
src/data/exe_parser.rs Normal file
View File

@ -0,0 +1,133 @@
use std::ops::Range;
use pelite::{
image::RT_BITMAP,
pe32::{headers::SectionHeaders, Pe, PeFile},
resources::{Directory, Entry, Name, Resources},
};
use crate::framework::error::{GameError::ParseError, GameResult};
#[derive(Debug)]
pub struct DataFile {
pub bytes: Vec<u8>,
pub name: String,
}
impl DataFile {
pub fn from(name: String, bytes: Vec<u8>) -> Self {
Self { name, bytes }
}
}
#[derive(Debug)]
pub struct ExeResourceDirectory {
pub name: String,
pub data_files: Vec<DataFile>,
}
impl ExeResourceDirectory {
pub fn new(name: String) -> Self {
Self { name, data_files: Vec::new() }
}
}
pub struct ExeParser<'a> {
pub image_base: u32,
pub resources: Resources<'a>,
pub section_headers: Box<&'a SectionHeaders>,
}
impl<'a> ExeParser<'a> {
pub fn from(file: &'a Vec<u8>) -> GameResult<Self> {
let pe = PeFile::from_bytes(file);
return match pe {
Ok(pe) => {
let resources = pe.resources();
if resources.is_err() {
return Err(ParseError("Failed to parse resources.".to_string()));
}
let section_headers = pe.section_headers();
let image_base = pe.nt_headers().OptionalHeader.ImageBase;
Ok(Self {
image_base,
resources: resources.unwrap(),
section_headers: Box::new(section_headers)
})
}
Err(_) => Err(ParseError("Failed to parse PE file".to_string())),
};
}
pub fn get_resource_dir(&self, name: String) -> GameResult<ExeResourceDirectory> {
let mut dir_data = ExeResourceDirectory::new(name.to_owned());
let path = format!("/{}", name.to_owned());
let dir = self.resources.find_dir(&path);
return match dir {
Ok(dir) => {
self.read_dir(dir, &mut dir_data, "unknown".to_string());
Ok(dir_data)
}
Err(_) => return Err(ParseError("Failed to find resource directory.".to_string())),
};
}
pub fn get_bitmap_dir(&self) -> GameResult<ExeResourceDirectory> {
let mut dir_data = ExeResourceDirectory::new("Bitmap".to_string());
let root = self.resources.root().unwrap();
let dir = root.get_dir(Name::Id(RT_BITMAP.into()));
return match dir {
Ok(dir) => {
self.read_dir(dir, &mut dir_data, "unknown".to_string());
Ok(dir_data)
}
Err(_) => return Err(ParseError("Failed to open bitmap directory.".to_string())),
};
}
pub fn get_named_section_byte_range(&self, name: String) -> GameResult<Option<Range<u32>>> {
let section_header = self.section_headers.by_name(name.as_bytes());
return match section_header {
Some(section_header) => Ok(Some(section_header.file_range())),
None => Ok(None),
};
}
fn read_dir(&self, directory: Directory, dir_data: &mut ExeResourceDirectory, last_dir_name: String) {
for dir in directory.entries() {
let raw_entry = dir.entry();
if raw_entry.is_err() {
continue;
}
if let Entry::Directory(entry) = raw_entry.unwrap() {
let dir_name = dir.name();
let name = match dir_name {
Ok(name) => name.to_string(),
Err(_) => last_dir_name.to_owned(),
};
self.read_dir(entry, dir_data, name);
}
if let Entry::DataEntry(entry) = raw_entry.unwrap() {
let entry_bytes = entry.bytes();
if entry_bytes.is_err() {
continue;
}
let bytes = entry_bytes.unwrap();
let data_file = DataFile::from(last_dir_name.to_owned(), bytes.to_vec());
dir_data.data_files.push(data_file);
}
}
}
}

3
src/data/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod builtin_fs;
pub mod exe_parser;
pub mod vanilla;

268
src/data/vanilla.rs Normal file
View File

@ -0,0 +1,268 @@
use std::{
env,
io::{Read, Write},
ops::Range,
path::PathBuf,
};
use byteorder::{LE, WriteBytesExt};
use crate::data::exe_parser::ExeParser;
use crate::framework::{
context::Context,
error::{GameError::ParseError, GameResult},
filesystem,
};
pub struct VanillaExtractor {
exe_buffer: Vec<u8>,
data_base_dir: String,
root: PathBuf,
}
const VANILLA_STAGE_COUNT: u32 = 95;
const VANILLA_STAGE_ENTRY_SIZE: u32 = 0xC8;
const VANILLA_STAGE_TABLE_SIZE: u32 = VANILLA_STAGE_COUNT * VANILLA_STAGE_ENTRY_SIZE;
trait RangeExt {
fn to_usize(&self) -> std::ops::Range<usize>;
}
impl RangeExt for Range<u32> {
fn to_usize(&self) -> std::ops::Range<usize> {
(self.start as usize)..(self.end as usize)
}
}
impl VanillaExtractor {
pub fn from(ctx: &mut Context, exe_name: String, data_base_dir: String) -> Option<Self> {
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
let mut vanilla_exe_path = env::current_dir().unwrap();
#[cfg(target_os = "android")]
let mut vanilla_exe_path = PathBuf::from(ndk_glue::native_activity().internal_data_path().to_string_lossy().to_string());
#[cfg(target_os = "horizon")]
let mut vanilla_exe_path = PathBuf::from("sdmc:/switch/doukutsu-rs/");
vanilla_exe_path.push(&exe_name);
log::info!("Looking for vanilla game executable at {:?}", vanilla_exe_path);
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
if !vanilla_exe_path.is_file() {
vanilla_exe_path = env::current_exe().unwrap();
vanilla_exe_path.pop();
vanilla_exe_path.push(&exe_name);
}
if !vanilla_exe_path.is_file() {
return None;
}
let mut root = vanilla_exe_path.clone();
root.pop();
log::info!("Found vanilla game executable, attempting to extract resources.");
if filesystem::exists(ctx, format!("{}/stage.sect", data_base_dir.clone())) {
log::info!("Vanilla resources are already extracted, not proceeding.");
return None;
}
let file = std::fs::File::open(vanilla_exe_path);
if file.is_err() {
log::error!("Failed to open vanilla game executable: {}", file.unwrap_err());
return None;
}
let mut exe_buffer = Vec::new();
let result = file.unwrap().read_to_end(&mut exe_buffer);
if result.is_err() {
log::error!("Failed to read vanilla game executable: {}", result.unwrap_err());
return None;
}
Some(Self { exe_buffer, data_base_dir, root })
}
pub fn extract_data(&self) -> GameResult {
let parser = ExeParser::from(&self.exe_buffer);
if parser.is_err() {
return Err(ParseError("Failed to create vanilla parser.".to_string()));
}
let parser = parser.unwrap();
self.extract_organya(&parser)?;
self.extract_bitmaps(&parser)?;
self.extract_stage_table(&parser)?;
Ok(())
}
fn deep_create_dir_if_not_exists(&self, path: PathBuf) -> GameResult {
if path.is_dir() {
return Ok(());
}
let result = std::fs::create_dir_all(path);
if result.is_err() {
return Err(ParseError(format!("Failed to create directory structure: {}", result.unwrap_err())));
}
Ok(())
}
fn extract_organya(&self, parser: &ExeParser) -> GameResult {
let orgs = parser.get_resource_dir("ORG".to_string());
if orgs.is_err() {
return Err(ParseError("Failed to retrieve Organya resource directory.".to_string()));
}
for org in orgs.unwrap().data_files {
let mut org_path = self.root.clone();
org_path.push(self.data_base_dir.clone());
org_path.push("Org/");
if self.deep_create_dir_if_not_exists(org_path.clone()).is_err() {
return Err(ParseError("Failed to create directory structure.".to_string()));
}
org_path.push(format!("{}.org", org.name));
let mut org_file = match std::fs::File::create(org_path) {
Ok(file) => file,
Err(_) => {
return Err(ParseError("Failed to create organya file.".to_string()));
}
};
let result = org_file.write_all(&org.bytes);
if result.is_err() {
return Err(ParseError("Failed to write organya file.".to_string()));
}
log::info!("Extracted organya file: {}", org.name);
}
Ok(())
}
fn extract_bitmaps(&self, parser: &ExeParser) -> GameResult {
let bitmaps = parser.get_bitmap_dir();
if bitmaps.is_err() {
return Err(ParseError("Failed to retrieve bitmap directory.".to_string()));
}
for bitmap in bitmaps.unwrap().data_files {
let mut data_path = self.root.clone();
data_path.push(self.data_base_dir.clone());
if self.deep_create_dir_if_not_exists(data_path.clone()).is_err() {
return Err(ParseError("Failed to create data directory structure.".to_string()));
}
data_path.push(format!("{}.pbm", bitmap.name));
let file = std::fs::File::create(data_path);
if file.is_err() {
return Err(ParseError("Failed to create bitmap file.".to_string()));
}
let mut file = file.unwrap();
file.write_u8(0x42)?; // B
file.write_u8(0x4D)?; // M
file.write_u32::<LE>(bitmap.bytes.len() as u32 + 0xE)?; // Size of BMP file
file.write_u32::<LE>(0)?; // unused null bytes
file.write_u32::<LE>(0x76)?; // Bitmap data offset (hardcoded for now, might wanna get the actual offset)
let result = file.write_all(&bitmap.bytes);
if result.is_err() {
return Err(ParseError("Failed to write bitmap file.".to_string()));
}
log::info!("Extracted bitmap file: {}", bitmap.name);
}
Ok(())
}
fn find_stage_table_offset(&self, parser: &ExeParser) -> Option<Range<u32>> {
let range = parser.get_named_section_byte_range(".csmap".to_string());
if range.is_err() {
return None;
}
let pattern = [
// add esp, 8
0x83u8, 0xc4, 0x08,
// mov eax, [ebp+arg_0]
0x8b, 0x45, 0x08,
// imul eax, 0C8h
0x69, 0xc0, 0xc8, 0x00, 0x00, 0x00,
// add eax, offset gTMT
0x05, // 0x??, 0x??, 0x??, 0x??
];
let text = parser.section_headers.by_name(".text")?;
let text_range = text.file_range().to_usize();
let text_range_start = text_range.start;
let offset = self.exe_buffer[text_range]
.windows(pattern.len())
.position(|window| window == pattern)?;
let offset = text_range_start + offset;
let offset = u32::from_le_bytes([
self.exe_buffer[offset + 13],
self.exe_buffer[offset + 14],
self.exe_buffer[offset + 15],
self.exe_buffer[offset + 16],
]);
log::info!("Found stage table offset: 0x{:X}", offset);
let section = parser.section_headers.by_rva(offset - parser.image_base)?;
let offset_inside_range = offset.checked_sub(section.VirtualAddress + parser.image_base)?;
let range = section.file_range();
let data_start = range.start + offset_inside_range;
let data_end = data_start + VANILLA_STAGE_TABLE_SIZE;
Some(data_start..data_end)
}
fn extract_stage_table(&self, parser: &ExeParser) -> GameResult {
let range = self.find_stage_table_offset(parser);
let range = match range {
Some(range) => range,
None => return Err(ParseError("Failed to retrieve stage table from executable.".to_string())),
};
let range = range.to_usize();
let byte_slice = &self.exe_buffer[range];
let mut stage_tbl_path = self.root.clone();
stage_tbl_path.push(self.data_base_dir.clone());
if self.deep_create_dir_if_not_exists(stage_tbl_path.clone()).is_err() {
return Err(ParseError("Failed to create data directory structure.".to_string()));
}
stage_tbl_path.push("stage.sect");
let mut stage_tbl_file = match std::fs::File::create(stage_tbl_path) {
Ok(file) => file,
Err(_) => {
return Err(ParseError("Failed to create stage table file.".to_string()));
}
};
let result = stage_tbl_file.write_all(byte_slice);
if result.is_err() {
return Err(ParseError("Failed to write to stage table file.".to_string()));
}
Ok(())
}
}

189
src/discord/mod.rs Normal file
View File

@ -0,0 +1,189 @@
use std::sync::Mutex;
use discord_rich_presence::{
activity::{Activity, Assets, Button},
DiscordIpc, DiscordIpcClient,
};
use crate::framework::error::GameResult;
use crate::game::{player::Player, shared_game_state::GameDifficulty, stage::StageData};
pub enum DiscordRPCState {
Initializing,
Idling,
InGame,
Jukebox,
}
pub struct DiscordRPC {
pub enabled: bool,
pub ready: bool,
client: DiscordIpcClient,
state: DiscordRPCState,
life: u16,
max_life: u16,
stage_name: String,
difficulty: Option<GameDifficulty>,
can_update: Mutex<bool>,
}
impl DiscordRPC {
pub fn new(app_id: &str) -> Self {
Self {
enabled: false,
ready: false,
client: DiscordIpcClient::new(app_id).unwrap(),
state: DiscordRPCState::Idling,
life: 0,
max_life: 0,
stage_name: String::new(),
difficulty: None,
can_update: Mutex::new(true),
}
}
pub fn start(&mut self) -> GameResult {
log::info!("Starting Discord RPC client...");
let mut can_update = self.can_update.lock().unwrap();
*can_update = false;
match self.client.connect() {
Ok(_) => {
self.ready = true;
*can_update = true;
Ok(())
}
Err(e) => {
log::warn!("Failed to start Discord RPC client (maybe Discord is not running?): {}", e);
Ok(())
}
}
}
fn update(&mut self) -> GameResult {
if !self.enabled || !self.ready {
return Ok(());
}
let mut can_update = self.can_update.lock().unwrap();
if !*can_update {
return Ok(());
}
*can_update = false;
let (state, details) = match self.state {
DiscordRPCState::Initializing => ("Initializing...".to_owned(), "Just started playing".to_owned()),
DiscordRPCState::Idling => ("In the menus".to_owned(), "Idling".to_owned()),
DiscordRPCState::InGame => {
(format!("Currently in: {}", self.stage_name), format!("HP: {} / {}", self.life, self.max_life))
}
DiscordRPCState::Jukebox => ("In the menus".to_owned(), "Listening to the soundtrack".to_owned()),
};
log::debug!("Updating Discord RPC state: {} - {}", state, details);
let mut activity_assets = Assets::new().large_image("drs");
if self.difficulty.is_some() {
let difficulty = self.difficulty.unwrap();
let asset_name = match difficulty {
GameDifficulty::Easy => "deasy",
GameDifficulty::Normal => "dnormal",
GameDifficulty::Hard => "dhard",
};
let asset_label = match difficulty {
GameDifficulty::Easy => "Easy",
GameDifficulty::Normal => "Normal",
GameDifficulty::Hard => "Hard",
};
activity_assets = activity_assets.small_image(asset_name).small_text(asset_label);
}
let activity = Activity::new()
.state(state.as_str())
.details(details.as_str())
.assets(activity_assets)
.buttons(vec![Button::new("doukutsu-rs on GitHub", "https://github.com/doukutsu-rs/doukutsu-rs")]);
match self.client.set_activity(activity) {
Ok(()) => {
*can_update = true;
log::debug!("Discord RPC state updated successfully");
}
Err(e) => log::error!("Failed to update Discord RPC state: {}", e),
};
Ok(()) // whatever
}
pub fn update_stage(&mut self, stage: &StageData) -> GameResult {
self.stage_name = stage.name.clone();
self.update()
}
pub fn update_hp(&mut self, player: &Player) -> GameResult {
self.life = player.life;
self.max_life = player.max_life;
self.update()
}
pub fn update_difficulty(&mut self, difficulty: GameDifficulty) -> GameResult {
self.difficulty = Some(difficulty);
self.update()
}
pub fn set_initializing(&mut self) -> GameResult {
self.set_state(DiscordRPCState::Initializing)
}
pub fn set_idling(&mut self) -> GameResult {
self.difficulty = None;
self.set_state(DiscordRPCState::Idling)
}
pub fn set_in_game(&mut self) -> GameResult {
self.set_state(DiscordRPCState::InGame)
}
pub fn set_in_jukebox(&mut self) -> GameResult {
self.set_state(DiscordRPCState::Jukebox)
}
pub fn set_state(&mut self, state: DiscordRPCState) -> GameResult {
self.state = state;
self.update()
}
pub fn clear(&mut self) -> GameResult {
if !self.ready {
return Ok(());
}
let _ = self.client.clear_activity();
Ok(())
}
pub fn dispose(&mut self) {
if !self.ready {
return;
}
let can_update = self.can_update.lock();
if can_update.is_ok() {
*can_update.unwrap() = false;
}
let _ = self.client.close();
}
}

View File

@ -2,14 +2,18 @@ use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
use imgui::{Image, MouseButton, Window, WindowFlags};
use imgui::{Image, MouseButton, Window};
use crate::common::{Color, Rect};
use crate::components::background::Background;
use crate::components::tilemap::{TileLayer, Tilemap};
use crate::frame::Frame;
use crate::stage::{Stage, StageTexturePaths};
use crate::{graphics, Context, GameResult, SharedGameState, I_MAG};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics;
use crate::game::shared_game_state::SharedGameState;
use crate::game::frame::Frame;
use crate::game::stage::{Stage, StageTexturePaths};
use crate::graphics::texture_set::I_MAG;
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum CurrentTool {

File diff suppressed because it is too large Load Diff

View File

@ -11,11 +11,12 @@ use crate::engine_constants::npcs::NPCConsts;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::filesystem;
use crate::framework::gamepad::{Axis, Button};
use crate::game::player::ControlMode;
use crate::game::scripting::tsc::text_script::TextScriptEncoding;
use crate::game::settings::Settings;
use crate::game::shared_game_state::{FontData, Season};
use crate::i18n::Locale;
use crate::player::ControlMode;
use crate::scripting::tsc::text_script::TextScriptEncoding;
use crate::settings::Settings;
use crate::shared_game_state::{Language, Season};
use crate::sound::pixtone::{Channel, Envelope, PixToneParameters, Waveform};
use crate::sound::SoundManager;
@ -66,7 +67,7 @@ pub struct GameConsts {
pub tile_offset_x: i32,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct CaretConsts {
pub offsets: [(i32, i32); 18],
pub bubble_left_rects: Vec<Rect<u16>>,
@ -96,34 +97,6 @@ pub struct TextureSizeTable {
sizes: HashMap<String, (u16, u16)>,
}
impl Clone for CaretConsts {
fn clone(&self) -> Self {
Self {
offsets: self.offsets,
bubble_left_rects: self.bubble_left_rects.clone(),
bubble_right_rects: self.bubble_right_rects.clone(),
projectile_dissipation_left_rects: self.projectile_dissipation_left_rects.clone(),
projectile_dissipation_right_rects: self.projectile_dissipation_right_rects.clone(),
projectile_dissipation_up_rects: self.projectile_dissipation_up_rects.clone(),
shoot_rects: self.shoot_rects.clone(),
zzz_rects: self.zzz_rects.clone(),
drowned_quote_left_rect: self.drowned_quote_left_rect,
drowned_quote_right_rect: self.drowned_quote_right_rect,
level_up_rects: self.level_up_rects.clone(),
level_down_rects: self.level_down_rects.clone(),
hurt_particles_rects: self.hurt_particles_rects.clone(),
explosion_rects: self.explosion_rects.clone(),
little_particles_rects: self.little_particles_rects.clone(),
exhaust_rects: self.exhaust_rects.clone(),
question_left_rect: self.question_left_rect,
question_right_rect: self.question_right_rect,
small_projectile_dissipation: self.small_projectile_dissipation.clone(),
empty_text: self.empty_text.clone(),
push_jump_key: self.push_jump_key.clone(),
}
}
}
#[derive(Debug, Copy, Clone)]
pub struct BulletData {
pub damage: u8,
@ -174,23 +147,13 @@ pub struct BulletRects {
pub b042_spur_trail_l3: [Rect<u16>; 6],
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct WeaponConsts {
pub bullet_table: Vec<BulletData>,
pub bullet_rects: BulletRects,
pub level_table: [[u16; 3]; 14],
}
impl Clone for WeaponConsts {
fn clone(&self) -> WeaponConsts {
WeaponConsts {
bullet_table: self.bullet_table.clone(),
bullet_rects: self.bullet_rects,
level_table: self.level_table,
}
}
}
#[derive(Debug, Copy, Clone)]
pub struct WorldConsts {
pub snack_rect: Rect<u16>,
@ -206,7 +169,7 @@ pub struct AnimatedFace {
#[derive(Debug, Clone)]
pub struct ExtraSoundtrack {
pub name: String,
pub id: String,
pub path: String,
pub available: bool,
}
@ -222,6 +185,7 @@ pub struct TextScriptConsts {
pub textbox_rect_bottom: Rect<u16>,
pub textbox_rect_yes_no: Rect<u16>,
pub textbox_rect_cursor: Rect<u16>,
pub textbox_item_marker_rect: Rect<u16>,
pub inventory_rect_top: Rect<u16>,
pub inventory_rect_middle: Rect<u16>,
pub inventory_rect_bottom: Rect<u16>,
@ -243,10 +207,11 @@ pub struct TextScriptConsts {
pub fade_ticks: i8,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct TitleConsts {
pub intro_text: String,
pub logo_rect: Rect<u16>,
pub logo_splash_rect: Rect<u16>,
pub menu_left_top: Rect<u16>,
pub menu_right_top: Rect<u16>,
pub menu_left_bottom: Rect<u16>,
@ -263,35 +228,32 @@ pub struct TitleConsts {
pub cursor_sue: [Rect<u16>; 4],
}
impl Clone for TitleConsts {
fn clone(&self) -> TitleConsts {
TitleConsts {
intro_text: self.intro_text.clone(),
logo_rect: self.logo_rect,
menu_left_top: self.menu_left_top,
menu_right_top: self.menu_right_top,
menu_left_bottom: self.menu_left_bottom,
menu_right_bottom: self.menu_right_bottom,
menu_top: self.menu_top,
menu_bottom: self.menu_bottom,
menu_middle: self.menu_middle,
menu_left: self.menu_left,
menu_right: self.menu_right,
cursor_quote: self.cursor_quote,
cursor_curly: self.cursor_curly,
cursor_toroko: self.cursor_toroko,
cursor_king: self.cursor_king,
cursor_sue: self.cursor_sue,
}
#[derive(Debug, Clone)]
pub struct GamepadConsts {
pub button_rects: HashMap<Button, [Rect<u16>; 4]>,
pub axis_rects: HashMap<Axis, [Rect<u16>; 4]>,
}
impl GamepadConsts {
fn rects(base: Rect<u16>) -> [Rect<u16>; 4] {
[
base,
Rect::new(base.left + 64, base.top, base.right + 64, base.bottom),
Rect::new(base.left + 128, base.top, base.right + 128, base.bottom),
Rect::new(base.left + 64, base.top + 128, base.right + 64, base.bottom + 128),
]
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct EngineConstants {
pub base_paths: Vec<String>,
pub is_cs_plus: bool,
pub is_switch: bool,
pub is_demo: bool,
pub supports_og_textures: bool,
pub has_difficulty_menu: bool,
pub supports_two_player: bool,
pub game: GameConsts,
pub player: PlayerConsts,
pub booster: BoosterConsts,
@ -304,49 +266,18 @@ pub struct EngineConstants {
pub title: TitleConsts,
pub inventory_dim_color: Color,
pub font_path: String,
pub font_scale: f32,
pub font_space_offset: f32,
pub soundtracks: Vec<ExtraSoundtrack>,
pub music_table: Vec<String>,
pub organya_paths: Vec<String>,
pub credit_illustration_paths: Vec<String>,
pub player_skin_paths: Vec<String>,
pub animated_face_table: Vec<AnimatedFace>,
pub string_table: HashMap<String, String>,
pub missile_flags: Vec<u16>,
pub locales: HashMap<String, Locale>,
}
impl Clone for EngineConstants {
fn clone(&self) -> EngineConstants {
EngineConstants {
base_paths: self.base_paths.clone(),
is_cs_plus: self.is_cs_plus,
is_switch: self.is_switch,
supports_og_textures: self.supports_og_textures,
game: self.game,
player: self.player,
booster: self.booster,
caret: self.caret.clone(),
world: self.world,
npc: self.npc,
weapon: self.weapon.clone(),
tex_sizes: self.tex_sizes.clone(),
textscript: self.textscript,
title: self.title.clone(),
inventory_dim_color: self.inventory_dim_color,
font_path: self.font_path.clone(),
font_scale: self.font_scale,
font_space_offset: self.font_space_offset,
soundtracks: self.soundtracks.clone(),
music_table: self.music_table.clone(),
organya_paths: self.organya_paths.clone(),
credit_illustration_paths: self.credit_illustration_paths.clone(),
animated_face_table: self.animated_face_table.clone(),
string_table: self.string_table.clone(),
missile_flags: self.missile_flags.clone(),
locales: self.locales.clone(),
}
}
pub locales: Vec<Locale>,
pub gamepad: GamepadConsts,
pub stage_encoding: Option<TextScriptEncoding>,
}
impl EngineConstants {
@ -355,7 +286,10 @@ impl EngineConstants {
base_paths: Vec::new(),
is_cs_plus: false,
is_switch: false,
is_demo: false,
supports_og_textures: false,
has_difficulty_menu: true,
supports_two_player: cfg!(not(target_os = "android")),
game: GameConsts {
intro_stage: 72,
intro_event: 100,
@ -1357,6 +1291,7 @@ impl EngineConstants {
"bkSunset" => (320, 240), // nxengine
"bkSunset480fix" => (480, 272), // nxengine
"bkWater" => (32, 48),
"buttons" => (256, 256),
"Bullet" => (320, 176),
"Caret" => (320, 240),
"casts" => (320, 240),
@ -1391,9 +1326,18 @@ impl EngineConstants {
"Face4" => (288, 240), // switch
"Face5" => (288, 240), // switch
"Fade" => (256, 32),
"headband/ogph/Casts" => (320, 240),
"headband/ogph/Npc/NpcGuest" => (320, 184),
"headband/ogph/Npc/NpcMiza" => (320, 240),
"headband/ogph/Npc/NpcRegu" => (320, 240),
"headband/plus/Casts" => (320, 240),
"headband/plus/Npc/NpcGuest" => (320, 184),
"headband/plus/Npc/NpcMiza" => (320, 240),
"headband/plus/Npc/NpcRegu" => (320, 240),
"ItemImage" => (256, 128),
"Loading" => (64, 8),
"MyChar" => (200, 64),
"mychar_p2" => (200, 384), // switch
"Npc/Npc0" => (32, 32),
"Npc/NpcAlmo1" => (320, 240),
"Npc/NpcAlmo2" => (320, 240),
@ -1475,6 +1419,7 @@ impl EngineConstants {
"Stage/PrtWhite" => (256, 240),
"TextBox" => (244, 144),
"Title" => (320, 48),
"triangles" => (20, 5),
},
textscript: TextScriptConsts {
encoding: TextScriptEncoding::ShiftJIS,
@ -1486,6 +1431,7 @@ impl EngineConstants {
textbox_rect_bottom: Rect { left: 0, top: 16, right: 244, bottom: 24 },
textbox_rect_yes_no: Rect { left: 152, top: 48, right: 244, bottom: 80 },
textbox_rect_cursor: Rect { left: 112, top: 88, right: 128, bottom: 104 },
textbox_item_marker_rect: Rect { left: 64, top: 48, right: 70, bottom: 54 },
inventory_rect_top: Rect { left: 0, top: 0, right: 244, bottom: 8 },
inventory_rect_middle: Rect { left: 0, top: 8, right: 244, bottom: 16 },
inventory_rect_bottom: Rect { left: 0, top: 16, right: 244, bottom: 24 },
@ -1518,6 +1464,7 @@ impl EngineConstants {
title: TitleConsts {
intro_text: "Studio Pixel presents".to_owned(),
logo_rect: Rect { left: 0, top: 0, right: 144, bottom: 40 },
logo_splash_rect: Rect { left: 0, top: 0, right: 0, bottom: 0 }, //Hidden so patches can display splash art / subtitle
menu_left_top: Rect { left: 0, top: 0, right: 8, bottom: 8 },
menu_right_top: Rect { left: 236, top: 0, right: 244, bottom: 8 },
menu_left_bottom: Rect { left: 0, top: 16, right: 8, bottom: 24 },
@ -1560,13 +1507,12 @@ impl EngineConstants {
},
inventory_dim_color: Color::from_rgba(0, 0, 0, 0),
font_path: "csfont.fnt".to_owned(),
font_scale: 1.0,
font_space_offset: 0.0,
soundtracks: vec![
ExtraSoundtrack { name: "Remastered".to_owned(), path: "/base/Ogg11/".to_owned(), available: false },
ExtraSoundtrack { name: "New".to_owned(), path: "/base/Ogg/".to_owned(), available: false },
ExtraSoundtrack { name: "Famitracks".to_owned(), path: "/base/ogg17/".to_owned(), available: false },
ExtraSoundtrack { name: "Ridiculon".to_owned(), path: "/base/ogg_ridic/".to_owned(), available: false },
ExtraSoundtrack { id: "remastered".to_owned(), path: "/base/Ogg11/".to_owned(), available: false },
ExtraSoundtrack { id: "new".to_owned(), path: "/base/Ogg/".to_owned(), available: false },
ExtraSoundtrack { id: "famitracks".to_owned(), path: "/base/ogg17/".to_owned(), available: false },
ExtraSoundtrack { id: "ridiculon".to_owned(), path: "/base/ogg_ridic/".to_owned(), available: false },
],
music_table: vec![
"xxxx".to_owned(),
@ -1620,18 +1566,46 @@ impl EngineConstants {
"/Resource/ORG/".to_owned(), // CSE2E
],
credit_illustration_paths: vec![
"".to_owned(),
String::new(),
"Resource/BITMAP/".to_owned(), // CSE2E
"endpic/".to_owned(), // NXEngine
],
player_skin_paths: vec!["MyChar".to_owned()],
animated_face_table: vec![AnimatedFace { face_id: 0, anim_id: 0, anim_frames: vec![(0, 0)] }],
string_table: HashMap::new(),
missile_flags: vec![200, 201, 202, 218, 550, 766, 880, 920, 1551],
locales: HashMap::new(),
locales: Vec::new(),
gamepad: GamepadConsts {
button_rects: HashMap::from([
(Button::North, GamepadConsts::rects(Rect::new(0, 0, 32, 16))),
(Button::South, GamepadConsts::rects(Rect::new(0, 16, 32, 32))),
(Button::East, GamepadConsts::rects(Rect::new(0, 32, 32, 48))),
(Button::West, GamepadConsts::rects(Rect::new(0, 48, 32, 64))),
(Button::DPadDown, GamepadConsts::rects(Rect::new(0, 64, 32, 80))),
(Button::DPadUp, GamepadConsts::rects(Rect::new(0, 80, 32, 96))),
(Button::DPadRight, GamepadConsts::rects(Rect::new(0, 96, 32, 112))),
(Button::DPadLeft, GamepadConsts::rects(Rect::new(0, 112, 32, 128))),
(Button::LeftShoulder, GamepadConsts::rects(Rect::new(32, 32, 64, 48))),
(Button::RightShoulder, GamepadConsts::rects(Rect::new(32, 48, 64, 64))),
(Button::Start, GamepadConsts::rects(Rect::new(32, 96, 64, 112))),
(Button::Back, GamepadConsts::rects(Rect::new(32, 112, 64, 128))),
(Button::LeftStick, GamepadConsts::rects(Rect::new(32, 0, 64, 16))),
(Button::RightStick, GamepadConsts::rects(Rect::new(32, 16, 64, 32))),
]),
axis_rects: HashMap::from([
(Axis::LeftX, GamepadConsts::rects(Rect::new(32, 0, 64, 16))),
(Axis::LeftY, GamepadConsts::rects(Rect::new(32, 0, 64, 16))),
(Axis::RightX, GamepadConsts::rects(Rect::new(32, 16, 64, 32))),
(Axis::RightY, GamepadConsts::rects(Rect::new(32, 16, 64, 32))),
(Axis::TriggerLeft, GamepadConsts::rects(Rect::new(32, 64, 64, 80))),
(Axis::TriggerRight, GamepadConsts::rects(Rect::new(32, 80, 64, 96))),
]),
},
stage_encoding: None,
}
}
pub fn apply_csplus_patches(&mut self, sound_manager: &SoundManager) {
pub fn apply_csplus_patches(&mut self, sound_manager: &mut SoundManager) {
log::info!("Applying Cave Story+ constants patches...");
self.is_cs_plus = true;
@ -1641,7 +1615,7 @@ impl EngineConstants {
self.tex_sizes.insert("Npc/NpcRegu".to_owned(), (320, 410));
self.tex_sizes.insert("ui".to_owned(), (128, 32));
self.textscript.reset_invicibility_on_any_script = false;
self.title.logo_rect = Rect { left: 0, top: 0, right: 214, bottom: 50 };
self.title.logo_rect = Rect { left: 0, top: 0, right: 216, bottom: 48 };
self.title.menu_left_top = Rect { left: 0, top: 0, right: 4, bottom: 4 };
self.title.menu_right_top = Rect { left: 12, top: 0, right: 16, bottom: 4 };
@ -1682,6 +1656,10 @@ impl EngineConstants {
let _ = sound_manager.set_sample_params(2, typewriter_sample);
}
pub fn is_base(&self) -> bool {
!self.is_switch && !self.is_cs_plus && !self.is_demo
}
pub fn apply_csplus_nx_patches(&mut self) {
log::info!("Applying Switch-specific Cave Story+ constants patches...");
@ -1702,10 +1680,23 @@ impl EngineConstants {
self.textscript.fade_ticks = 21;
self.game.tile_offset_x = 3;
self.game.new_game_player_pos = (13, 8);
self.player_skin_paths.push("mychar_p2".to_owned());
}
pub fn apply_csdemo_patches(&mut self) {
log::info!("Applying Wiiware DEMO-specific Cave Story+ constants patches...");
self.is_demo = true;
self.supports_og_textures = true;
self.game.new_game_stage = 11;
self.game.new_game_event = 302;
self.game.new_game_player_pos = (8, 6);
self.title.logo_splash_rect = Rect { left: 224, top: 0, right: 320, bottom: 48 };
}
pub fn rebuild_path_list(&mut self, mod_path: Option<String>, season: Season, settings: &Settings) {
self.base_paths.clear();
self.base_paths.push("/builtin/builtin_data/".to_owned());
self.base_paths.push("/".to_owned());
if self.is_cs_plus {
@ -1721,12 +1712,12 @@ impl EngineConstants {
}
}
if settings.locale != Language::English {
self.base_paths.insert(0, format!("/base/{}/", settings.locale.to_language_code()));
if settings.locale != "en".to_string() {
self.base_paths.insert(0, format!("/base/{}/", settings.locale));
}
} else {
if settings.locale != Language::English {
self.base_paths.insert(0, format!("/{}/", settings.locale.to_language_code()));
if settings.locale != "en".to_string() {
self.base_paths.insert(0, format!("/{}/", settings.locale));
}
}
@ -1792,9 +1783,29 @@ impl EngineConstants {
}
pub fn load_locales(&mut self, ctx: &mut Context) -> GameResult {
for language in Language::values() {
self.locales.insert(language.to_string(), Locale::new(ctx, language.to_language_code(), language.font()));
log::info!("Loaded locale {} ({}).", language.to_string(), language.to_language_code());
self.locales.clear();
let locale_files = filesystem::read_dir_find(ctx, &self.base_paths, "locale/");
for locale_file in locale_files.unwrap() {
if locale_file.extension().unwrap() != "json" {
continue;
}
let locale_code = {
let filename = locale_file.file_name().unwrap().to_string_lossy();
let mut parts = filename.split('.');
parts.next().unwrap().to_string()
};
let mut locale = Locale::new(ctx, &self.base_paths, &locale_code);
if locale_code == "jp" && filesystem::exists(ctx, "/base/credit_jp.tsc") {
locale.set_font(FontData::new("csfontjp.fnt".to_owned(), 0.5, 0.0));
}
self.locales.push(locale.clone());
log::info!("Loaded locale {} ({})", locale_code, locale.name.clone());
}
Ok(())
@ -1803,7 +1814,7 @@ impl EngineConstants {
pub fn apply_constant_json_files(&mut self) {}
pub fn load_texture_size_hints(&mut self, ctx: &mut Context) -> GameResult {
if let Ok(file) = filesystem::open_find(ctx, &self.base_paths, "/texture_sizes.json") {
if let Ok(file) = filesystem::open_find(ctx, &self.base_paths, "texture_sizes.json") {
match serde_json::from_reader::<_, TextureSizeTable>(file) {
Ok(tex_overrides) => {
for (key, (x, y)) in tex_overrides.sizes {
@ -1820,7 +1831,7 @@ impl EngineConstants {
/// even though they match vanilla 1:1, we should load them for completeness
/// or if any crazy person uses it for a CS+ mod...
pub fn load_csplus_tables(&mut self, ctx: &mut Context) -> GameResult {
if let Ok(mut file) = filesystem::open_find(ctx, &self.base_paths, "/bullet.tbl") {
if let Ok(mut file) = filesystem::open_find(ctx, &self.base_paths, "bullet.tbl") {
let mut data = Vec::new();
file.read_to_end(&mut data)?;
let bullets = data.len() / 0x2A;
@ -1851,7 +1862,7 @@ impl EngineConstants {
log::info!("Loaded bullet.tbl.");
}
if let Ok(mut file) = filesystem::open_find(ctx, &self.base_paths, "/arms_level.tbl") {
if let Ok(mut file) = filesystem::open_find(ctx, &self.base_paths, "arms_level.tbl") {
let mut data = Vec::new();
file.read_to_end(&mut data)?;
let mut f = Cursor::new(data);
@ -1879,7 +1890,7 @@ impl EngineConstants {
// Bugfix for Malco cutscene - this face should be used but the original tsc has the wrong ID
self.animated_face_table.push(AnimatedFace { face_id: 5, anim_id: 4, anim_frames: vec![(4, 0)] });
if let Ok(file) = filesystem::open_find(ctx, &self.base_paths, "/faceanm.dat") {
if let Ok(file) = filesystem::open_find(ctx, &self.base_paths, "faceanm.dat") {
let buf = BufReader::new(file);
let mut face_id = 1;
let mut anim_id = 0;

View File

@ -2,8 +2,8 @@ use std::fmt::Debug;
use std::marker::PhantomData;
use std::ops::Index;
use serde::de::{Error, SeqAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
use serde::de::{Error, SeqAccess, Visitor};
use crate::common::Rect;
use crate::macros::fmt::Formatter;
@ -14,8 +14,8 @@ pub struct SafeNPCRect<const T: usize>(pub [Rect<u16>; T]);
impl<const T: usize> Serialize for SafeNPCRect<T> {
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
@ -31,8 +31,8 @@ impl<'de, const T: usize> Visitor<'de> for SafeNPCRectArrayVisitor<T> {
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
where
A: SeqAccess<'de>,
{
let mut rects = [Rect::default(); T];
for (i, rect) in rects.iter_mut().enumerate() {
@ -46,8 +46,8 @@ impl<'de, const T: usize> Visitor<'de> for SafeNPCRectArrayVisitor<T> {
impl<'de, const T: usize> Deserialize<'de> for SafeNPCRect<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
where
D: Deserializer<'de>,
{
deserializer.deserialize_seq(SafeNPCRectArrayVisitor(PhantomData)).map(SafeNPCRect)
}

View File

@ -1,8 +1,8 @@
use crate::common::interpolate_fix9_scale;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::shared_game_state::SharedGameState;
use crate::game::frame::Frame;
use crate::game::shared_game_state::SharedGameState;
pub trait GameEntity<C> {
fn tick(&mut self, state: &mut SharedGameState, custom: C) -> GameResult;

View File

@ -1,12 +1,12 @@
use std::any::Any;
use imgui::DrawData;
use crate::common::{Color, Rect};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics::BlendMode;
use crate::Game;
use crate::graphics::VSyncMode;
use crate::framework::graphics::{BlendMode, VSyncMode};
use crate::game::Game;
#[repr(C)]
#[derive(Copy, Clone)]
@ -25,13 +25,17 @@ pub enum BackendShader {
}
pub trait Backend {
fn create_event_loop(&self) -> GameResult<Box<dyn BackendEventLoop>>;
fn create_event_loop(&self, ctx: &Context) -> GameResult<Box<dyn BackendEventLoop>>;
fn as_any(&self) -> &dyn Any;
}
pub trait BackendEventLoop {
fn run(&mut self, game: &mut Game, ctx: &mut Context);
fn new_renderer(&self, ctx: *mut Context) -> GameResult<Box<dyn BackendRenderer>>;
fn as_any(&self) -> &dyn Any;
}
pub trait BackendRenderer {
@ -41,7 +45,9 @@ pub trait BackendRenderer {
fn present(&mut self) -> GameResult;
fn set_vsync_mode(&mut self, _mode: VSyncMode) -> GameResult { Ok(()) }
fn set_vsync_mode(&mut self, _mode: VSyncMode) -> GameResult {
Ok(())
}
fn prepare_draw(&mut self, _width: f32, _height: f32) -> GameResult {
Ok(())
@ -79,6 +85,8 @@ pub trait BackendRenderer {
texture: Option<&Box<dyn BackendTexture>>,
shader: BackendShader,
) -> GameResult;
fn as_any(&self) -> &dyn Any;
}
pub trait BackendTexture {
@ -93,12 +101,23 @@ pub trait BackendTexture {
fn as_any(&self) -> &dyn Any;
}
pub trait BackendGamepad {
fn set_rumble(&mut self, low_freq: u16, high_freq: u16, duration_ms: u32) -> GameResult;
fn instance_id(&self) -> u32;
}
#[allow(unreachable_code)]
pub fn init_backend(headless: bool, size_hint: (u16, u16)) -> GameResult<Box<dyn Backend>> {
if headless {
return crate::framework::backend_null::NullBackend::new();
}
#[cfg(all(feature = "backend-horizon"))]
{
return crate::framework::backend_horizon::HorizonBackend::new();
}
#[cfg(all(feature = "backend-glutin"))]
{
return crate::framework::backend_glutin::GlutinBackend::new();
@ -117,4 +136,5 @@ pub enum SpriteBatchCommand {
DrawRect(Rect<f32>, Rect<f32>),
DrawRectFlip(Rect<f32>, Rect<f32>, bool, bool),
DrawRectTinted(Rect<f32>, Rect<f32>, Color),
DrawRectFlipTinted(Rect<f32>, Rect<f32>, bool, bool, Color),
}

View File

@ -1,23 +1,29 @@
use std::any::Any;
use std::cell::{RefCell, UnsafeCell};
use std::ffi::c_void;
use std::io::Read;
use std::mem;
use std::rc::Rc;
use std::sync::Arc;
use std::vec::Vec;
use glutin::{Api, ContextBuilder, GlProfile, GlRequest, PossiblyCurrent, WindowedContext};
use glutin::event::{ElementState, Event, TouchPhase, VirtualKeyCode, WindowEvent};
use glutin::event_loop::{ControlFlow, EventLoop};
use glutin::window::WindowBuilder;
use glutin::{Api, ContextBuilder, GlProfile, GlRequest, PossiblyCurrent, WindowedContext};
use imgui::{DrawCmdParams, DrawData, DrawIdx, DrawVert};
use winit::window::Icon;
use crate::{Game, GAME_SUSPENDED};
use crate::common::Rect;
use crate::framework::backend::{Backend, BackendEventLoop, BackendRenderer, BackendTexture, SpriteBatchCommand};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::filesystem;
use crate::framework::gl;
use crate::framework::keyboard::ScanCode;
use crate::framework::render_opengl::{GLContext, OpenGLRenderer};
use crate::game::Game;
use crate::game::GAME_SUSPENDED;
use crate::input::touch_controls::TouchPoint;
pub struct GlutinBackend;
@ -29,7 +35,7 @@ impl GlutinBackend {
}
impl Backend for GlutinBackend {
fn create_event_loop(&self) -> GameResult<Box<dyn BackendEventLoop>> {
fn create_event_loop(&self, _ctx: &Context) -> GameResult<Box<dyn BackendEventLoop>> {
#[cfg(target_os = "android")]
loop {
match ndk_glue::native_window().as_ref() {
@ -43,6 +49,10 @@ impl Backend for GlutinBackend {
Ok(Box::new(GlutinEventLoop { refs: Rc::new(UnsafeCell::new(None)) }))
}
fn as_any(&self) -> &dyn Any {
self
}
}
pub struct GlutinEventLoop {
@ -50,17 +60,19 @@ pub struct GlutinEventLoop {
}
impl GlutinEventLoop {
fn get_context(&self, event_loop: &EventLoop<()>) -> &mut WindowedContext<PossiblyCurrent> {
fn get_context(&self, ctx: &Context, event_loop: &EventLoop<()>) -> &mut WindowedContext<PossiblyCurrent> {
let mut refs = unsafe { &mut *self.refs.get() };
if refs.is_none() {
let mut window = WindowBuilder::new();
let windowed_context = ContextBuilder::new();
let windowed_context = windowed_context.with_gl(GlRequest::Specific(Api::OpenGl, (3, 0)));
#[cfg(target_os = "android")]
let windowed_context = windowed_context.with_gl(GlRequest::Specific(Api::OpenGlEs, (2, 0)));
let windowed_context = windowed_context.with_gl_profile(GlProfile::Core)
let windowed_context = windowed_context
.with_gl_profile(GlProfile::Core)
.with_gl_debug_flag(false)
.with_pixel_format(24, 8)
.with_vsync(true);
@ -72,7 +84,24 @@ impl GlutinEventLoop {
}
window = window.with_title("doukutsu-rs");
#[cfg(not(any(target_os = "windows", target_os = "android", target_os = "horizon")))]
{
let mut file = filesystem::open(&ctx, "/builtin/icon.bmp").unwrap();
let mut buf: Vec<u8> = Vec::new();
file.read_to_end(&mut buf);
let mut img = match image::load_from_memory_with_format(buf.as_slice(), image::ImageFormat::Bmp) {
Ok(image) => image.into_rgba8(),
Err(e) => panic!("Cannot set window icon")
};
let (width, height) = img.dimensions();
let icon = Icon::from_rgba(img.into_raw(), width, height).unwrap();
window = window.with_window_icon(Some(icon));
}
let windowed_context = windowed_context.build_windowed(window, event_loop).unwrap();
let windowed_context = unsafe { windowed_context.make_current().unwrap() };
@ -111,18 +140,24 @@ fn request_android_redraw() {
#[cfg(target_os = "android")]
fn get_insets() -> GameResult<(f32, f32, f32, f32)> {
unsafe {
use jni::objects::JObject;
use jni::JavaVM;
let vm_ptr = ndk_glue::native_activity().vm();
let vm = unsafe { jni::JavaVM::from_raw(vm_ptr) }?;
let vm = JavaVM::from_raw(vm_ptr)?;
let vm_env = vm.attach_current_thread()?;
//let class = vm_env.find_class("io/github/doukutsu_rs/MainActivity")?;
let class = vm_env.new_global_ref(ndk_glue::native_activity().activity())?;
let class = vm_env.new_global_ref(JObject::from_raw(ndk_glue::native_activity().activity()))?;
let field = vm_env.get_field(class.as_obj(), "displayInsets", "[I")?.to_jni().l as jni::sys::jintArray;
let mut elements = [0; 4];
vm_env.get_int_array_region(field, 0, &mut elements)?;
vm_env.delete_local_ref(field.into());
vm_env.delete_local_ref(JObject::from_raw(field));
//Game always runs with horizontal orientation so top and bottom cutouts not needed and only wastes piece of the screen
elements[1] = 0;
elements[3] = 0;
Ok((elements[0] as f32, elements[1] as f32, elements[2] as f32, elements[3] as f32))
}
@ -140,8 +175,7 @@ impl BackendEventLoop for GlutinEventLoop {
let event_loop = EventLoop::new();
let state_ref = unsafe { &mut *game.state.get() };
let window: &'static mut WindowedContext<PossiblyCurrent> =
unsafe { std::mem::transmute(self.get_context(&event_loop)) };
unsafe { std::mem::transmute(self.get_context(&ctx, &event_loop)) };
{
let size = window.window().inner_size();
ctx.real_screen_size = (size.width, size.height);
@ -288,6 +322,17 @@ impl BackendEventLoop for GlutinEventLoop {
}
}
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
{
if state_ref.settings.window_mode.get_glutin_fullscreen_type() != window.window().fullscreen() {
let fullscreen_type = state_ref.settings.window_mode.get_glutin_fullscreen_type();
let cursor_visible = state_ref.settings.window_mode.should_display_mouse_cursor();
window.window().set_fullscreen(fullscreen_type);
window.window().set_cursor_visible(cursor_visible);
}
}
game.update(ctx).unwrap();
#[cfg(target_os = "android")]
@ -319,7 +364,7 @@ impl BackendEventLoop for GlutinEventLoop {
});
}
fn new_renderer(&self, _ctx: *mut Context) -> GameResult<Box<dyn BackendRenderer>> {
fn new_renderer(&self, ctx: *mut Context) -> GameResult<Box<dyn BackendRenderer>> {
let mut imgui = imgui::Context::create();
imgui.io_mut().display_size = [640.0, 480.0];
@ -338,7 +383,7 @@ impl BackendEventLoop for GlutinEventLoop {
std::ptr::null()
}
};
*user_data = Rc::into_raw(refs) as *mut c_void;
result
@ -358,10 +403,14 @@ impl BackendEventLoop for GlutinEventLoop {
*user_data = Rc::into_raw(refs) as *mut c_void;
}
let gl_context = GLContext { gles2_mode: true, get_proc_address, swap_buffers, user_data };
let gl_context = GLContext { gles2_mode: true, is_sdl: false, get_proc_address, swap_buffers, user_data, ctx };
Ok(Box::new(OpenGLRenderer::new(gl_context, UnsafeCell::new(imgui))))
}
fn as_any(&self) -> &dyn Any {
self
}
}
fn conv_keycode(code: VirtualKeyCode) -> Option<ScanCode> {

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